diff --git a/setup.py b/setup.py index 239d7b0..abae423 100755 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ are valid. """, license="LGPLv2+", - version="2.1.1", + version="2.1.2", author="Viktor Hercinger", author_email="viktor.hercinger@balabit.com", maintainer="Viktor Hercinger", @@ -68,6 +68,5 @@ install_requires=[ 'nose', 'sphinx', - 'typing_inspect', ] ) diff --git a/tox.ini b/tox.ini index 39b318d..ecc8ab4 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,6 @@ deps= behave coverage - typing_inspect py34: typing commands= diff --git a/typesafety/tests/test_typing_inspect.py b/typesafety/tests/test_typing_inspect.py new file mode 100644 index 0000000..556d145 --- /dev/null +++ b/typesafety/tests/test_typing_inspect.py @@ -0,0 +1,37 @@ +# +# Copyright (c) 2013-2018 Balabit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import typing +import unittest + +from typesafety.typing_inspect import is_union_type, get_union_args + + +class TestTypingInspect(unittest.TestCase): + def test_inspect_union_type(self): + self.assertTrue(is_union_type(typing.Union)) + self.assertTrue(is_union_type(typing.Union[int, str])) + self.assertTrue(is_union_type(typing.Optional[int])) + self.assertFalse(is_union_type(typing.List[int])) + self.assertFalse(is_union_type(typing.Any)) + + def test_get_union_args(self): + self.assertEqual((), get_union_args(typing.Union)) + self.assertEqual((int, str), get_union_args(typing.Union[int, str])) + self.assertEqual((int, type(None)), get_union_args(typing.Optional[int])) + self.assertRaises(TypeError, get_union_args, typing.List[int]) + self.assertRaises(TypeError, get_union_args, typing.Any) diff --git a/typesafety/typing_inspect.py b/typesafety/typing_inspect.py new file mode 100644 index 0000000..d13fc45 --- /dev/null +++ b/typesafety/typing_inspect.py @@ -0,0 +1,51 @@ +# +# Copyright (c) 2013-2018 Balabit +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +''' +This module contains helper methods for dealing with the contents of the typing module. This +module is not really stable so there are differences even between minor versions. See: + + https://docs.python.org/3.6/library/typing.html + +Note that this module is flaky and very hacky, but hopefully later we can use some external +tool for this. +''' + +# There is no good, idiomatic way to determine if an annotation is a union or not, +# so we're saddled with this hacky solution. +# pylint: disable=unidiomatic-typecheck,protected-access +import typing + + +def is_union_type(cls): + if hasattr(typing, '_Union'): + return type(cls) is typing._Union + + return type(cls) is typing.UnionMeta + + +def get_union_args(cls): + if not is_union_type(cls): + raise TypeError('expected union type') + + if hasattr(cls, '__args__'): + res = cls.__args__ + + else: + res = cls.__union_params__ + + return res if res is not None else () diff --git a/typesafety/validator.py b/typesafety/validator.py index bfd38f3..94dffad 100644 --- a/typesafety/validator.py +++ b/typesafety/validator.py @@ -19,7 +19,7 @@ import inspect import warnings -from typing_inspect import is_union_type, get_args +from typesafety.typing_inspect import is_union_type, get_union_args class TypesafetyError(Exception): @@ -174,6 +174,9 @@ def validate_arguments(self, locals_dict): raise TypesafetyError(message) def __format_expectation(self, annotation): + if is_union_type(annotation): + return 'typing.Union[{}]'.format(', '.join(entry.__name__ for entry in get_union_args(annotation))) + if isinstance(annotation, tuple): return "({})".format( ", ".join(self.__format_expectation(a) for a in annotation) @@ -278,7 +281,7 @@ def __is_valid(self, value, validator): if is_union_type(validator): return any( self.__is_valid(value, subvalidator) - for subvalidator in get_args(validator) + for subvalidator in get_union_args(validator) ) if isinstance(validator, type): @@ -307,7 +310,7 @@ def __is_valid_typecheck_annotation(self, validator): if is_union_type(validator): return all( self.__is_valid_typecheck_annotation(subvalidator) - for subvalidator in get_args(validator) + for subvalidator in get_union_args(validator) ) if isinstance(validator, type):