10000 backport ParamSpecArgs/Kwargs (#798) · python/typing@4ba98e8 · GitHub
[go: up one dir, main page]

Skip to content

Commit 4ba98e8

Browse files
backport ParamSpecArgs/Kwargs (#798)
From python/cpython#25298. I also added more tests for get_args/get_origin, which previously didn't exist in in typing_extensions.
1 parent 40932e3 commit 4ba98e8

File tree

4 files changed

+174
-14
lines changed

4 files changed

+174
-14
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ Workflow
2727
Workflow for PyPI releases
2828
--------------------------
2929

30-
* Run tests under all supported versions. As of May 2019 this includes
31-
2.7, 3.4, 3.5, 3.6, 3.7.
30+
* Run tests under all supported versions. As of April 2021 this includes
31+
2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9.
3232

3333
* On macOS, you can use `pyenv <https://github.com/pyenv/pyenv>`_ to
3434
manage multiple Python installations. Long story short:

typing_extensions/README.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,10 @@ Python 3.4+ only:
5757
-----------------
5858

5959
- ``ChainMap``
60+
- ``ParamSpec``
61+
- ``Concatenate``
62+
- ``ParamSpecArgs``
63+
- ``ParamSpecKwargs``
6064

6165
Python 3.5+ only:
6266
-----------------

typing_extensions/src_py3/test_typing_extensions.py

Lines changed: 89 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
import subprocess
88
import types
99
from unittest import TestCase, main, skipUnless, skipIf
10-
from typing import TypeVar, Optional
10+
from typing import TypeVar, Optional, Union
1111
from typing import T, KT, VT # Not in __all__.
12-
from typing import Tuple, List, Dict, Iterator
12+
from typing import Tuple, List, Dict, Iterator, Callable
1313
from typing import Generic
1414
from typing import no_type_check
1515
from typing_extensions import NoReturn, ClassVar, Final, IntVar, Literal, Type, NewType, TypedDict
16-
from typing_extensions import TypeAlias, ParamSpec, Concatenate
16+
from typing_extensions import TypeAlias, ParamSpec, Concatenate, ParamSpecArgs, ParamSpecKwargs
1717

1818
try:
1919
from typing_extensions import Protocol, runtime, runtime_checkable
@@ -519,6 +519,80 @@ def test_final_forward_ref(self):
519519
self.assertNotEqual(gth(Loop, globals())['attr'], Final)
520520

521521

522+
@skipUnless(PEP_560, "Python 3.7+ required")
523+
class GetUtilitiesTestCase(TestCase):
524+
def test_get_origin(self):
525+
from typing_extensions import get_origin
526+
527+
T = TypeVar('T')
528+
P = ParamSpec('P')
529+
class C(Generic[T]): pass
530+
self.assertIs(get_origin(C[int]), C)
531+
self.assertIs(get_origin(C[T]), C)
532+
self.assertIs(get_origin(int), None)
533+
self.assertIs(get_origin(ClassVar[int]), ClassVar)
534+
self.assertIs(get_origin(Union[int, str]), Union)
535+
self.assertIs(get_origin(Literal[42, 43]), Literal)
536+
self.assertIs(get_origin(Final[List[int]]), Final)
537+
self.assertIs(get_origin(Generic), Generic)
538+
self.assertIs(get_origin(Generic[T]), Generic)
539+
self.assertIs(get_origin(List[Tuple[T, T]][int]), list)
540+
self.assertIs(get_origin(Annotated[T, 'thing']), Annotated)
541+
self.assertIs(get_origin(List), list)
542+
self.assertIs(get_origin(Tuple), tuple)
543+
self.assertIs(get_origin(Callable), collections.abc.Callable)
544+
if sys.version_info >= (3, 9):
545+
self.assertIs(get_origin(list[int]), list)
546+
self.assertIs(get_origin(list), None)
547+
self.assertIs(get_origin(P.args), P)
548+
self.assertIs(get_origin(P.kwargs), P)
549+
550+
def test_get_args(self):
551+
from typing_extensions import get_args
552+
553+
T = TypeVar('T')
554+
class C(Generic[T]): pass
555+
self.assertEqual(get_args(C[int]), (int,))
556+
self.assertEqual(get_args(C[T]), (T,))
557+
self.assertEqual(get_args(int), ())
558+
self.assertEqual(get_args(ClassVar[int]), (int,))
559+
self.assertEqual(get_args(Union[int, str]), (int, str))
560+
self.assertEqual(get_args(Literal[42, 43]), (42, 43))
561+
self.assertEqual(get_args(Final[List[int]]), (List[int],))
562+
self.assertEqual(get_args(Union[int, Tuple[T, int]][str]),
563+
(int, Tuple[str, int]))
564+
self.assertEqual(get_args(typing.Dict[int, Tuple[T, T]][Optional[int]]),
565+
(int, Tuple[Optional[int], Optional[int]]))
566+
self.assertEqual(get_args(Callable[[], T][int]), ([], int))
567+
self.assertEqual(get_args(Callable[..., int]), (..., int))
568+
self.assertEqual(get_args(Union[int, Callable[[Tuple[T, ...]], str]]),
569+
(int, Callable[[Tuple[T, ...]], str]))
570+
self.assertEqual(get_args(Tuple[int, ...]), (int, ...))
571+
self.assertEqual(get_args(Tuple[()]), ((),))
572+
self.assertEqual(get_args(Annotated[T, 'one', 2, ['three']]), (T, 'one', 2, ['three']))
573+
self.assertEqual(get_args(List), ())
574+
self.assertEqual(get_args(Tuple), ())
575+
self.assertEqual(get_args(Callable), ())
576+
if sys.version_info >= (3, 9):
577+
self.assertEqual(get_args(list[int]), (int,))
578+
self.assertEqual(get_args(list), ())
579+
if sys.version_info >= (3, 9):
580+
# Support Python versions with and without the fix for
581+
# https://bugs.python.org/issue42195
582+
# The first variant is for 3.9.2+, the second for 3.9.0 and 1
583+
self.assertIn(get_args(collections.abc.Callable[[int], str]),
584+
(([int], str), ([[int]], str)))
585+
self.assertIn(get_args(collections.abc.Callable[[], str]),
586+
(([], str), ([[]], str)))
587+
self.assertEqual(get_args(collections.abc.Callable[..., str]), (..., str))
588+
P = ParamSpec('P')
589+
# In 3.9 and lower we use typing_extensions's hacky implementation
590+
# of ParamSpec, which gets incorrectly wrapped in a list
591+
self.assertIn(get_args(Callable[P, int]), [(P, int), ([P], int)])
592+
self.assertEqual(get_args(Callable[Concatenate[int, P], int]),
593+
(Concatenate[int, P], int))
594+
595+
522596
class CollectionsAbcTests(BaseTestCase):
523597

524598
def test_isinstance_collections(self):
@@ -1952,8 +2026,17 @@ def test_valid_uses(self):
19522026
# ParamSpec instances should also have args and kwargs attributes.
19532027
self.assertIn('args', dir(P))
19542028
self.assertIn('kwargs', dir(P))
1955-
P.args
1956-
P.kwargs
2029+
2030+
def test_args_kwargs(self):
2031+
P = ParamSpec('P')
2032+
self.assertIn('args', dir(P))
2033+
self.assertIn('kwargs', dir(P))
2034+
self.assertIsInstance(P.args, ParamSpecArgs)
2035+
self.assertIsInstance(P.kwargs, ParamSpecKwargs)
2036+
self.assertIs(P.args.__origin__, P)
2037+
self.assertIs(P.kwargs.__origin__, P)
2038+
self.assertEqual(repr(P.args), "P.args")
2039+
self.assertEqual(repr(P.kwargs), "P.kwargs")
19572040

19582041
# Note: ParamSpec doesn't work for pre-3.10 user-defined Generics due
19592042
# to type checks inside Generic.
@@ -2072,7 +2155,7 @@ def test_typing_extensions_defers_when_possible(self):
20722155
'Final',
20732156
'get_type_hints'
20742157
}
2075-
if sys.version_info[:2] == (3, 8):
2158+
if sys.version_info < (3, 10):
20762159
exclude |= {'get_args', 'get_origin'}
20772160
for item in typing_extensions.__all__:
20782161
if item not in exclude and hasattr(typing, item):

typing_extensions/src_py3/typing_extensions.py

Lines changed: 79 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2065,11 +2065,23 @@ class Annotated(metaclass=AnnotatedMeta):
20652065

20662066
# Python 3.8 has get_origin() and get_args() but those implementations aren't
20672067
# Annotated-aware, so we can't use those, only Python 3.9 versions will do.
2068-
if sys.version_info[:2] >= (3, 9):
2068+
# Similarly, Python 3.9's implementation doesn't support ParamSpecArgs and
2069+
# ParamSpecKwargs.
2070+
if sys.version_info[:2] >= (3, 10):
20692071
get_origin = typing.get_origin
20702072
get_args = typing.get_args
20712073
elif PEP_560:
2072-
from typing import _GenericAlias # noqa
2074+
from typing import _GenericAlias
2075+
try:
2076+
# 3.9+
2077+
from typing import _BaseGenericAlias
2078+
except ImportError:
2079+
_BaseGenericAlias = _GenericAlias
2080+
try:
2081+
# 3.9+
2082+
from typing import GenericAlias
2083+
except ImportError:
2084+
GenericAlias = _GenericAlias
20732085

20742086
def get_origin(tp):
20752087
"""Get the unsubscripted version of a type.
@@ -2084,10 +2096,12 @@ def get_origin(tp):
20842096
get_origin(Generic[T]) is Generic
20852097
get_origin(Union[T, int]) is Union
20862098
get_origin(List[Tuple[T, T]][int]) == list
2099+
get_origin(P.args) is P
20872100
"""
20882101
if isinstance(tp, _AnnotatedAlias):
20892102
return Annotated
2090-
if isinstance(tp, _GenericAlias):
2103+
if isinstance(tp, (_GenericAlias, GenericAlias, _BaseGenericAlias,
2104+
ParamSpecArgs, ParamSpecKwargs)):
20912105
return tp.__origin__
20922106
if tp is Generic:
20932107
return Generic
@@ -2106,7 +2120,9 @@ def get_args(tp):
21062120
"""
21072121
if isinstance(tp, _AnnotatedAlias):
21082122
return (tp.__origin__,) + tp.__metadata__
2109-
if isinstance(tp, _GenericAlias) and not tp._special:
2123+
if isinstance(tp, (_GenericAlias, GenericAlias)):
2124+
if getattr(tp, "_special", False):
2125+
return ()
21102126
res = tp.__args__
21112127
if get_origin(tp) is collections.abc.Callable and res[0] is not Ellipsis:
21122128
res = (list(res[:-1]), res[-1])
@@ -2210,9 +2226,60 @@ class TypeAlias(metaclass=_TypeAliasMeta, _root=True):
22102226

22112227

22122228
# Python 3.10+ has PEP 612
2229+
if hasattr(typing, 'ParamSpecArgs'):
2230+
ParamSpecArgs = typing.ParamSpecArgs
2231+
ParamSpecKwargs = typing.ParamSpecKwargs
2232+
else:
2233+
class _Immutable:
2234+
"""Mixin to indicate that object should not be copied."""
2235+
__slots__ = ()
2236+
2237+
def __copy__(self):
2238+
return self
2239+
2240+
def __deepcopy__(self, memo):
2241+
return self
2242+
2243+
class ParamSpecArgs(_Immutable):
2244+
"""The args for a ParamSpec object.
2245+
2246+
Given a ParamSpec object P, P.args is an instance of ParamSpecArgs.
2247+
2248+
ParamSpecArgs objects have a reference back to their ParamSpec:
2249+
2250+
P.args.__origin__ is P
2251+
2252+
This type is meant for runtime introspection and has no special meaning to
2253+
static type checkers.
2254+
"""
2255+
def __init__(self, origin):
2256+
self.__origin__ = origin
2257+
2258+
def __repr__(self):
2259+
return "{}.args".format(self.__origin__.__name__)
2260+
2261+
class ParamSpecKwargs(_Immutable):
2262+
"""The kwargs for a ParamSpec object.
2263+
2264+
Given a ParamSpec object P, P.kwargs is an instance of ParamSpecKwargs.
2265+
2266+
ParamSpecKwargs objects have a reference back to their ParamSpec:
2267+
2268+
P.kwargs.__origin__ is P
2269+
2270+
This type is meant for runtime introspection and has no special meaning to
2271+
static type checkers.
2272+
"""
2273+
def __init__(self, origin):
2274+
self.__origin__ = origin
2275+
2276+
def __repr__(self):
2277+
return "{}.kwargs".format(self.__origin__.__name__)
2278+
22132279
if hasattr(typing, 'ParamSpec'):
22142280
ParamSpec = typing.ParamSpec
22152281
else:
2282+
22162283
# Inherits from list as a workaround for Callable checks in Python < 3.9.2.
22172284
class ParamSpec(list):
22182285
"""Parameter specification variable.
@@ -2260,8 +2327,14 @@ def add_two(x: float, y: float) -> float:
22602327
Note that only parameter specification variables defined in global scope can
22612328
be pickled.
22622329
"""
2263-
args = object()
2264-
kwargs = object()
2330+
2331+
@property
2332+
def args(self):
2333+
return ParamSpecArgs(self)
2334+
2335+
@property
2336+
def kwargs(self):
2337+
return ParamSpecKwargs(self)
22652338

22662339
def __init__(self, name, *, bound=None, covariant=False, contravariant=False):
22672340
super().__init__([self])

0 commit comments

Comments
 (0)
0