-
Notifications
You must be signed in to change notification settings - Fork 2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Example use case for intersection #40
Comments
PS: hopefully that's actually helpful and not just a wall of text! |
I've got a similar use case to yours -- a decorator that I cannot currently type annotate adequately b/c it adds attributes to a passed-in class. (I need to say that for the user's type |
PS up top for visibility: I'd be happy to clean these up for use in a PEP. And by that I mean, distill them into "minimum viable examples" without all of the application-specific confusery. If desired, Just ran into another use case -- generic protocols and typeguards. Though tbh, I'm not 100% sure there isn't an alternative way of doing this right now; I found, for example, this SO answer which is similar. Anyways, this is another, completely unrelated set of code to what I posted above -- this time for a library to make it more convenient to define and use test vectors, especially for integration tests -- but I'm again using the same decorators + protocols pattern. Admittedly this is a little bit verbose, but... well, that's a whole separate discussion, and it's late here in CET, almost midnight. So this is the code as it exists right now: class _TevecType(Protocol):
_tevec_fields: ClassVar[tuple[str, ...]]
_tevec_applicators: ClassVar[dict[str, str]]
__dataclass_fields__: ClassVar[dict[str, Field[Any]]]
__call__: Callable
def _is_tevec_type(obj) -> TypeGuard[type[_TevecType]]:
return (
hasattr(obj, '_tevec_fields')
and hasattr(obj, '_tevec_applicators')
and callable(obj)
and hasattr(obj, '__dataclass_fields__')) The problem comes every time I use the type guard: the protocol is intended as a mixin, but after calling the type guard, pyright will narrow the type to eliminate all of the existing attributes. So, for example, in the test code: def test_field_proxies_on_decorated_class(self):
@tevec_type
class Foo:
foo: int
bar: Optional[int] = None
assert _is_tevec_type(Foo)
assert fields(Foo)
# Using set here to remove ordering anomalies
assert set(Foo._tevec_fields) == {'foo', 'bar'}
assert not Foo._tevec_applicators
assert hasattr(Foo, 'foo')
assert hasattr(Foo, 'bar')
# NOTE: both of these fail type checking because we need an
# intersection type for the _is_tevec_type type guard
assert isinstance(Foo.foo, _TevecFieldProxy)
assert isinstance(Foo.bar, _TevecFieldProxy) What I want to do instead is something along the lines of: def _is_tevec_type[T: type](obj: T) -> TypeGuard[Intersection[T, type[_TevecType]]]:
... (except probably I'd need to overload that because anything where That being said... the "spelling" here is a little bizzare, and I'm having a hard time wrapping my head around it. I think if the type checker has only narrowed the type of Edit: almost forgot, while poking around for this, I found these two issues on pyright, which are related:
As this comment mentions, without an intersection type, the problem is basically un-solveable; you can only choose one issue or the other. |
I agree that it would be useful to add type hints when patching attributes onto an existing object. Particularly methods. from typing import TypeVar, Protocol, Union as Intersection, cast
import inspect
T = TypeVar("T")
F = TypeVar("F")
class MyAttrContainer(Protocol[T]):
my_attr: T
def add_my_attr(attr: T) -> Callable[[F], Intersection[F, MyAttrContainer[T]]]:
def wrap(func: F) -> Intersection[F, MyAttrContainer[T]]:
func_ = cast(Intersection[F, MyAttrContainer[T]], func)
func_.my_attr = attr
return func_
return wrap
class Test:
@add_my_attr("hello world")
def test(self) -> str:
return "test"
t = Test()
print(t.test.my_attr)
print(t.test())
print(inspect.ismethod(t.test)) Edit: For completeness the above example can be achieved using the current typing system but you can't stack decorators otherwise the original would get lost. Intersection would allow stacking multiple attribute adding decorators. A typed example for adding an attribute to a method or function can be found here. |
While reading through #29, I saw this comment:
So I figured I'd chime in with one that I run into very, very frequently! Hopefully this might be of some use. As some background: I make heavy use of decorators, both for "plain" callables as well as classes. In fact, a very common pattern I use is to combine both of them: decorators on methods to indicate some special behavior, and then a decorator on the class to collect those and do some kind of transform on them. I find this to be a pretty clean library API, and I use it all the time, for a wide variety of things. Critical to note here is that the decorators always return the original object, with a few attributes added to it. This is something I've found much more ergonomic than wrapping, but that's a whole separate discussion that I won't go into. The important thing to know is that adding class attributes to classes, or attributes to functions, is extremely common for me.
So without further ado, here is a (simplified) example of the kind of thing I currently use fairly frequently, but cannot correctly type-hint due to missing intersections:
This is then coupled with a bunch of other code that operates on
_Stator
,_StatorAction
, etc. Code using this looks something like this (note that this is the unsimplified version; in reality most of those decorators are second-order decorators).There are a couple things I want to point out here:
from_checkpoint
, which is added by the@state_machine
decorator. Explicitly subclassingStateMachine
(as I did above) is a workaround for thefrom_checkpoint
, but for more sophisticated decorators (or ones that are meant for functions and not for classes), this isn't an option. Furthermore, in reality you might want the@stator
decorator to add some methods on to the class -- just like@dataclass
does. At that point, you're really SOL._Stator
,_StateMachine
, etc protocols internally, but they can't do that without sacrificing the API presented to library consumers. So as a result, the types applied to implementation functions are both overly broad (lots of unqualifiedtype
s) and still fail type checking because the type checker doesn't know about the intersections, so I either have to wrap the implementation in two calls tocast
(first to apply the protocol, then to revert to the original type), or I have to add a bunch oftype: ignore
comments.The text was updated successfully, but these errors were encountered: