Can I use this to define a proxy/wrapper type?
jace opened this issue · comments
Something like this:
class ProxyWrapper(Generic[T]):
if not TYPE_CHECKING:
def __init__(self, obj: T):
object.__setattr(self, '_obj', obj)
def __getattr__(self, attr) -> Any:
# Logic to allow or deny this attr
return getattr(self._obj, attr)
def __setattr__(self, attr, value): ... # Similar
@classmethod
def wrap(cls, obj: T2) -> Has[ProxyWrapper[T2], T2]:
return cls(obj)
The expectation being that the proxy wrapper now has the same attributes as the type it wraps, except where explicitly overridden in the proxy. Is this possible?
Not sure what you're trying to achieve. Could you provide a more complete example of how you'd intend to use this?
The general idea is discussed in python/typing#802.
There's a recurring case of this in pre-PEP 681 descriptors across frameworks (SQLAlchemy, etc). To make this work…
class DataObject(FrameworkBaseClass):
field = Field(...)
…the Field class is typically implemented as a descriptor with unbound and bound variants:
F = TypeVar('F', bound='Field')
T = TypeVar('T')
class Field:
# Insert @overload variants
def __get__(self: F, obj: T | None, cls: type[T]) -> F | BoundField[F, T]:
if obj is None:
return self
return BoundField(self, obj)
class BoundField(Generic[F, T]):
def __init__(self, field: F, obj: T) -> None: ...
def __getattr__(self, attr: str) -> Any: # Proxy typing needed here
return getattr(self._field, attr)
... # Insert methods that override Field's methods where necessary (eg: `__call__`)
The trouble is that without proxy typing support, DataObject().field.attr
will be typed Any
, while class-level access as DataObject.field.attr
will identify the correct type. The current workaround is for the wrapper to explicitly declare the proxied attributes, but this will only work if (a) the wrapper is limited to specific types (as here), and (b) future subclasses of the wrapped type are not expected to add more attributes.
A clean syntax would be nice. Either __get__
returns an intersection type:
class Field:
@overload
def __get__(self: F, obj: None, cls: type[T]) -> F: ...
@overload
def __get__(self: F, obj: T, cls: type[T]) -> Has[BoundField, F]: ...
Or the wrapper is declared to be a proxy generic, appearing to follow subclass semantics only for type checking and only in a generic-bound instance:
class BoundField(Generic[Proxy[F], T]): ...
The first syntax is very close to what your plugin achieves, but using types and TypeVars instead of Protocols.
Proxying can be somewhat implemented, provided that you only operate on Protocols (this plugin will only ever work with Protocols).
A simplified example:
from typing import Protocol, Generic, TypeVar, Any
from typing_protocol_intersection import ProtocolIntersection
class Dog:
def pet(self) -> None: print("pet")
def feed(self) -> None: print("feed")
def bark(self) -> None: print("bark")
class IsAnimal(Protocol):
def pet(self) -> None: pass
def feed(self) -> None: pass
class ExtraAbilities(Protocol):
def fly(self) -> None: pass
T = TypeVar("T")
class FlyAdder(Generic[T]):
def extend(self, t: T) -> ProtocolIntersection[T, ExtraAbilities]:
class ExtendedT:
def fly(self) -> None: print("fly")
def __getattr__(self, name: str) -> Any:
return getattr(t, name)
return ExtendedT()
extra_animal = FlyAdder[IsAnimal]().extend(Dog())
# reveal_type(extra_animal) -> Revealed type is "typing_protocol_intersection.types.ProtocolIntersection[procy.ExtraAbilities, procy.IsAnimal]"
extra_animal.pet()
extra_animal.fly()
# extra_animal.bark() <- this will not typecheck, as `bark` is not part of neither `ExtraAbilities` or `IsAnimal` protocols, even though it is an attriubute of the originally extended instance
Also note, you could currently say extra_animal = FlyAdder[Dog]().extend(Dog())
, but it is incorrect - it's a bug in the plugin I have just discovered and will be fixing soon. If you used this, you couldn't trust mypy.
I believe the answer was satisfactory. Closing.