davetapley
(Dave Tapley)
October 30, 2024, 9:18pm
1
This library has recently added typing, and you can see a good artifact of a previous design choice causing complication / confusion.
It used a CONSUME_MULTIPLE_SEGMENTS
class variable to decide whether an abstract convert
method should receive str
or list[str]
.
It’s currently incorrectly hard coded to str
, which is a bug:
Typing for BaseConverter convert doesn't respect CONSUME_MULTIPLE_SEGMENTS · Issue #2396 · falconry/falcon · GitHub .
Simple fix is to replace str
with str | list[str]
, but then the implementor needs to assert the concrete type, even though it is known based on the value of CONSUME_MULTIPLE_SEGMENTS
.
I assume there’s no way to type convert
based on the ClassVar
, but I wanted to check
class BaseConverter(metaclass=abc.ABCMeta):
"""Abstract base class for URI template field converters."""
CONSUME_MULTIPLE_SEGMENTS: ClassVar[bool] = False
"""When set to ``True`` it indicates that this converter will consume
multiple URL path segments. Currently a converter with
``CONSUME_MULTIPLE_SEGMENTS=True`` must be at the end of the URL template
effectively meaning that it will consume all of the remaining URL path
segments.
"""
@abc.abstractmethod
def convert(self, value: str) -> Any:
"""Convert a URI template field value to another format or type.
Args:
value (str or List[str]): Original string to convert.
Not certain it will work, but perhaps worth trying is to use a union of two narrow definitions instead of a base class.
class SingleConverter(Protocol):
CONSUME_MULTIPLE_SEGMENTS: Literal[False]
def convert(self, value: str, /) -> Any: ...
class MultiConverter(Protocol):
CONSUME_MULTIPLE_SEGMENTS: Literal[True]
def convert(self, value: Sequence[str], /) -> Any: ...
Converter: TypeAlias = SingleConverter | MultiConverter
Usually this pattern also works well in conjunction with having a (private) base class if needed.
1 Like
srittau
(Sebastian Rittau)
October 31, 2024, 10:22am
3
I haven’t tried it, but depending on how CONSUME_MULTIPLE_SEGMENTS
is set (by sub-classes?) you might be able to make this work using some trickery using Generic
:
# untested
_B = TypeVar("_B", bool, default=False)
class BaseConverter(Generic[_B]):
CONSUME_MULTIPLE_SEGMENTS: ClassVar[_B] = False
@overload
@abc.abstractmethod
def convert(self: Self[False], value: str) -> Any: ...
@overload
@abc.abstractmethod
def convert(self: Self[True], value: list[str]) -> Any: ...
def convert(self: Self[True], value: list[str]) -> Any: pass
class ConverterImpl(BaseConverter[True]):
CONSUME_MULTIPLE_SEGMENTS = True
# probably won't work easily
def convert(self: Self[True], value: list[str]) -> Any:
pass
Alternatively, in sub-classes just define the allowed version and # type: ignore
the overload errors.
I’m not sure that any of those solutions (or rather “hacks”) really works for your use case.
Eneg
(Eneg)
November 3, 2024, 12:14pm
4
Sadly, type variables are not allowed in ClassVar
s: Class type assignability — typing documentation
Note that a ClassVar
parameter cannot include any type variables, regardless of the level of nesting: ClassVar[T]
and ClassVar[list[set[T]]]
are both invalid if T
is a type variable.
1 Like