-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
bpo-40336: Refactor typing._SpecialForm #19620
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
Merged
serhiy-storchaka
merged 2 commits into
python:master
from
serhiy-storchaka:refactor-typing-special-form
Apr 23, 2020
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -141,8 +141,9 @@ def _type_check(arg, msg, is_argument=True): | |
if (isinstance(arg, _GenericAlias) and | ||
arg.__origin__ in invalid_generic_forms): | ||
raise TypeError(f"{arg} is not valid as type argument") | ||
if (isinstance(arg, _SpecialForm) and arg not in (Any, NoReturn) or | ||
arg in (Generic, Protocol)): | ||
if arg in (Any, NoReturn): | ||
return arg | ||
if isinstance(arg, _SpecialForm) or arg in (Generic, Protocol): | ||
raise TypeError(f"Plain {arg} is not valid as type argument") | ||
if isinstance(arg, (type, TypeVar, ForwardRef)): | ||
return arg | ||
|
@@ -299,41 +300,18 @@ def __deepcopy__(self, memo): | |
return self | ||
|
||
|
||
class _SpecialForm(_Final, _Immutable, _root=True): | ||
"""Internal indicator of special typing constructs. | ||
See _doc instance attribute for specific docs. | ||
""" | ||
|
||
__slots__ = ('_name', '_doc') | ||
|
||
def __new__(cls, *args, **kwds): | ||
"""Constructor. | ||
|
||
This only exists to give a better error message in case | ||
someone tries to subclass a special typing object (not a good idea). | ||
""" | ||
if (len(args) == 3 and | ||
isinstance(args[0], str) and | ||
isinstance(args[1], tuple)): | ||
# Close enough. | ||
raise TypeError(f"Cannot subclass {cls!r}") | ||
return super().__new__(cls) | ||
|
||
def __init__(self, name, doc): | ||
self._name = name | ||
self._doc = doc | ||
|
||
@property | ||
def __doc__(self): | ||
return self._doc | ||
# Internal indicator of special typing constructs. | ||
# See __doc__ instance attribute for specific docs. | ||
class _SpecialForm(_Final, _root=True): | ||
__slots__ = ('_name', '__doc__', '_getitem') | ||
|
||
def __eq__(self, other): | ||
if not isinstance(other, _SpecialForm): | ||
return NotImplemented | ||
return self._name == other._name | ||
def __init__(self, getitem): | ||
self._getitem = getitem | ||
self._name = getitem.__name__ | ||
self.__doc__ = getitem.__doc__ | ||
|
||
def __hash__(self): | ||
return hash((self._name,)) | ||
def __mro_entries__(self, bases): | ||
raise TypeError(f"Cannot subclass {self!r}") | ||
|
||
def __repr__(self): | ||
return 'typing.' + self._name | ||
|
@@ -352,31 +330,10 @@ def __subclasscheck__(self, cls): | |
|
||
@_tp_cache | ||
def __getitem__(self, parameters): | ||
if self._name in ('ClassVar', 'Final'): | ||
item = _type_check(parameters, f'{self._name} accepts only single type.') | ||
return _GenericAlias(self, (item,)) | ||
if self._name == 'Union': | ||
if parameters == (): | ||
raise TypeError("Cannot take a Union of no types.") | ||
if not isinstance(parameters, tuple): | ||
parameters = (parameters,) | ||
msg = "Union[arg, ...]: each arg must be a type." | ||
parameters = tuple(_type_check(p, msg) for p in parameters) | ||
parameters = _remove_dups_flatten(parameters) | ||
if len(parameters) == 1: | ||
return parameters[0] | ||
return _GenericAlias(self, parameters) | ||
if self._name == 'Optional': | ||
arg = _type_check(parameters, "Optional[t] requires a single type.") | ||
return Union[arg, type(None)] | ||
if self._name == 'Literal': | ||
# There is no '_type_check' call because arguments to Literal[...] are | ||
# values, not types. | ||
return _GenericAlias(self, parameters) | ||
raise TypeError(f"{self} is not subscriptable") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Splitting this is really great. I always wanted to do this, but didn't find a way to do this that wouldn't require boilerplate code, it looks like you have found such way. |
||
|
||
|
||
Any = _SpecialForm('Any', doc= | ||
return self._getitem(self, parameters) | ||
|
||
@_SpecialForm | ||
def Any(self, parameters): | ||
"""Special type indicating an unconstrained type. | ||
|
||
- Any is compatible with every type. | ||
|
@@ -386,9 +343,11 @@ def __getitem__(self, parameters): | |
Note that all the above statements are true from the point of view of | ||
static type checkers. At runtime, Any should not be used with instance | ||
or class checks. | ||
""") | ||
""" | ||
raise TypeError(f"{self} is not subscriptable") | ||
|
||
NoReturn = _SpecialForm('NoReturn', doc= | ||
@_SpecialForm | ||
def NoReturn(self, parameters): | ||
"""Special type indicating functions that never return. | ||
Example:: | ||
|
||
|
@@ -399,9 +358,11 @@ def stop() -> NoReturn: | |
|
||
This type is invalid in other positions, e.g., ``List[NoReturn]`` | ||
will fail in static type checkers. | ||
""") | ||
""" | ||
raise TypeError(f"{self} is not subscriptable") | ||
|
||
ClassVar = _SpecialForm('ClassVar', doc= | ||
@_SpecialForm | ||
def ClassVar(self, parameters): | ||
"""Special type construct to mark class variables. | ||
|
||
An annotation wrapped in ClassVar indicates that a given | ||
|
@@ -416,9 +377,12 @@ class Starship: | |
|
||
Note that ClassVar is not a class itself, and should not | ||
be used with isinstance() or issubclass(). | ||
""") | ||
""" | ||
item = _type_check(parameters, f'{self} accepts only single type.') | ||
return _GenericAlias(self, (item,)) | ||
|
||
Final = _SpecialForm('Final', doc= | ||
@_SpecialForm | ||
def Final(self, parameters): | ||
"""Special typing construct to indicate final names to type checkers. | ||
|
||
A final name cannot be re-assigned or overridden in a subclass. | ||
|
@@ -434,9 +398,12 @@ class FastConnector(Connection): | |
TIMEOUT = 1 # Error reported by type checker | ||
|
||
There is no runtime checking of these properties. | ||
""") | ||
""" | ||
item = _type_check(parameters, f'{self} accepts only single type.') | ||
return _GenericAlias(self, (item,)) | ||
|
||
Union = _SpecialForm('Union', doc= | ||
@_SpecialForm | ||
def Union(self, parameters): | ||
"""Union type; Union[X, Y] means either X or Y. | ||
|
||
To define a union, use e.g. Union[int, str]. Details: | ||
|
@@ -461,15 +428,29 @@ class FastConnector(Connection): | |
|
||
- You cannot subclass or instantiate a union. | ||
- You can use Optional[X] as a shorthand for Union[X, None]. | ||
""") | ||
|
||
Optional = _SpecialForm('Optional', doc= | ||
""" | ||
if parameters == (): | ||
raise TypeError("Cannot take a Union of no types.") | ||
if not isinstance(parameters, tuple): | ||
parameters = (parameters,) | ||
msg = "Union[arg, ...]: each arg must be a type." | ||
parameters = tuple(_type_check(p, msg) for p in parameters) | ||
parameters = _remove_dups_flatten(parameters) | ||
if len(parameters) == 1: | ||
return parameters[0] | ||
return _GenericAlias(self, parameters) | ||
|
||
@_SpecialForm | ||
def Optional(self, parameters): | ||
"""Optional type. | ||
|
||
Optional[X] is equivalent to Union[X, None]. | ||
""") | ||
""" | ||
arg = _type_check(parameters, f"{self} requires a single type.") | ||
return Union[arg, type(None)] | ||
|
||
Literal = _SpecialForm('Literal', doc= | ||
@_SpecialForm | ||
def Literal(self, parameters): | ||
"""Special typing form to define literal types (a.k.a. value types). | ||
|
||
This form can be used to indicate to type checkers that the corresponding | ||
|
@@ -486,10 +467,13 @@ def open_helper(file: str, mode: MODE) -> str: | |
open_helper('/some/path', 'r') # Passes type check | ||
open_helper('/other/path', 'typo') # Error in type checker | ||
|
||
Literal[...] cannot be subclassed. At runtime, an arbitrary value | ||
is allowed as type argument to Literal[...], but type checkers may | ||
impose restrictions. | ||
""") | ||
Literal[...] cannot be subclassed. At runtime, an arbitrary value | ||
is allowed as type argument to Literal[...], but type checkers may | ||
impose restrictions. | ||
""" | ||
# There is no '_type_check' call because arguments to Literal[...] are | ||
# values, not types. | ||
return _GenericAlias(self, parameters) | ||
|
||
|
||
class ForwardRef(_Final, _root=True): | ||
|
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be good to keep a custom error message, but not important.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are right, I tested again
class A(Any): pass
, and got "__init__() takes 2 positional arguments but 4 were given
" which does not look user friendly.But the former message was "
Cannot subclass <class 'typing._SpecialForm'>
" which does not look correct, because we subclass an instance of_SpecialForm
, not_SpecialForm
itself. It is not possible to fix in__new__
, but we can raise better error in custom__mro_entries__
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using
__mro_entries__
is a great idea!