Repository navigation
Should typing be calling getattr on arbitrary classes? #113320
Copy link
Copy link
Closed
Labels
3.12only security fixesonly security fixes3.13only security fixesonly security fixesstdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-typingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Description
Bug report
Bug description:
I was updating my CI to support python3.12 and ran into the following error that isn't present in the 3.12 builds https://github.com/Gobot1234/steam.py/actions/runs/7276342755/job/19825996032
MRE:
from enum import IntEnum
from typing import Protocol
class classproperty:
def __init__(self, func):
self.__func__ = func
def __get__(self, instance, type):
return self.__func__(type)
class _CommentThreadType(IntEnum):
WorkshopAccountDeveloper = 2
class Commentable(Protocol):
@classproperty
def _COMMENTABLE_TYPE(cls):
return _CommentThreadType[cls.__name__]Traceback on main:
Running Debug|x64 interpreter...
Traceback (most recent call last):
File "C:\Users\alexw\coding\test_protocol.py", line 14, in <module>
class Commentable(Protocol):
...<2 lines>...
return _CommentThreadType[cls.__name__]
File "C:\Users\alexw\coding\cpython\Lib\typing.py", line 1838, in __init__
cls.__callable_proto_members_only__ = all(
~~~^
callable(getattr(cls, attr, None)) for attr in cls.__protocol_attrs__
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "C:\Users\alexw\coding\cpython\Lib\typing.py", line 1839, in <genexpr>
callable(getattr(cls, attr, None)) for attr in cls.__protocol_attrs__
~~~~~~~^^^^^^^^^^^^^^^^^
File "C:\Users\alexw\coding\test_protocol.py", line 9, in __get__
return self.__func__(type)
~~~~~~~~~~~~~^^^^^^
File "C:\Users\alexw\coding\test_protocol.py", line 17, in _COMMENTABLE_TYPE
return _CommentThreadType[cls.__name__]
~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
File "C:\Users\alexw\coding\cpython\Lib\enum.py", line 763, in __getitem__
return cls._member_map_[name]
~~~~~~~~~~~~~~~~^^^^^^
KeyError: 'Commentable'Should typing be more careful about accessing random attributes on classes to prevent these kinds of errors in the future?
CPython versions tested on:
3.12
Operating systems tested on:
macOS
Linked PRs
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
3.12only security fixesonly security fixes3.13only security fixesonly security fixesstdlibStandard Library Python modules in the Lib/ directoryStandard Library Python modules in the Lib/ directorytopic-typingtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
Activity
AlexWaygood commentedon Dec 20, 2023
Did you mean: "That isn't present in the 3.11 builds"? :)
AlexWaygood commentedon Dec 20, 2023
Some scattered thoughts, some of which I already stated on Discord:
_COMMENTABLE_TYPE, which may raise non-AttributeErrorexceptions when the__get__method of the descriptor is called, are pretty badly behaved and should be discouraged. However, we've already made the decision thattyping.Protocolshould be extremely careful aboutgetattr()calls on instances, so perhaps it stands to reason thattypng.Protocolshould also be extremely careful aboutgetattr()calls on class objects themselves as well.getattr_static()rather thangetattr(). Currently we do thegetattr()calls against the class object in various places, but we could fairly easily compute the set of callable members in_ProtocolMeta.__init__()to get rid of any performance penalty. I'd want to check whether this made protocol class creation significantly slower, though.getattr_static()for thesegetattr()calls could have any other unexpected/undesirable behaviour changes.AlexWaygood commentedon Dec 20, 2023
This diff fixes things without breaking any existing tests, but I haven't done any benchmarking yet. It'll certainly slow down the import time of
typing.py, sinceinspectis a very heavy import, and this patch would mean we'd depend oninspectat import time:Details
AlexWaygood commentedon Dec 20, 2023
@carljm, any thoughts from you on whether typing.Protocol should take care to account for the fact that attempting to access attributes on classes could potentially cause any exception to be raised, due to the possibility of badly behaved classproperties? :)
carljm commentedon Dec 21, 2023
My first question looking at the MRE is, why are we introspecting the Protocol class at all when it hasn't been decorated as runtime-checkable? It seems like at the very least we should confine such problems to people who are explicitly opting in to runtime-checkable Protocols. But it seems like this would require some significant reorganization to do the introspection more lazily? (I could be way off base here, haven't had my head in this code for a while.)
AlexWaygood commentedon Dec 21, 2023
Indeed. If you change the MRE to this, it fails with a similar traceback even on Python 3.11 (before we started mucking about with the code to make it faster). The only difference is that on Python 3.12, you get the exception raised even if the class isn't runtime-checkable:
Repro that triggers even on Python 3.11
The reason why it now triggers even for non-runtime protocols is that for performance reasons, we now inspect the Protocol class at the point when it's being created (as part of
_ProtocolMeta.__init__(). And at class creation time, we don't know yet whether it's runtime-checkable or not! The@runtime_checkabledecorator adds the_runtime_checkableattribute to the protocol class after the class has been initialised by_ProtocolMeta.__init__()AlexWaygood commentedon Dec 21, 2023
Here's a patch that restores things to the way they were on Python 3.11 -- people using classproperties that raise non-
AttributeErrors would still be broken if they used@runtime_checkableprotocols, but wouldn't be broken if their protocols weren't@runtime_checkable:Possible patch
carljm commentedon Dec 21, 2023
I do think that's a regression that we should fix. Runtime-checkable protocols are finicky (and arguably just a bad idea in the first place :) ) -- we shouldn't foist their issues onto every user of Protocol.
AlexWaygood commentedon Dec 22, 2023
Cool, I'm working on a PR. Here's a slightly more minimal repro, FWIW:
Run the file (with py312):
pythongh-113320: Reduce the number of dangerous `getattr()` calls whe…
gh-113320: Reduce the number of dangerous `getattr()` calls when cons…
[3.12] pythongh-113320: Reduce the number of dangerous `getattr()` ca…
[3.12] gh-113320: Reduce the number of dangerous `getattr()` calls wh…
pythongh-113320: Reduce the number of dangerous `getattr()` calls whe…
pythongh-113320: Reduce the number of dangerous `getattr()` calls whe…
pythongh-113320: Reduce the number of dangerous `getattr()` calls whe…