-
-
Notifications
You must be signed in to change notification settings - Fork 30.4k
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
isinstance
on runtime_checkable
Protocol
has side-effects for @property
methods
#102433
Comments
isinstance
on runtime_checkable
Protocol
has side-effects for @property
methods
Apologies, if this is a duplicate, or the wrong place to raise this, but I couldn't find anything much on this issue outside of https://stackoverflow.com/questions/66641191/rationale-of-instancecheck-on-protocols |
Hi @chrisjsewell. I've traced the problem to the
I can't think of any particular downside to making this change so will open a PR shortly:) |
Brilliant thanks |
@JosephSBoyle, a better solution would probably be to use Looking in an object's >>> class Foo:
... def __init__(self):
... self.x = 5
... def __dir__(self):
... return []
...
>>> "x" in dir(Foo())
False A better solution is to look in an object's >>> class Foo:
... a = 1
... def __init__(self):
... self.b = 2
...
>>> obj = Foo()
>>> "a" in obj.__dict__
False
>>> "b" in obj.__dict__
True
>>> "a" in obj.__class__.__dict__
True
>>> "b" in obj.__class__.__dict__
False (It gets even more complicated if the attribute is defined on a superclass, or if the class defines
Having said that, importing |
Thanks @AlexWaygood, I wasn't aware of The docs suggest that using this might result in changing some other existing result in a change in behaviour, for instance "not being able to fetch dynamically created attributes". I'm not sure if this is wrong, or if I'm misunderstanding what is stated. Change: # typing.py
if cls._is_protocol:
# Check if attr is defined on the instance using `inspect.getattr_static`
# to avoid triggering possible side-effects in function style properties
# (i.e those with @property decorators) that would occur if `hasattr`
# were used.
import inspect
def hasattr_static(instance, attr):
try:
inspect.getattr_static(instance, attr)
return True
except AttributeError:
return False
if all(hasattr_static(instance, attr) and
# All *methods* can be blocked by setting them to None.
(not callable(getattr(cls, attr, None)) or
getattr(instance, attr) is not None)
for attr in _get_protocol_attrs(cls)):
return True
return super().__instancecheck__(instance) Test for dynamic attribute checking with def test_dynamically_added_attribute(self):
"""Test a class dynamically becoming adherent to a Protocol."""
@runtime_checkable
class P(Protocol):
@property
def foo(self):
pass
class X:
pass
x = X()
self.assertFalse(isinstance(x, P))
x.foo = "baz"
self.assertTrue(isinstance(x, P))
x2 = X()
x2.foo = property(lambda self: "baz")
self.assertTrue(isinstance(x2, P)) The above test passes, suggesting getattr static can in fact find dynamically added attributes. |
I think you're misunderstanding, but the This may or may not be what the inspect docs are talking about here, but here's a situation where using >>> from typing import ClassVar, Protocol, runtime_checkable
>>> @runtime_checkable
... class HasBar(Protocol):
... @property
... def bar(self) -> int: ...
...
>>> class Foo:
... x: ClassVar[bool] = False
... @property
... def bar(self) -> int:
... if self.x:
... return 42
... raise AttributeError
...
>>> import inspect
>>> f = Foo()
>>> inspect.getattr_static(f, "bar")
<property object at 0x11f7b0f48>
>>> hasattr(f, 'bar')
False
>>> if isinstance(f, HasBar): # False with current implementation, True with getattr_static implementation
... y = f.bar # type checkers will assume this is a safe attribute access, but if we used getattr_static, this would be unsafe
... |
The best solution here may actually be to just add a warning to the docs for |
😢 personally I would say this kind of makes |
I'm open to suggestions, if anybody can think of a way of improving the implementation here while maintaining type safety! 🙂 |
Thanks for the example, @AlexWaygood. I agree it seems like we're unfortunately unable to support both use cases and so should stick with the one we've supported thus far and provide a warning w.r.t the second use case as you suggest. I'm sorry to hear this makes Protocol unusable for the cases you had in mind @chrisjsewell, if you have a concrete case in mind feel free to open a S.O issue and link it here, I'd be happy to take a look. |
class X:
def __init__(self):
self._is_open = False
def __enter__(self):
# open some resource
self._is_open = True
def __exit__(self, *args):
# close the resource
self._is_open = False
@property
def x(self):
if not self._is_open:
raise RuntimeError("cannot access x unless in a context")
# fetch x ... |
As part of investigation issue #102433, I discovered what I believe to be an error where two classes `CI` and `DI` are not being used. The assertions beneath them act on `C` and `D`, duplicating existing assertions in this test. Automerge-Triggered-By: GH:AlexWaygood
As part of investigation issue python#102433, I discovered what I believe to be an error where two classes `CI` and `DI` are not being used. The assertions beneath them act on `C` and `D`, duplicating existing assertions in this test. (cherry picked from commit 7894bbe) Co-authored-by: JosephSBoyle <48555120+JosephSBoyle@users.noreply.github.com> Automerge-Triggered-By: GH:AlexWaygood
As part of investigation issue python#102433, I discovered what I believe to be an error where two classes `CI` and `DI` are not being used. The assertions beneath them act on `C` and `D`, duplicating existing assertions in this test. (cherry picked from commit 7894bbe) Co-authored-by: JosephSBoyle <48555120+JosephSBoyle@users.noreply.github.com> Automerge-Triggered-By: GH:AlexWaygood
It just feels very unexpected that |
I assume that you want to compare instances of this class against a @runtime_checkable
class HasX(Protocol):
x: Any It would be much more idiomatic in this case to raise |
Plus the fact you really have no idea what |
Thinking out loud, perhaps you have to have a separate Protocol for an "open" instance vs a "closed" one, Line 3164 in 7894bbe
|
As part of investigation issue #102433, I discovered what I believe to be an error where two classes `CI` and `DI` are not being used. The assertions beneath them act on `C` and `D`, duplicating existing assertions in this test. (cherry picked from commit 7894bbe) Co-authored-by: JosephSBoyle <48555120+JosephSBoyle@users.noreply.github.com> Automerge-Triggered-By: GH:AlexWaygood
As part of investigation issue #102433, I discovered what I believe to be an error where two classes `CI` and `DI` are not being used. The assertions beneath them act on `C` and `D`, duplicating existing assertions in this test. (cherry picked from commit 7894bbe) Co-authored-by: JosephSBoyle <48555120+JosephSBoyle@users.noreply.github.com> Automerge-Triggered-By: GH:AlexWaygood
I agree that perhaps it's unintuitive that the code in a property would be run but it's not any more of a security risk than if it didn't run the contents of the property method, since by even importing a class in the first place you're allowing it to run with whatever permissions you have set anyways. To add to Alex's suggestion of raising an |
I certainly take the suggestions onboard thanks 🙏 , but still... I feel the point of using |
…untime_checkable` protocols
…untime_checkable` protocols
…untime_checkable` protocols
…untime_checkable` protocols
I find this example messy as the behavior for type checkers depends on type checker. pyright disagrees with mypy here and pyright thinks that Foo is compatible with HasBar and behaves like if check is True. pyright even gives an error in strict mode of isinstance(f, HasBar) is always true. I also don't think even mypy interprets that code as you stated. If I adjust code a little to have, f: HasBar = Foo() mypy gives 0 errors so mypy also believes that Foo matches HasBar protocol. I did a check with other main type checkers and pyright/pyre/pytype all are happy allowing HasBar annotation on that line. This issue feels like it fits better to discuss in typing repository. I think requiring runtime evaluation though of properties is awkward enough behavior that I'd just avoid any code I work with mixing both runtime checkable and properties. I do sometimes use runtime checkable and it's behaviors make more sense with methods only. edit: I'm confused on meaning of example/how mypy or another type checker is supposed to protect you. A simpler, f = Foo()
f.bar raises at runtime, but passes type checking with 4 type checkers. |
I didn't mean to imply that mypy had different behaviour. What I'm concerned about is situations like the following: def print_attr(obj: object) -> None:
if isinstance(obj, HasBar):
print(obj.attr) Type checkers will assume that the Anyway, @JelleZijlstra also expressed reservations in #102449 (comment), so this is evidently more controversial than I realised :) I'll start a discussion on python/typing to see what others think. |
Does it though? class A:
@property
def a(self): ...
class B(A):
@property
def a(self):
raise AttributeError
isinstance(B(), A) that passes, then raises For me, trying to account for |
As part of investigation issue python#102433, I discovered what I believe to be an error where two classes `CI` and `DI` are not being used. The assertions beneath them act on `C` and `D`, duplicating existing assertions in this test. Automerge-Triggered-By: GH:AlexWaygood
I've opened an issue here, attempting to summarise the discussion, and seeking thoughts from the typing community more broadly: |
As part of investigation issue python/cpython#102433, I discovered what I believe to be an error where two classes `CI` and `DI` are not being used. The assertions beneath them act on `C` and `D`, duplicating existing assertions in this test. Automerge-Triggered-By: GH:AlexWaygood
…sinstance()` checks on `typing.runtime_checkable` protocols (#102449) Co-authored-by: Carl Meyer <carl@oddbird.net>
…ith `isinstance()` checks on `typing.runtime_checkable` protocols (pythonGH-102449) (cherry picked from commit 5ffdaf7) Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com> Co-authored-by: Carl Meyer <carl@oddbird.net>
…ith `isinstance()` checks on `typing.runtime_checkable` protocols (pythonGH-102449) (cherry picked from commit 5ffdaf7) Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com> Co-authored-by: Carl Meyer <carl@oddbird.net>
…ith `isinstance()` checks on `typing.runtime_checkable` protocols (python#102449) Co-authored-by: Carl Meyer <carl@oddbird.net>
#103034 has been merged, meaning that descriptors, properties and It might be possible to backport the change to Thanks for changing my mind on this one! |
…a.__instancecheck__` (python#103034)
…a.__instancecheck__` (python#103034)
For example:
will raise the
RuntimeError
This is an issue, for example, if
myproperty
is an expensive call, has unwanted side effects, or excepts outside of a context managerLinked PRs
isinstance()
checks ontyping.runtime_checkable
protocols #102449isinstance()
checks ontyping.runtime_checkable
protocols (GH-102449) #102592isinstance()
checks ontyping.runtime_checkable
protocols (GH-102449) #102593inspect.getattr_static
intyping._ProtocolMeta.__instancecheck__
#103034The text was updated successfully, but these errors were encountered: