Skip to content
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

Avoid error if origin has a buggy __eq__ #422

Merged
merged 6 commits into from
Jun 3, 2024
Merged

Conversation

JelleZijlstra
Copy link
Member

@JelleZijlstra JelleZijlstra requested a review from AlexWaygood June 3, 2024 10:21
@AlexWaygood
Copy link
Member

This is a good solution. Something we could also do is to check that frame.f_globals.get("__name__") == "typing" before we check the value of origin. We only specifically care about an origin local variable if it's being set in a frame from typing.py, not if it's being set in a frame from site-packages/dirty_equals/_base.py

@JelleZijlstra
Copy link
Member Author

Not sure that applies here, since in the pydantic stacktrace we're still being invoked from typing.py, it's just that origin is some funny object that has a broken __eq__. However, it's definitely possible that someone would invoke typing_extensions. _collect_type_vars directly without going through typing, so I'll add that check.

@AlexWaygood
Copy link
Member

Yes, I don't think it applies here, I'm just inclined at this point to err on the side of absolute caution when it comes to this hack :-)

Have you been able to figure out exactly why the NameError is occurring in the pydantic tests? _length_repr looks like it's defined here. My guess would be that something to do with the _get_frame hack means that the __eq__ method can't find it in the globals for some reason, but I haven't been able to quickly find a minimised repro (and probably shouldn't spend too much time on it right now :/ )

@AlexWaygood
Copy link
Member

AlexWaygood commented Jun 3, 2024

(To be clear I don't think we need to necessarily have a test for this one, since it's pretty obvious that this would fix it. But I'd still be curious to know exactly what the cause of this is, if we can figure it out. We don't need to figure it out before merging this.)

@JelleZijlstra
Copy link
Member Author

Yeah, I don't understand the NameError either. I was able to reproduce the failure in a test:

+    def test_generic_with_broken_eq(self):
+        # See https://github.com/python/typing_extensions/pull/422 for context
+        class BrokenEq(abc.ABCMeta):
+            def __eq__(self, other):
+                if other is typing_extensions.Protocol:
+                    raise TypeError("I'm broken")
+                return False
+
+        class G(Generic[T], metaclass=BrokenEq):
+            pass
+
+        alias = G[int]
+        self.assertIs(get_origin(alias), G)
+        self.assertEqual(get_args(alias), (int,))
+
Traceback (most recent call last):
  File "test_typing_extensions.py", line 6631, in test_generic_with_broken_eq
    alias = G[int]
  File "/Users/jelle/.pyenv/versions/3.8.16/lib/python3.8/typing.py", line 261, in inner
    return func(*args, **kwds)
  File "/Users/jelle/.pyenv/versions/3.8.16/lib/python3.8/typing.py", line 898, in __class_getitem__
    return _GenericAlias(cls, params)
  File "/Users/jelle/.pyenv/versions/3.8.16/lib/python3.8/typing.py", line 672, in __init__
    self.__parameters__ = _collect_type_vars(params)
  File "/Users/jelle/py/typing_extensions/src/typing_extensions.py", line 2994, in _collect_type_vars
    enforce_default_ordering = _has_generic_or_protocol_as_origin()
  File "/Users/jelle/py/typing_extensions/src/typing_extensions.py", line 2961, in _has_generic_or_protocol_as_origin
    return frame.f_locals.get("origin") in (
  File "test_typing_extensions.py", line 6625, in __eq__
    raise TypeError("I'm broken")
TypeError: I'm broken

Though it only reproduces on 3.8 for some reason.

@AlexWaygood
Copy link
Member

AlexWaygood commented Jun 3, 2024

That test looks good enough to me; it's raising the TypeError in the right place in the stack trace

Though it only reproduces on 3.8 for some reason.

Yeah, and the same with the pydantic test failure, right? Maybe something about frames where it wouldn't lookup globals properly under tracing got fixed in Python 3.9. Who knows.

@JelleZijlstra
Copy link
Member Author

Actually the NameError appears to be because the error happens while dirty_equals is being imported. The code that triggers the error is on line 215 of _sequence.py, and _length_repr is on line 257, so it doesn't exist yet.

@JelleZijlstra
Copy link
Member Author

I guess I found a CPython bug too.

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
@AlexWaygood
Copy link
Member

If we want to be maximally paranoid, we could also make this change:

--- a/src/typing_extensions.py
+++ b/src/typing_extensions.py
@@ -2954,8 +2954,10 @@ if not _PEP_696_IMPLEMENTED:
 def _has_generic_or_protocol_as_origin() -> bool:
     try:
         frame = sys._getframe(2)
-    # not all platforms have sys._getframe()
-    except AttributeError:
+    # - Catch AttributeError: not all Python implementations have sys._getframe()
+    # - Catch ValueError: maybe we're called from an unexpected module
+    #   and the call stack isn't deep enough
+    except (AttributeError, ValueError):

@JelleZijlstra
Copy link
Member Author

% python -c 'from typing_extensions import _has_generic_or_protocol_as_origin; _has_generic_or_protocol_as_origin()'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/Users/jelle/py/typing_extensions/src/typing_extensions.py", line 2956, in _has_generic_or_protocol_as_origin
    frame = sys._getframe(2)
            ^^^^^^^^^^^^^^^^
ValueError: call stack is not deep enough

I guess I get to fix another critical bug.

@JelleZijlstra JelleZijlstra marked this pull request as ready for review June 3, 2024 11:37
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
Copy link
Member

@AlexWaygood AlexWaygood left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@JelleZijlstra JelleZijlstra merged commit 53bcdde into main Jun 3, 2024
21 checks passed
@JelleZijlstra JelleZijlstra deleted the JelleZijlstra-patch-2 branch June 3, 2024 11:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants