Skip to content

Commit

Permalink
Making the is_new_type check more robust then just checking __superty…
Browse files Browse the repository at this point in the history
…pe__ existance (#78)

Before this commit everthing which has an attribute called __supertype__
will be accepted as ‚NewType‘ which is wrong.

Now it is additionally checked that the __qualname__ must be
'NewType.<locals>.new_type', and that the module where the symbol is
defined has the name 'typing'. A user could still fullfill all those
conditions in a custom class, but the probability bacame quite low.

Furtheremore support for python 3.10¹, where NewType is not a function
any more but a class, is added. This check is implemented by
isincance() and should work always correctly.

The symbol ‚NewType‘ (i.e. not the call of it) is now also cosidered as
a NewType by doing a `tp is NewType` check. This is in alignment with
other functions, such as `is_classvar()` which does an `tp is ClassVar`
or `is_union_type()` which do a `tp is Union`.

¹ To be specific, the first RC of python 3.10, i.e. in all bata
versions, NewType is still a function. Please also note that the class
NewType in python 3.10.0rc has a different __qualname__ then the
function NewType before.
  • Loading branch information
raabf committed Oct 25, 2021
1 parent 01f1b91 commit 8f6aa20
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 21 deletions.
8 changes: 6 additions & 2 deletions test-requirements.txt
@@ -1,6 +1,10 @@
flake8; python_version >= '3.6'
flake8-bugbear; python_version >= '3.6'
pytest>=2.8; python_version >= '3.3'
importlib-metadata<3.0; python_version < '3.6'
packaging<21.0; python_version < '3.6'
pytest~=4.6; python_version < '3.6'
pytest>=4.6; python_version >= '3.6'
typing >= 3.7.4; python_version < '3.5'
mypy_extensions >= 0.3.0
typing_extensions >= 3.7.4
typing_extensions >= 3.7.4; python_version >= '3.6'
typing_extensions >= 3.7.4, < 3.10; python_version < '3.6'
52 changes: 36 additions & 16 deletions test_typing_inspect.py
Expand Up @@ -4,7 +4,8 @@
is_optional_type, is_final_type, is_literal_type, is_typevar, is_classvar,
is_forward_ref, get_origin, get_parameters, get_last_args, get_args, get_bound,
get_constraints, get_generic_type, get_generic_bases, get_last_origin,
typed_dict_keys, get_forward_arg, WITH_FINAL, WITH_LITERAL, LEGACY_TYPING)
typed_dict_keys, get_forward_arg, WITH_FINAL, WITH_LITERAL, LEGACY_TYPING, WITH_NEWTYPE,
)
from unittest import TestCase, main, skipIf, skipUnless
from typing import (
Union, Callable, Optional, TypeVar, Sequence, AnyStr, Mapping,
Expand All @@ -15,15 +16,7 @@
from typing_extensions import TypedDict as TETypedDict
from typing_extensions import Final
from typing_extensions import Literal

# Does this raise an exception ?
# from typing import NewType
if sys.version_info < (3, 5, 2):
WITH_NEWTYPE = False
else:
from typing import NewType
WITH_NEWTYPE = True

from typing_extensions import NewType as NewType_

# Does this raise an exception ?
# from typing import ClassVar
Expand Down Expand Up @@ -120,6 +113,18 @@ class Other(dict):
exec(PY36_TESTS)


# It is important for the test that this function is called 'NewType' to simulate the same __qualname__
# - which is "NewType.<locals>.new_type" - as typing.NewType has, i.e. it should be checked that is_new_type
# still do not accept a function which has the same __qualname__ and an attribute called __supertype__.
def NewType(name, tp):
def new_type(x):
return x

new_type.__name__ = name
new_type.__supertype__ = tp
return new_type


class IsUtilityTestCase(TestCase):
def sample_test(self, fun, samples, nonsamples):
msg = "Error asserting that %s(%s) is %s"
Expand Down Expand Up @@ -248,13 +253,22 @@ def test_classvar(self):
@skipIf(not WITH_NEWTYPE, "NewType is not present")
def test_new_type(self):
T = TypeVar('T')

class WithAttrSuperTypeCls:
__supertype__ = str

class WithAttrSuperTypeObj:
def __init__(self):
self.__supertype__ = str

samples = [
NewType('A', int),
NewType('B', complex),
NewType('C', List[int]),
NewType('D', Union['p', 'y', 't', 'h', 'o', 'n']),
NewType('E', List[Dict[str, float]]),
NewType('F', NewType('F_', int)),
NewType_,
NewType_('A', int),
NewType_('B', complex),
NewType_('C', List[int]),
NewType_('D', Union['p', 'y', 't', 'h', 'o', 'n']),
NewType_('E', List[Dict[str, float]]),
NewType_('F', NewType('F_', int)),
]
nonsamples = [
int,
Expand All @@ -264,6 +278,12 @@ def test_new_type(self):
Union["u", "v"],
type,
T,
NewType,
NewType('N', int),
WithAttrSuperTypeCls,
WithAttrSuperTypeCls(),
WithAttrSuperTypeObj,
WithAttrSuperTypeObj(),
]
self.sample_test(is_new_type, samples, nonsamples)

Expand Down
31 changes: 28 additions & 3 deletions typing_inspect.py
Expand Up @@ -30,11 +30,13 @@
WITH_FINAL = True
WITH_LITERAL = True
WITH_CLASSVAR = True
WITH_NEWTYPE = True
LEGACY_TYPING = False

if NEW_TYPING:
from typing import (
Generic, Callable, Union, TypeVar, ClassVar, Tuple, _GenericAlias, ForwardRef
Generic, Callable, Union, TypeVar, ClassVar, Tuple, _GenericAlias,
ForwardRef, NewType,
)
from typing_extensions import Final, Literal
if sys.version_info[:3] >= (3, 9, 0):
Expand All @@ -44,7 +46,8 @@
typingGenericAlias = (_GenericAlias,)
else:
from typing import (
Callable, CallableMeta, Union, Tuple, TupleMeta, TypeVar, GenericMeta, _ForwardRef
Callable, CallableMeta, Union, Tuple, TupleMeta, TypeVar, GenericMeta,
_ForwardRef,
)
try:
from typing import _Union, _ClassVar
Expand All @@ -70,6 +73,14 @@
except ImportError:
WITH_LITERAL = False

try: # python < 3.5.2
from typing_extensions import NewType
except ImportError:
try:
from typing import NewType
except ImportError:
WITH_NEWTYPE = False


def _gorg(cls):
"""This function exists for compatibility with old typing versions."""
Expand Down Expand Up @@ -247,10 +258,24 @@ def is_new_type(tp):
"""Tests if the type represents a distinct type. Examples::
is_new_type(int) == False
is_new_type(NewType) == True
is_new_type(NewType('Age', int)) == True
is_new_type(NewType('Scores', List[Dict[str, float]])) == True
"""
return getattr(tp, '__supertype__', None) is not None
if not WITH_NEWTYPE:
return False
elif sys.version_info[:3] >= (3, 10, 0) and sys.version_info.releaselevel != 'beta':
return tp is NewType or isinstance(tp, NewType)
elif sys.version_info[:3] >= (3, 0, 0):
return (tp is NewType or
(getattr(tp, '__supertype__', None) is not None and
getattr(tp, '__qualname__', '') == 'NewType.<locals>.new_type' and
tp.__module__ in ('typing', 'typing_extensions')))
else: # python 2
# __qualname__ is not available in python 2, so we simplify the test here
return (tp is NewType or
(getattr(tp, '__supertype__', None) is not None and
tp.__module__ in ('typing', 'typing_extensions')))


def is_forward_ref(tp):
Expand Down

0 comments on commit 8f6aa20

Please sign in to comment.