Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6567,6 +6567,90 @@ def test_lazy_import(self):
"annotationlib",
})

def make_tp_cached(self, func, *, typed=False):
# Wrap *func* with typing._tp_cache, undoing the module-global
# registration afterwards so the test stays reference-leak clean.
before = len(typing._cleanups)
wrapped = typing._tp_cache(func, typed=typed)
self.addCleanup(typing._caches.__delitem__, func)
self.addCleanup(typing._cleanups.__delitem__, slice(before, None))
return wrapped

def test_tp_cache_does_not_swallow_type_error(self):
# gh-154115: A TypeError raised by the wrapped function itself used
# to be mistaken for an unhashable-argument error, causing the
# function to run a second time before the error propagated.
calls = []

@self.make_tp_cached
def func(*args, **kwds):
calls.append((args, kwds))
raise TypeError("boom")

for args, kwds in [(("hashable",), {}), ((), {"kwd": "hashable"})]:
with self.subTest(args=args, kwds=kwds):
calls.clear()
with self.assertRaisesRegex(TypeError, "boom"):
func(*args, **kwds)
self.assertEqual(calls, [(args, kwds)])

def test_tp_cache_unhashable_fallback(self):
# Unhashable arguments cannot be cached, so the wrapped function is
# still used directly as a fallback and runs exactly once.
calls = []

@self.make_tp_cached
def func(*args, **kwds):
calls.append((args, kwds))
return "result"

unhashable = [1, 2, 3]
for args, kwds in [((unhashable,), {}), ((), {"kwd": unhashable})]:
with self.subTest(args=args, kwds=kwds):
calls.clear()
self.assertEqual(func(*args, **kwds), "result")
self.assertEqual(calls, [(args, kwds)])

def test_tp_cache_caches_repeated_calls(self):
# A successful call with hashable arguments is cached: calling again
# with the same arguments returns the same object without running
# the wrapped function a second time.
calls = []

@self.make_tp_cached
def func(arg):
calls.append(arg)
return object()

first = func(42)
second = func(42)
self.assertIs(first, second)
self.assertEqual(calls, [42])

def test_tp_cache_typed_falls_back_for_unhashable_type(self):
# gh-154115: with typed=True the cache key also includes the type of
# each argument, so a hashable value whose type is unhashable cannot
# be cached. Such a call must fall back to the wrapped function
# rather than being mistaken for a function-raised TypeError.
class Meta(type):
__hash__ = None

class C(metaclass=Meta):
def __hash__(self):
return 0

calls = []

def func(arg):
calls.append(arg)
return "result"

func = self.make_tp_cached(func, typed=True)
obj = C()
self.assertEqual(func(obj), "result")
self.assertEqual(len(calls), 1)
self.assertIs(calls[0], obj)


@lru_cache()
def cached_func(x, y):
Expand Down
14 changes: 13 additions & 1 deletion Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,19 @@ def inner(*args, **kwds):
try:
return _caches[func](*args, **kwds)
except TypeError:
pass # All real errors (not unhashable args) are raised below.
# A TypeError here may mean the arguments are unhashable
# (so they cannot be cached and the original function is
# used as a fallback), or that the wrapped function itself
# raised TypeError. Rebuild the same key lru_cache uses to
# tell the two apart: only fall back when the key really is
# unhashable, otherwise re-raise so the function is not run
# a second time.
try:
hash(functools._make_key(args, kwds, typed))
except TypeError:
pass # Unhashable arguments: fall back below.
else:
raise
return func(*args, **kwds)
return inner

Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -2008,6 +2008,7 @@ Wm. Keith van der Meulen
Eric N. Vander Weele
Andrew Vant
Atul Varma
Vyron Vasileiadis
Dmitry Vasiliev
Sebastian Ortiz Vasquez
Alexandre Vassalotti
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Subscripting a :mod:`typing` special form or generic alias with arguments that
are rejected with a :exc:`TypeError` (such as ``typing.List[int, str]``) no
longer evaluates the subscription twice before raising the error.
Loading