├── .gitignore ├── LICENSE ├── README.md ├── setup.py ├── tests ├── conftest.py ├── test_decorator.py └── test_handlers.py ├── tox.ini └── typo ├── __init__.py ├── _version.py ├── codegen.py ├── decorator.py ├── handlers.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | *.py[cod] 3 | *$py.class 4 | *.so 5 | /env/ 6 | /build/ 7 | /dist/ 8 | /sdist/ 9 | *.egg-info/ 10 | *.egg 11 | pip-log.txt 12 | htmlcov/ 13 | .tox/ 14 | .coverage 15 | .coverage.* 16 | .cache 17 | nosetests.xml 18 | coverage.xml 19 | *,cover 20 | .hypothesis/ 21 | .ipynb_checkpoints 22 | .python-version 23 | .env 24 | *.ipynb 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 Ivan Smirnov, @aldanor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### typo 2 | 3 | This package intends to provide run-time type checking for functions annotated 4 | with argument type hints (standard library `typing` module in Python 3.5, or 5 | `backports.typing` package in Python 3.3 / 3.4). 6 | 7 | Example: 8 | 9 | ```python 10 | from typing import Sequence, List 11 | from typo import type_check 12 | 13 | @type_check 14 | def f(x: int, s: Sequence[int]) -> List[int]: 15 | ... 16 | ``` 17 | 18 | The `@type_check` decorator ensures that the values passed to annotated 19 | arguments will have their types checked before the function is executed; 20 | return value can be optionally checked as well. 21 | 22 | If the value types are not consistent with the function signature, a 23 | `TypeError` with a descriptive error message will be raised. For 24 | instance, calling function `f` from the example above with wrong 25 | argument types, 26 | 27 | ```python 28 | >>> f(1, (0, 2.2)) 29 | ``` 30 | 31 | results in an exception being thrown: 32 | 33 | ``` 34 | TypeError: invalid item #1 of `s`: expected int, got float 35 | ``` 36 | 37 | *Note:* this is work-in-progress and not all `typing` primitives are 38 | supported; however all supported constructs should be covered by a 39 | good number of tests. 40 | 41 | Here's some of the supported type hints: simple types, `List`, `Dict`, 42 | `Tuple`, `Sequence`, `Set`, `TypeVar` (with support for constraints 43 | and upper bounds). 44 | 45 | What's not supported: `Iterator` and `Generator` (which we can't 46 | inspect due to their laziness), `Callable` (which we can't check 47 | without calling it), forward references (which is possible to 48 | support but requires more work), covariant and contravariant 49 | type variables (this requires more thought but isn't likely 50 | to be helpful in the runtime context). -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import io 4 | import re 5 | 6 | try: 7 | from setuptools import setup 8 | except ImportError: 9 | from distutils.core import setup 10 | 11 | version = '' 12 | with io.open('typo/_version.py', 'r') as fd: 13 | version = re.search(r'^__version__\s*=\s*[\'"]([^\'"]*)[\'"]', 14 | fd.read(), re.MULTILINE).group(1) 15 | 16 | setup( 17 | name='typo', 18 | version=version, 19 | description='Runtime type checking for functions with type annotations', 20 | author='Ivan Smirnov', 21 | author_email='i.s.smirnov@gmail.com', 22 | url='https://github.com/aldanor/typo', 23 | license='MIT', 24 | packages=['typo'], 25 | classifiers=( 26 | 'Development Status :: 3 - Alpha', 27 | 'Intended Audience :: Developers', 28 | 'Natural Language :: English', 29 | 'License :: OSI Approved :: MIT License', 30 | 'Programming Language :: Python', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: 3.3', 33 | 'Programming Language :: Python :: 3.4', 34 | 'Programming Language :: Python :: 3.5', 35 | ), 36 | keywords='typing type checking annotations', 37 | extras_require={ 38 | ':python_version == "3.3"': 'typing >= 3.5', 39 | ':python_version == "3.4"': 'typing >= 3.5' 40 | } 41 | ) 42 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import inspect 4 | import pytest 5 | 6 | 7 | def pytest_namespace(): 8 | return {'add_handler_test': add_handler_test, 9 | 'type_check_test': type_check_test, 10 | '_': _} 11 | 12 | 13 | def add_handler_test(name, bound, exp_str, ok=[], fail=[]): 14 | from typo.handlers import Handler 15 | 16 | handlers, params, ids = [], [], [] 17 | for n, b in enumerate(bound if isinstance(bound, tuple) else [bound]): 18 | h = Handler(b) 19 | handlers.append((h, h.compile())) 20 | 21 | params += [('str', n, None)] 22 | params += [('ok', n, arg) for arg in ok] 23 | params += [('fail', n, arg) for arg in fail] 24 | 25 | ids += ['str-{}'.format(n)] 26 | ids += ['ok-{}-{}'.format(n, i) for i in range(len(ok))] 27 | ids += ['fail-{}-{}'.format(n, i) for i in range(len(fail))] 28 | 29 | @pytest.mark.parametrize('test, n, arg', params, ids=ids) 30 | def func(test, n, arg): 31 | h, f = handlers[n] 32 | if test == 'str': 33 | assert str(h) == exp_str 34 | elif test == 'ok': 35 | f(arg) 36 | elif test == 'fail': 37 | arg, msg = arg 38 | pytest.raises_regexp(TypeError, msg, f, arg) 39 | 40 | inspect.stack()[1][0].f_locals[name] = func 41 | 42 | 43 | class _: 44 | def __init__(self, *args, **kwargs): 45 | self.args = args 46 | self.kwargs = kwargs 47 | 48 | def apply(self, func): 49 | return func(*self.args, **self.kwargs) 50 | 51 | 52 | def type_check_test(ok=[], fail=[]): 53 | from typo.decorator import type_check 54 | 55 | params, ids = [], [] 56 | 57 | params += [('func', None)] 58 | params += [('ok', arg) for arg in ok] 59 | params += [('fail', arg) for arg in fail] 60 | 61 | ids += ['func'] 62 | ids += ['ok-{}'.format(i) for i in range(len(ok))] 63 | ids += ['fail-{}'.format(i) for i in range(len(fail))] 64 | 65 | def decorator(func): 66 | wrapped = type_check(func) 67 | 68 | @pytest.mark.parametrize('test, arg', params, ids=ids) 69 | def test_runner(test, arg): 70 | if test == 'func': 71 | for magic in ('module', 'name', 'qualname', 'doc', 'annotations'): 72 | attr = '__' + magic + '__' 73 | assert getattr(func, attr) == getattr(wrapped, attr) 74 | assert isinstance(wrapped.wrapper_code, str) 75 | elif test == 'ok': 76 | arg.apply(wrapped) 77 | elif test == 'fail': 78 | arg, msg = arg 79 | pytest.raises_regexp(TypeError, msg, arg.apply, wrapped) 80 | 81 | test_runner.__name__ == func.__name__ 82 | return test_runner 83 | 84 | return decorator 85 | -------------------------------------------------------------------------------- /tests/test_decorator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from pytest import _, type_check_test 4 | 5 | 6 | @type_check_test() 7 | def test_wrapper(x: int, *args, **kwargs: type('T', (), {})) -> int: 8 | "Test docstring." 9 | 10 | 11 | @type_check_test( 12 | ok=[ 13 | _(1.1) 14 | ], 15 | fail=[ 16 | (_(1), 'invalid `x`: expected float, got int'), 17 | (_('foo'), 'invalid `x`: expected float, got str'), 18 | (_(0.), 'invalid return value: expected tuple, got float') 19 | ] 20 | ) 21 | def test_basic(x: float) -> tuple: 22 | return (x, x) if x else x 23 | 24 | 25 | @type_check_test( 26 | ok=[ 27 | _(), 28 | _(x=1), 29 | _(x=1, y=2), 30 | ], 31 | fail=[ 32 | (_(x='foo'), 'invalid keyword argument `x`: expected int, got str'), 33 | (_(x=1, y=2.2), 'invalid keyword argument `y`: expected int, got float') 34 | ] 35 | ) 36 | def test_kwargs(**kwargs: int): 37 | ... 38 | 39 | @type_check_test( 40 | ok=[ 41 | _(), 42 | _(1), 43 | _(1, 2) 44 | ], 45 | fail=[ 46 | (_('a'), r'invalid item #0 of `\*args`: expected int, got str'), 47 | (_(1, 'a'), r'invalid item #1 of `\*args`: expected int, got str') 48 | ] 49 | ) 50 | def test_varargs(*args: int): 51 | ... 52 | 53 | @type_check_test( 54 | ok=[ 55 | _(1), 56 | _(1, b='a'), 57 | _(1, c=1.1, d=1.2), 58 | _(1, b='a', c=1.1, d=1.2) 59 | ], 60 | fail=[ 61 | (_('a'), 'invalid `a`: expected int, got str'), 62 | (_(1, b=1), 'invalid `b`: expected str, got int'), 63 | (_(1, c=1.1, d='a'), 'keyword argument `d`: expected float, got str'), 64 | ] 65 | ) 66 | def test_mixed_args(a: int, *, b: str = 'foo', **kwargs: float): 67 | ... 68 | -------------------------------------------------------------------------------- /tests/test_handlers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import collections 4 | import pytest 5 | 6 | from collections import OrderedDict 7 | 8 | from typo.handlers import Handler 9 | from typing import Any, List, Tuple, Dict, Sequence, MutableSequence, Set, TypeVar 10 | 11 | 12 | pytest.add_handler_test( 13 | 'test_any', (Any, object), 'Any', 14 | ok=[ 15 | None, 16 | (1, '2', {}) 17 | ] 18 | ) 19 | 20 | pytest.add_handler_test( 21 | 'test_builtin_type', int, 'int', 22 | ok=[ 23 | 42 24 | ], 25 | fail=[ 26 | ('foo', 'expected int, got str') 27 | ] 28 | ) 29 | 30 | P = type('P', (), {}) 31 | A = type('A', (P,), {}) 32 | B = type('B', (A,), {}) 33 | 34 | pytest.add_handler_test( 35 | 'test_user_type', A, 'test_handlers.A', 36 | ok=[ 37 | A(), 38 | B() 39 | ], 40 | fail=[ 41 | (A, 'expected test_handlers.A, got type'), 42 | (42, 'expected test_handlers.A, got int'), 43 | (P(), 'expected test_handlers.A, got test_handlers.P') 44 | ] 45 | ) 46 | 47 | pytest.add_handler_test( 48 | 'test_list_basic', List[int], 'List[int]', 49 | ok=[ 50 | [], 51 | [1], 52 | [1, 2, 3] 53 | ], 54 | fail=[ 55 | (1, 'expected list, got int'), 56 | ([1, 'foo', 2], 'invalid item #1.*expected int, got str') 57 | ] 58 | ) 59 | 60 | pytest.add_handler_test( 61 | 'test_list_nested', List[List[int]], 'List[List[int]]', 62 | ok=[ 63 | [], 64 | [[]], 65 | [[], []], 66 | [[1, 2], [], [3]] 67 | ], 68 | fail=[ 69 | (1, 'expected list, got int'), 70 | ([[], 1, []], 'invalid item #1.*expected list, got int'), 71 | ([[], [1, 2, [], 4]], 'invalid item #2 of item #1.*expected int, got list') 72 | ] 73 | ) 74 | 75 | pytest.add_handler_test( 76 | 'test_list_no_typevar', (List, List[Any], List[object]), 'list', 77 | ok=[ 78 | [], 79 | [1, 'foo'] 80 | ], 81 | fail=[ 82 | (1, 'expected list, got int') 83 | ] 84 | ) 85 | 86 | pytest.add_handler_test( 87 | 'test_tuple_no_ellipsis', Tuple[int, str], 'Tuple[int, str]', 88 | ok=[ 89 | (1, 'foo') 90 | ], 91 | fail=[ 92 | (42, 'expected tuple, got int'), 93 | ((1, 2, 3), 'expected tuple of length 2, got tuple of length 3'), 94 | (('foo', 'bar'), 'invalid item #0.*expected int, got str'), 95 | ((1, 2), 'invalid item #1.*expected str, got int') 96 | ] 97 | ) 98 | 99 | pytest.add_handler_test( 100 | 'test_tuple_ellipsis', Tuple[int, ...], 'Tuple[int, ...]', 101 | ok=[ 102 | (), 103 | (1,), 104 | (1, 2, 3) 105 | ], 106 | fail=[ 107 | (42, 'expected tuple, got int'), 108 | ((1, 'foo'), 'invalid item #1.*expected int, got str') 109 | ] 110 | ) 111 | 112 | pytest.add_handler_test( 113 | 'test_tuple_no_typevar', (Tuple, Tuple[Any, ...], Tuple[object, ...]), 'tuple', 114 | ok=[ 115 | (), 116 | (1, 'foo') 117 | ], 118 | fail=[ 119 | (1, 'expected tuple, got int') 120 | ] 121 | ) 122 | 123 | pytest.add_handler_test( 124 | 'test_dict_basic', Dict[int, str], 'Dict[int, str]', 125 | ok=[ 126 | {}, 127 | {1: 'foo', 2: 'bar'} 128 | ], 129 | fail=[ 130 | (42, 'expected dict, got int'), 131 | ({1: 'foo', 2: 3}, 'invalid value at 2 of.*expected str, got int'), 132 | ({1: 'foo', 'bar': 'baz'}, 'invalid key of.*expected int, got str') 133 | ] 134 | ) 135 | 136 | pytest.add_handler_test( 137 | 'test_dict_complex', Dict[Tuple[object, int], List[Dict[Any, str]]], 138 | 'Dict[Tuple[Any, int], List[Dict[Any, str]]]', 139 | ok=[ 140 | {}, 141 | {('foo', 1): [{2: 'bar'}]} 142 | ], 143 | fail=[ 144 | ({('foo', 'bar'): []}, 145 | 'invalid item #1 of key of.*expected int, got str'), 146 | ({(1, 1): [{3: 'bar', 'baz': 2}]}, 147 | r'invalid value at \'baz\' of item #0 of value at \(1, 1\) of.*expected str, got int') 148 | ] 149 | ) 150 | 151 | pytest.add_handler_test( 152 | 'test_dict_no_typevar', (Dict, Dict[Any, Any], Dict[Any, object], 153 | Dict[object, Any], Dict[object, object]), 'dict', 154 | ok=[ 155 | {}, 156 | {1: 'foo', 'bar': 2} 157 | ], 158 | fail=[ 159 | (42, 'expected dict, got int') 160 | ] 161 | ) 162 | 163 | 164 | class MySequence(collections.Sequence): 165 | def __len__(self): 166 | return 2 167 | 168 | def __getitem__(self, k): 169 | return [0, 1][k] 170 | 171 | 172 | class MyMutableSequence(MySequence, collections.MutableSequence): 173 | def __setitem__(self, k): 174 | pass 175 | 176 | def __delitem__(self, k): 177 | pass 178 | 179 | def insert(self, k, v): 180 | pass 181 | 182 | 183 | pytest.add_handler_test( 184 | 'test_sequence', Sequence[int], 'Sequence[int]', 185 | ok=[ 186 | [], 187 | [1, 2], 188 | MySequence(), 189 | MyMutableSequence() 190 | ], 191 | fail=[ 192 | (42, 'expected sequence, got int'), 193 | ([1, '2'], 'invalid item #1.*expected int, got str') 194 | ] 195 | ) 196 | 197 | pytest.add_handler_test( 198 | 'test_sequence_no_typevar', (Sequence, collections.Sequence, 199 | Sequence[object], Sequence[Any]), 200 | 'Sequence', 201 | ok=[ 202 | [], 203 | [1, 'foo'], 204 | MySequence(), 205 | MyMutableSequence() 206 | ], 207 | fail=[ 208 | (42, 'expected sequence, got int') 209 | ] 210 | ) 211 | 212 | pytest.add_handler_test( 213 | 'test_mutable_sequence', MutableSequence[int], 'MutableSequence[int]', 214 | ok=[ 215 | [], 216 | [1, 2], 217 | MyMutableSequence() 218 | ], 219 | fail=[ 220 | (42, 'expected mutable sequence, got int'), 221 | (MySequence(), 'expected mutable sequence, got test_handlers.MySequence'), 222 | ([1, '2'], 'invalid item #1.*expected int, got str') 223 | ] 224 | ) 225 | 226 | pytest.add_handler_test( 227 | 'test_sequence_no_typevar', (MutableSequence, collections.MutableSequence, 228 | MutableSequence[object], MutableSequence[Any]), 229 | 'MutableSequence', 230 | ok=[ 231 | [], 232 | [1, 'foo'], 233 | MyMutableSequence() 234 | ], 235 | fail=[ 236 | (42, 'expected mutable sequence, got int'), 237 | (MySequence(), 'expected mutable sequence, got test_handlers.MySequence') 238 | ] 239 | ) 240 | 241 | pytest.add_handler_test( 242 | 'test_set', Set[int], 'Set[int]', 243 | ok=[ 244 | set(), 245 | {1}, 246 | {1, 2, 3} 247 | ], 248 | fail=[ 249 | (1, 'expected set, got int'), 250 | ({1, 'foo', 2}, 'invalid item of.*expected int, got str') 251 | ] 252 | ) 253 | 254 | pytest.add_handler_test( 255 | 'test_set_no_typevar', (Set, Set[Any], Set[object]), 'set', 256 | ok=[ 257 | set(), 258 | {1, 'foo'} 259 | ], 260 | fail=[ 261 | (1, 'expected set, got int') 262 | ] 263 | ) 264 | 265 | 266 | @pytest.mark.parametrize('bound', [ 267 | List['T'], List[TypeVar('T', int, 'T')] 268 | ]) 269 | def test_forward_reference(bound): 270 | pytest.raises_regexp(ValueError, 'forward references are not currently supported', 271 | Handler, bound) 272 | 273 | 274 | @pytest.mark.parametrize('bound', [ 275 | List[int], Dict[int, int], Set[int], TypeVar('X'), List[TypeVar('X')] 276 | ]) 277 | def test_invalid_typevar_bound(bound): 278 | T = TypeVar('T', bound=bound) 279 | pytest.raises_regexp(ValueError, 'invalid typevar bound', 280 | Handler, T) 281 | 282 | 283 | @pytest.mark.parametrize('constraint', [ 284 | Any, List[int], Dict[int, int], Set[int] 285 | ]) 286 | def test_invalid_typevar_constraint(constraint): 287 | T = TypeVar('T', int, constraint) 288 | pytest.raises_regexp(ValueError, 'invalid typevar constraint', 289 | Handler, T) 290 | 291 | 292 | class Int(int): 293 | ... 294 | 295 | T, U = TypeVar('T'), TypeVar('U') 296 | 297 | 298 | pytest.add_handler_test( 299 | 'test_typevar_basic', Tuple[T, Dict[U, T]], 'Tuple[T, Dict[U, T]]', 300 | ok=[ 301 | (1, {}), 302 | (1, {1: 2, 3: 4}), 303 | ({}, {'a': {1: 2}}) 304 | ], 305 | fail=[ 306 | (1, 'expected tuple, got int'), 307 | ((1, {2: '3'}), 'invalid value at 2 of item #1.*cannot assign str to T'), 308 | ((1, OrderedDict([('a', 1), (2, 3)])), 'key.*cannot assign int to U'), 309 | ((1, OrderedDict([(Int(2), 3), (4, 5)])), 'key.*cannot assign int to U'), 310 | ((1, OrderedDict([(2, 3), (Int(4), 5)])), 'key.*cannot assign test_handlers.Int to U'), 311 | ((Int(1), {'a': 2}), 'invalid value.*cannot assign int to T'), 312 | ((1, {'a': Int(2)}), 'invalid value.*cannot assign test_handlers.Int to T') 313 | ] 314 | ) 315 | 316 | V = TypeVar('V', bound=int) 317 | 318 | pytest.add_handler_test( 319 | 'test_typevar_bound', List[V], 'List[V]', 320 | ok=[ 321 | [], 322 | [1, 2], 323 | [Int(1), Int(2)] 324 | ], 325 | fail=[ 326 | ('foo', 'expected list, got str'), 327 | (['a'], r'cannot assign str to V'), 328 | ([1, 'a'], r'cannot assign str to V'), 329 | ([1, Int(2)], r'cannot assign test_handlers.Int to V'), 330 | ([Int(1), 2], r'cannot assign int to V') 331 | ] 332 | ) 333 | 334 | W = TypeVar('W', int, float) 335 | 336 | pytest.add_handler_test( 337 | 'test_typevar_basic_constraints', Tuple[W, W], 'Tuple[W, W]', 338 | ok=[ 339 | (1, 2), 340 | (1.1, 2.2) 341 | ], 342 | fail=[ 343 | (('a', 'b'), 'cannot assign str to W'), 344 | ((1, 2.2), 'cannot assign float to W'), 345 | ((1.1, 2), 'cannot assign int to W'), 346 | ((Int(1), Int(1)), 'cannot assign test_handlers.Int to W') 347 | ] 348 | ) 349 | 350 | X = TypeVar('X', str, W, T) 351 | 352 | pytest.add_handler_test( 353 | 'test_typevar_complex_constraints', Tuple[X, W, T], 'Tuple[X, W, T]', 354 | ok=[ 355 | ('a', 1, {}), 356 | ('a', 2.2, {}), 357 | (1, 2, {}), 358 | (1.1, 2.2, {}), 359 | ([], 1, []), 360 | ([], 1.1, []) 361 | ], 362 | fail=[ 363 | (('a', 'b', {}), 'cannot assign str to W'), 364 | ((1, 'b', {}), 'cannot assign str to W'), 365 | ((1.1, 'b', {}), 'cannot assign str to W'), 366 | (([], 'b', {}), 'cannot assign str to W'), 367 | (([], 1, 'a'), 'cannot assign str to T') 368 | ] 369 | ) 370 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = cov-clean, py{33,34,35}, cov-report 3 | 4 | [testenv] 5 | deps = 6 | coverage >=4.0 7 | pytest >=3.0 8 | pytest-raisesregexp 9 | commands = 10 | coverage run -p --rcfile={toxinidir}/tox.ini -m pytest -s {posargs} 11 | 12 | [testenv:cov-clean] 13 | skip_install = true 14 | deps = coverage 15 | commands = 16 | coverage erase 17 | 18 | [testenv:cov-report] 19 | skip_install = true 20 | deps = coverage 21 | commands = 22 | coverage combine --rcfile={toxinidir}/tox.ini 23 | coverage report --rcfile={toxinidir}/tox.ini 24 | coverage html --rcfile={toxinidir}/tox.ini 25 | 26 | [run] 27 | branch = True 28 | source = typo 29 | omit = 30 | **/__init__.py 31 | **/_version.py 32 | 33 | [report] 34 | exclude_lines = 35 | pragma: no cover 36 | raise NotImplementedError 37 | @abc.abstract 38 | 39 | [paths] 40 | source = 41 | typo 42 | .tox/py*/lib/python*/site-packages/typo 43 | 44 | [flake8] 45 | max-line-length = 99 46 | -------------------------------------------------------------------------------- /typo/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from typo.decorator import type_check 4 | 5 | __all__ = ('type_check',) 6 | -------------------------------------------------------------------------------- /typo/_version.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | __version__ = '0.0.1' 4 | -------------------------------------------------------------------------------- /typo/codegen.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import collections 4 | import contextlib 5 | import typing 6 | 7 | from typing import Any, Union, Tuple, List 8 | 9 | from typo.utils import type_name 10 | 11 | 12 | class Codegen: 13 | _v_cache_seq = {list: True, tuple: True, str: True, bytes: True, 14 | bytearray: True, memoryview: True} 15 | _v_cache_mut_seq = {list: True} 16 | 17 | def __init__(self, typevars=None): 18 | # TODO: accept list of handlers, build the set of typevars here 19 | self.lines = [] 20 | self.indent_level = 0 21 | self.next_var_id = 0 22 | self.next_type_id = 0 23 | self.types = {} 24 | self.typevars = sorted(typevars or [], key=str) 25 | # TODO: all names injected through context should start with underscore 26 | self.context = { 27 | 'collections': collections, 28 | 'typing': typing, 29 | 'rt_fail': self.rt_fail, 30 | 'rt_type_fail': self.rt_type_fail, 31 | 'rt_fail_msg': self.rt_fail_msg, 32 | 'v_cache_seq': self._v_cache_seq, 33 | 'v_cache_mut_seq': self._v_cache_mut_seq, 34 | } 35 | for i, tv in enumerate(self.typevars): 36 | if tv.__constraints__: 37 | self.context['constraints_{}'.format(i)] = tv.__constraints__ 38 | 39 | def typevar_id(self, typevar): 40 | return self.typevars.index(typevar) 41 | 42 | def init_typevars(self): 43 | self.write_line('tv = [{!r}]'.format([None] * len(self.typevars))) 44 | 45 | def compile(self, name): 46 | context = self.context.copy() 47 | exec(str(self), context) 48 | return context[name] 49 | 50 | @staticmethod 51 | def rt_fail(desc: str, expected: str, var: Any, got: str, **kwargs): 52 | raise TypeError('invalid {}: expected {}, got {}' 53 | .format(desc.format(**kwargs), expected, got.format(**kwargs))) 54 | 55 | @staticmethod 56 | def rt_type_fail(desc: str, expected: str, var: Any, **kwargs): 57 | raise TypeError('invalid {}: expected {}, got {}' 58 | .format(desc.format(**kwargs), expected, type_name(type(var)))) 59 | 60 | @staticmethod 61 | def rt_fail_msg(desc: str, msg: str, var: Any, **kwargs): 62 | raise TypeError('invalid {}: {}'.format(desc.format(**kwargs), 63 | msg.format(tp=type_name(type(var)), **kwargs))) 64 | 65 | def write_line(self, line): 66 | self.lines.append(' ' * self.indent_level * 4 + line) 67 | 68 | @contextlib.contextmanager 69 | def indent(self): 70 | self.indent_level += 1 71 | yield 72 | self.indent_level -= 1 73 | 74 | def new_var(self): 75 | varname = 'v_{:03d}'.format(self.next_var_id) 76 | self.next_var_id += 1 77 | return varname 78 | 79 | def new_vars(self, n): 80 | return tuple(self.new_var() for _ in range(n)) 81 | 82 | def ref_type(self, tp): 83 | if tp.__module__ == 'builtins': 84 | return tp.__name__ 85 | elif tp.__module__ == 'collections.abc': 86 | return 'collections.' + tp.__name__ 87 | elif tp.__module__ == 'typing': 88 | return 'typing.' + tp.__name__ 89 | elif tp not in self.types: 90 | varname = 'T_{}'.format(self.next_type_id) 91 | self.next_type_id += 1 92 | self.types[tp] = varname 93 | self.context[varname] = tp 94 | return self.types[tp] 95 | 96 | def fail(self, desc: str, expected: str, varname: str, got: str=None): 97 | if desc is None: 98 | self.write_line('raise TypeError') 99 | elif got is None: 100 | self.write_line('rt_type_fail("{}", "{}", {}, **locals())' 101 | .format(desc, expected, varname)) 102 | else: 103 | self.write_line('rt_fail("{}", "{}", {}, "{}", **locals())' 104 | .format(desc, expected, varname, got)) 105 | 106 | def fail_msg(self, desc: str, msg: str, varname: str): 107 | if desc is None: 108 | self.write_line('raise TypeError') 109 | else: 110 | self.write_line('rt_fail_msg("{}", "{}", {}, **locals())' 111 | .format(desc, msg, varname)) 112 | 113 | def if_not_isinstance(self, varname: str, tp: Union[type, Tuple[type, ...]]) -> None: 114 | if isinstance(tp, tuple): 115 | if len(tp) == 1: 116 | tp = self.ref_type(tp[0]) 117 | else: 118 | tp = '({})'.format(', '.join(map(self.ref_type, tp))) 119 | else: 120 | tp = self.ref_type(tp) 121 | 122 | self.write_line('if not isinstance({}, {}):'.format(varname, tp)) 123 | 124 | def check_type(self, varname: str, desc: str, tp: Union[Tuple[type, ...], type]): 125 | if isinstance(tp, tuple): 126 | if len(tp) == 1: 127 | expected = type_name(tp) 128 | else: 129 | expected = ' or '.join(map(type_name, tp)) 130 | else: 131 | expected = type_name(tp) 132 | 133 | self.if_not_isinstance(varname, tp) 134 | with self.indent(): 135 | self.fail(desc, expected, varname) 136 | 137 | def iter_and_check(self, varname: str, desc: str, 138 | handler: 'typo.handlers.Handler') -> None: 139 | var_v = self.new_var() 140 | self.write_line('for {} in {}:'.format(var_v, varname)) 141 | with self.indent(): 142 | handler(self, var_v, None if desc is None else 143 | 'item of {}'.format(desc)) 144 | 145 | def enumerate_and_check(self, varname: str, desc: str, 146 | handler: 'typo.handlers.Handler') -> None: 147 | var_i, var_v = self.new_var(), self.new_var() 148 | self.write_line('for {}, {} in enumerate({}):'.format(var_i, var_v, varname)) 149 | with self.indent(): 150 | handler(self, var_v, None if desc is None else 151 | 'item #{{{}}} of {}'.format(var_i, desc)) 152 | 153 | def check_attrs_cached(self, varname: str, desc: str, expected: str, 154 | cache: str, attrs: List[str]) -> None: 155 | var_t = self.new_var() 156 | self.write_line('{} = type({})'.format(var_t, varname)) 157 | self.write_line('if {} in {}:'.format(var_t, cache)) 158 | var_a = self.new_var() 159 | with self.indent(): 160 | self.write_line('{} = {}[{}]'.format(var_a, cache, var_t)) 161 | self.write_line('else:') 162 | with self.indent(): 163 | conds = ['hasattr({}, "{}")'.format(varname, attr) for attr in attrs] 164 | self.write_line('{} = {}'.format(var_a, ' and '.join(conds))) 165 | self.write_line('{}[{}] = {}'.format(cache, var_t, var_a)) 166 | self.write_line('if not {}:'.format(var_a)) 167 | with self.indent(): 168 | self.fail(desc, expected, varname) 169 | 170 | def __str__(self): 171 | return '\n'.join(self.lines) + '\n' 172 | -------------------------------------------------------------------------------- /typo/decorator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import inspect 4 | import functools 5 | 6 | from typing import Any, Callable, Optional 7 | 8 | from typo.codegen import Codegen 9 | from typo.handlers import Handler 10 | 11 | 12 | class KeywordArgsHandler(Handler): 13 | def __init__(self, bound: Any) -> None: 14 | super().__init__(bound) 15 | self.handler = Handler(bound) 16 | 17 | def __call__(self, gen: Codegen, varname: str, desc: Optional[str]) -> None: 18 | if not self.handler.is_any: 19 | var_k, var_v = gen.new_vars(2) 20 | gen.write_line('for {}, {}, in {}.items():'.format(var_k, var_v, varname)) 21 | with gen.indent(): 22 | self.handler(gen, var_v, None if desc is None else 23 | 'keyword argument `{{{}}}`'.format(var_k)) 24 | 25 | def __str__(self) -> str: 26 | return 'KeywordArgs[{}]'.format(self.handler) 27 | 28 | 29 | class PositionalArgsHandler(Handler): 30 | def __init__(self, bound: Any) -> None: 31 | super().__init__(bound) 32 | self.handler = Handler(bound) 33 | 34 | def __call__(self, gen: Codegen, varname: str, desc: Optional[str]) -> None: 35 | if not self.handler.is_any: 36 | gen.enumerate_and_check(varname, desc, self.handler) 37 | 38 | def __str__(self) -> str: 39 | return 'PositionalArgs[{}]'.format(self.handler) 40 | 41 | 42 | def type_check(func: Callable) -> Callable: 43 | annotations = func.__annotations__ 44 | 45 | # Extract function signature without type annotations -- this is because annotations 46 | # may contain user types, so we don't want to stringify them and instead pass the 47 | # annotations dict to the wrapped function as is. 48 | func.__annotations__ = {} 49 | signature = inspect.signature(func) 50 | func.__annotations__ = annotations 51 | 52 | # Build call arguments and type checking handlers for annotated arguments. 53 | return_handler = Handler(annotations.get('return', Any)) 54 | call_args, handlers = [], {} 55 | for arg, param in signature.parameters.items(): 56 | handler_type = Handler 57 | call_prefix = arg + '=' 58 | if param.kind == inspect._VAR_KEYWORD: 59 | handler_type = KeywordArgsHandler 60 | call_prefix = '**' 61 | elif param.kind == inspect._VAR_POSITIONAL: 62 | handler_type = PositionalArgsHandler 63 | call_prefix = '*' 64 | call_args.append(call_prefix + arg) 65 | if arg in annotations: 66 | handlers[arg] = handler_type(annotations[arg]) 67 | 68 | # Generate a set of all typevars used in the function signature. 69 | typevars = set.union(return_handler.typevars, *(h.typevars for h in handlers.values())) 70 | 71 | # Store the function itself in the codegen context (wrapper closure). 72 | gen = Codegen(typevars=typevars) 73 | func_var, return_var = gen.new_vars(2) 74 | gen.context[func_var] = func 75 | 76 | # Generate code for the function body. 77 | gen.write_line('def {}{}:'.format(func.__name__, str(signature))) 78 | with gen.indent(): 79 | # Initialize typevars if required. 80 | if gen.typevars: 81 | gen.init_typevars() 82 | 83 | # Execute all handlers. 84 | for arg in signature.parameters: 85 | if arg in handlers: 86 | handler = handlers[arg] 87 | if not handler.is_any: 88 | var_desc = { 89 | KeywordArgsHandler: 'keyword arguments', 90 | PositionalArgsHandler: '`*{}`'.format(arg) 91 | }.get(type(handler), '`{}`'.format(arg)) 92 | handler(gen, arg, var_desc) 93 | 94 | # Call the function and remember the return value. 95 | # Optionally, also check the return value type before returning. 96 | func_call = '{}({})'.format(func_var, ', '.join(call_args)) 97 | if not return_handler.is_any: 98 | gen.write_line('{} = {}'.format(return_var, func_call)) 99 | return_handler(gen, return_var, 'return value') 100 | gen.write_line('return {}'.format(return_var)) 101 | else: 102 | gen.write_line('return {}'.format(func_call)) 103 | 104 | # Compile the wrapper and reattach docstring, annotations, qualname, etc. 105 | compiled = gen.compile(func.__name__) 106 | wrapper = functools.wraps(func)(compiled) 107 | wrapper.wrapper_code = str(gen) 108 | 109 | return wrapper 110 | -------------------------------------------------------------------------------- /typo/handlers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import abc 4 | import collections 5 | 6 | from typing import ( 7 | Any, Dict, List, Tuple, Union, Optional, Callable, Sequence, MutableSequence, Set, 8 | TypeVar, _ForwardRef 9 | ) 10 | 11 | from typo.codegen import Codegen 12 | from typo.utils import type_name 13 | 14 | 15 | class HandlerMeta(abc.ABCMeta): 16 | origin_handlers = {} 17 | subclass_handlers = {} 18 | 19 | def __new__(meta, name, bases, ns, *, origin=None, subclass=None): 20 | cls = super().__new__(meta, name, bases, ns) 21 | if origin is not None: 22 | meta.origin_handlers[origin] = cls 23 | elif subclass is not None: 24 | meta.subclass_handlers[type(subclass)] = cls 25 | return cls 26 | 27 | def __init__(self, name, bases, ns, **kwargs): 28 | super().__init__(name, bases, ns) 29 | 30 | def __call__(cls, bound: Any) -> None: 31 | if cls is not Handler: 32 | tp = cls 33 | else: 34 | bound = { 35 | Tuple: Tuple[Any, ...], 36 | collections.Sequence: Sequence, 37 | collections.MutableSequence: MutableSequence 38 | }.get(bound, bound) 39 | 40 | origin = getattr(bound, '__origin__', None) 41 | 42 | # Note that it should be possible to resolve forward references since they 43 | # store frames in which they were declared; would require a bit more work. 44 | if isinstance(bound, _ForwardRef): 45 | raise ValueError('forward references are not currently supported: {}' 46 | .format(bound)) 47 | 48 | if bound in (object, Any): 49 | tp = AnyHandler 50 | elif origin in cls.origin_handlers: 51 | tp = cls.origin_handlers[origin] 52 | elif bound in cls.origin_handlers: 53 | tp = cls.origin_handlers[bound] 54 | bound = bound[(Any,) * len(bound.__parameters__)] 55 | elif type(bound) in cls.subclass_handlers: 56 | tp = cls.subclass_handlers[type(bound)] 57 | elif isinstance(bound, type): 58 | tp = TypeHandler 59 | else: 60 | raise TypeError('invalid type annotation: {!r}'.format(bound)) 61 | 62 | instance = object.__new__(tp) 63 | instance.__init__(bound) 64 | return instance 65 | 66 | 67 | class Handler(metaclass=HandlerMeta): 68 | def __init__(self, bound: Any) -> None: 69 | self.bound = bound 70 | 71 | @abc.abstractmethod 72 | def __call__(self, gen: Codegen, varname: str, desc: Optional[str]) -> None: 73 | raise NotImplementedError 74 | 75 | @abc.abstractmethod 76 | def __str__(self) -> str: 77 | raise NotImplementedError 78 | 79 | @property 80 | def args(self) -> Tuple[Any, ...]: 81 | if hasattr(self.bound, '__args__'): 82 | return self.bound.__args__ 83 | return self.bound.__parameters__ 84 | 85 | def compile(self) -> Callable[[Any], None]: 86 | gen = Codegen(typevars=self.typevars) 87 | var = gen.new_var() 88 | gen.write_line('def check({}):'.format(var)) 89 | with gen.indent(): 90 | if self.typevars: 91 | gen.init_typevars() 92 | self(gen, var, 'input') 93 | return gen.compile('check') 94 | 95 | @property 96 | def is_any(self) -> bool: 97 | return False 98 | 99 | @property 100 | def typevars(self) -> Set[type(TypeVar)]: 101 | return set() 102 | 103 | @property 104 | def valid_typevar_bound(self) -> bool: 105 | return False 106 | 107 | def valid_typevar_constraint(self, typevar) -> bool: 108 | return self.valid_typevar_bound 109 | 110 | 111 | class SingleArgumentHandler(Handler): 112 | def __init__(self, bound: Any) -> None: 113 | super().__init__(bound) 114 | self.handler = Handler(self.args[0]) 115 | 116 | @property 117 | def typevars(self) -> Set[type(TypeVar)]: 118 | return self.handler.typevars 119 | 120 | 121 | class AnyHandler(Handler): 122 | def __call__(self, gen: Codegen, varname: str, desc: Optional[str]) -> None: 123 | gen.write_line('pass') 124 | 125 | def __str__(self) -> str: 126 | return 'Any' 127 | 128 | @property 129 | def is_any(self) -> bool: 130 | return True 131 | 132 | @property 133 | def valid_typevar_bound(self) -> bool: 134 | return True 135 | 136 | 137 | class TypeHandler(Handler): 138 | def __call__(self, gen: Codegen, varname: str, desc: Optional[str]) -> None: 139 | gen.check_type(varname, desc, self.bound) 140 | 141 | def __str__(self) -> str: 142 | return type_name(self.bound) 143 | 144 | @property 145 | def valid_typevar_bound(self) -> bool: 146 | return True 147 | 148 | 149 | class TypeVarHandler(Handler, subclass=TypeVar('')): 150 | def __init__(self, bound: Any) -> None: 151 | super().__init__(bound) 152 | 153 | self.type_constraints = () 154 | self.typevar_constraints = [] 155 | if self.bound.__constraints__ is not None: 156 | handlers = [Handler(c) for c in self.bound.__constraints__] 157 | for h in handlers: 158 | if not isinstance(h, (TypeHandler, TypeVarHandler)): 159 | raise ValueError('invalid typevar constraint: {}'.format(h)) 160 | if self.bound in h.typevars: 161 | raise ValueError('recursive typevar constraint: {}'.format(h)) 162 | self.type_constraints = tuple(h.bound for h in handlers if isinstance(h, TypeHandler)) 163 | self.typevar_constraints = [h for h in handlers if isinstance(h, TypeVarHandler)] 164 | 165 | self.bound_handler = None 166 | if self.bound.__bound__ is not None: 167 | self.bound_handler = Handler(self.bound.__bound__) 168 | if not self.bound_handler.valid_typevar_bound: 169 | raise ValueError('invalid typevar bound: {}'.format(self.bound_handler)) 170 | 171 | @property 172 | def has_constraints(self) -> bool: 173 | return bool(self.type_constraints or self.typevar_constraints) 174 | 175 | @property 176 | def has_bound(self) -> bool: 177 | return self.bound_handler is not None 178 | 179 | def __call__(self, gen: Codegen, varname: str, desc: Optional[str]) -> None: 180 | # TODO: don't need outer list if there are no generic unions 181 | # TODO: can infer when the typevar has been definitely set 182 | # TODO: can infer when the typevar is definitely uninitialized 183 | # TODO: more efficient typevar processing for sequences 184 | # TODO: simplify indent/write_line, make them accept fmt args 185 | # TODO: List, Dict, Sequence, etc should be valid bounds 186 | # TODO: support forward references (_ForwardRef), care about recursion 187 | var_i, var_tv, var_tp, var_len, var_k = gen.new_vars(5) 188 | index = gen.typevar_id(self.bound) 189 | tv = '{}[{}]'.format(var_tv, index) 190 | gen.write_line('{} = type({})'.format(var_tp, varname)) 191 | gen.write_line('{} = len(tv)'.format(var_len)) 192 | 193 | # for all possible assignments to type variables 194 | gen.write_line('for {}, {} in enumerate(list(tv)):'.format(var_i, var_tv)) 195 | with gen.indent(): 196 | gen.write_line('{} = {} + len(tv) - {}'.format(var_k, var_i, var_len)) 197 | 198 | # if the type variable of interest is already set in this assignment 199 | gen.write_line('if {} is not None:'.format(tv)) 200 | with gen.indent(): 201 | # check if value type matches it exactly 202 | gen.write_line('if {} is not {}:'.format(tv, var_tp)) 203 | with gen.indent(): 204 | # if not, the assignment is inconsistent, remove it 205 | gen.write_line('tv.pop({})'.format(var_k)) 206 | # and go on to the next assignment 207 | gen.write_line('continue') 208 | 209 | # otherwise, the type variable has not been bound; first, consider 210 | # the case where the type variable has a specified upper bound 211 | if self.has_bound: 212 | # try to run the bound handler 213 | gen.write_line('try:') 214 | with gen.indent(): 215 | self.bound_handler(gen, varname, None) 216 | # if it succeeds, bind the type variable to class of the value 217 | gen.write_line('{} = {}'.format(tv, var_tp)) 218 | gen.write_line('except TypeError:') 219 | with gen.indent(): 220 | # otherwise, the assignment is inconsistent, remove it 221 | gen.write_line('tv.pop({})'.format(var_k)) 222 | 223 | # second, if the type variable has invariant constraints 224 | elif self.has_constraints: 225 | # if there are simple class constraints, check them first 226 | if self.type_constraints: 227 | types = ', '.join(gen.ref_type(tp) for tp in self.type_constraints) + ', ' 228 | # if the class of the value matches one of the constraints exactly 229 | gen.write_line('if {} in ({}):'.format(var_tp, types)) 230 | with gen.indent(): 231 | # then bind the type variable to the class of the value and go on 232 | gen.write_line('{} = {}'.format(tv, var_tp)) 233 | gen.write_line('continue') 234 | 235 | # if there are no simple class constraints or if they are not satisfied, 236 | # check if there are any generic constraints 237 | if self.typevar_constraints: 238 | # this is complicated... (somewhat similar to union type) 239 | # ... should really use linked lists for all of this stuff 240 | var_old_tv, var_tv_init, var_tv_res = gen.new_vars(3) 241 | gen.write_line('{} = [list({})]'.format(var_tv_init, var_tv)) 242 | gen.write_line('{}[0][{}] = {}'.format(var_tv_init, index, var_tp)) 243 | gen.write_line('{} = tv'.format(var_old_tv)) 244 | gen.write_line('tv.pop({})'.format(var_k)) 245 | for handler in self.typevar_constraints: 246 | # TODO: simplify this down a bit 247 | gen.write_line('tv = [list({}[0])]'.format(var_tv_init)) 248 | gen.write_line('try:') 249 | with gen.indent(): 250 | handler(gen, varname, None) 251 | gen.write_line('for {} in tv:'.format(var_tv_res)) 252 | with gen.indent(): 253 | gen.write_line('{}.insert({}, {})' 254 | .format(var_old_tv, var_k, var_tv_res)) 255 | gen.write_line('except TypeError:') 256 | with gen.indent(): 257 | gen.write_line('pass') 258 | gen.write_line('tv = {}'.format(var_old_tv)) 259 | 260 | # there are no generic constraints, and simple constraints failed 261 | else: 262 | gen.write_line('tv.pop({})'.format(var_k)) 263 | 264 | # there are no bounds nor constraints, just bind the type variable 265 | else: 266 | gen.write_line('{} = {}'.format(tv, var_tp)) 267 | 268 | # if the list of valid assignments is now empty, it is a fail 269 | gen.write_line('if not tv:') 270 | with gen.indent(): 271 | gen.fail_msg(desc, 'cannot assign {{tp}} to {}'.format(self), varname) 272 | 273 | def __str__(self) -> str: 274 | return self.bound.__name__ 275 | 276 | @property 277 | def typevars(self) -> Set[type(TypeVar)]: 278 | return {self.bound} 279 | 280 | @property 281 | def valid_typevar_bound(self) -> bool: 282 | # Technically, this is possible but would require a bit more codegen work. The problem 283 | # is that in the current implementation running the bound handler would mutate the 284 | # current state of typevars the first time it sees the bound, i.e. in this example: 285 | # class Int(int): ... 286 | # T = TypeVar('T') 287 | # U = TypeVar('U', bound=T) 288 | # and this signature: 289 | # Tuple[U, T] 290 | # the following input would fail: 291 | # (Int(1), 2) 292 | # although Int is a subclass of int, this should be accepted but it's not, because 293 | # T is erroneously set to Int; instead it should remember that Int is now a 294 | # *subclass* (lower bound) for type variable T. 295 | return False 296 | 297 | 298 | class DictHandler(Handler, origin=Dict): 299 | def __init__(self, bound: Any) -> None: 300 | super().__init__(bound) 301 | self.key_handler = Handler(self.args[0]) 302 | self.value_handler = Handler(self.args[1]) 303 | 304 | def __call__(self, gen: Codegen, varname: str, desc: Optional[str]) -> None: 305 | gen.check_type(varname, desc, dict) 306 | if not self.key_handler.is_any or not self.value_handler.is_any: 307 | var_k, var_v = gen.new_var(), gen.new_var() 308 | gen.write_line('for {}, {} in {}.items():'.format(var_k, var_v, varname)) 309 | with gen.indent(): 310 | self.key_handler(gen, var_k, None if desc is None else 311 | 'key of {}'.format(desc)) 312 | self.value_handler(gen, var_v, None if desc is None else 313 | 'value at {{{}!r}} of {}'.format(var_k, desc)) 314 | 315 | def __str__(self) -> str: 316 | if self.key_handler.is_any and self.value_handler.is_any: 317 | return 'dict' 318 | return 'Dict[{}, {}]'.format(self.key_handler, self.value_handler) 319 | 320 | @property 321 | def typevars(self) -> Set[type(TypeVar)]: 322 | return self.key_handler.typevars | self.value_handler.typevars 323 | 324 | 325 | class ListHandler(SingleArgumentHandler, origin=List): 326 | def __call__(self, gen: Codegen, varname: str, desc: Optional[str]) -> None: 327 | gen.check_type(varname, desc, list) 328 | if not self.handler.is_any: 329 | gen.enumerate_and_check(varname, desc, self.handler) 330 | 331 | def __str__(self) -> str: 332 | if self.handler.is_any: 333 | return 'list' 334 | return 'List[{}]'.format(self.handler) 335 | 336 | 337 | class UnionHandler(Handler, subclass=Union): 338 | def __init__(self, bound: Any) -> None: 339 | super().__init__(bound) 340 | self.all_handlers = [Handler(p) for p in bound.__union_params__] 341 | self.handlers = [h for h in self.all_handlers 342 | if not isinstance(h, (AnyHandler, TypeHandler))] 343 | self.types = tuple(h.bound for h in self.all_handlers 344 | if isinstance(h, TypeHandler)) 345 | 346 | def __call__(self, gen: Codegen, varname: str, desc: Optional[str]) -> None: 347 | if not self.handlers and not self.types: 348 | gen.write_line('pass') 349 | elif len(self.handlers) == 1 and not self.types: 350 | self.handlers[0](gen, varname, desc) 351 | elif not self.handlers and self.types: 352 | gen.check_type(varname, desc, self.types) 353 | else: 354 | var = gen.new_var() 355 | gen.write_line('{} = True'.format(var)) 356 | handlers = self.handlers 357 | if self.types: 358 | handlers = [self.types] + handlers 359 | for handler in handlers: 360 | if isinstance(handler, tuple): 361 | gen.if_not_isinstance(varname, handler) 362 | else: 363 | gen.write_line('try:') 364 | with gen.indent(): 365 | handler(gen, varname, None) 366 | gen.write_line('except TypeError:') 367 | gen.indent_level += 1 368 | gen.write_line('{} = False'.format(var)) 369 | gen.indent_level -= len(handlers) 370 | gen.write_line('if not {}:'.format(var)) 371 | expected = '{} or {}'.format( 372 | ', '.join(map(str, self.all_handlers[:-1])), self.all_handlers[-1]) 373 | with gen.indent(): 374 | gen.fail(desc, expected, varname) 375 | 376 | def __str__(self) -> str: 377 | return 'Union[{}]'.format(', '.join(map(str, self.all__handlers))) 378 | 379 | @property 380 | def typevars(self) -> Set[type(TypeVar)]: 381 | return set(t for h in self.handlers for t in h.typevars) 382 | 383 | 384 | class TupleHandler(Handler, subclass=Tuple): 385 | def __init__(self, bound: Any) -> None: 386 | super().__init__(bound) 387 | params = bound.__tuple_params__ 388 | self.ellipsis = bound.__tuple_use_ellipsis__ 389 | if self.ellipsis: 390 | self.handler = Handler(params[0]) 391 | else: 392 | self.handlers = [Handler(p) for p in params] 393 | 394 | def __call__(self, gen: Codegen, varname: str, desc: Optional[str]) -> None: 395 | gen.check_type(varname, desc, tuple) 396 | if self.ellipsis: 397 | gen.enumerate_and_check(varname, desc, self.handler) 398 | else: 399 | n = len(self.handlers) 400 | var_n = gen.new_var() 401 | gen.write_line('{} = len({})'.format(var_n, varname)) 402 | gen.write_line('if {} != {}:'.format(var_n, n)) 403 | with gen.indent(): 404 | gen.fail(desc, 'tuple of length {}'.format(n), varname, 405 | got='tuple of length {{{}}}'.format(var_n)) 406 | for i, handler in enumerate(self.handlers): 407 | handler(gen, '{}[{}]'.format(varname, i), 408 | None if desc is None else 'item #{} of {}'.format(i, desc)) 409 | 410 | def __str__(self) -> str: 411 | if self.ellipsis: 412 | if self.handler.is_any: 413 | return 'tuple' 414 | return 'Tuple[{}, ...]'.format(self.handler) 415 | return 'Tuple[{}]'.format(', '.join(map(str, self.handlers))) 416 | 417 | @property 418 | def typevars(self) -> Set[type(TypeVar)]: 419 | if self.ellipsis: 420 | return self.handler.typevars 421 | return set(t for h in self.handlers for t in h.typevars) 422 | 423 | 424 | class SequenceHandler(SingleArgumentHandler, origin=Sequence): 425 | def __call__(self, gen: Codegen, varname: str, desc: Optional[str]) -> None: 426 | gen.check_attrs_cached(varname, desc, 'sequence', 'v_cache_seq', 427 | ['__iter__', '__getitem__', '__len__', '__contains__']) 428 | if not self.handler.is_any: 429 | gen.enumerate_and_check(varname, desc, self.handler) 430 | 431 | def __str__(self) -> str: 432 | if self.handler.is_any: 433 | return 'Sequence' 434 | return 'Sequence[{}]'.format(self.handler) 435 | 436 | 437 | class MutableSequenceHandler(SingleArgumentHandler, origin=MutableSequence): 438 | def __call__(self, gen: Codegen, varname: str, desc: Optional[str]) -> None: 439 | gen.check_attrs_cached(varname, desc, 'mutable sequence', 'v_cache_mut_seq', 440 | ['__iter__', '__getitem__', '__len__', '__contains__', 441 | '__setitem__', '__delitem__']) 442 | if not self.handler.is_any: 443 | gen.enumerate_and_check(varname, desc, self.handler) 444 | 445 | def __str__(self) -> str: 446 | if self.handler.is_any: 447 | return 'MutableSequence' 448 | return 'MutableSequence[{}]'.format(self.handler) 449 | 450 | 451 | class SetHandler(SingleArgumentHandler, origin=Set): 452 | def __call__(self, gen: Codegen, varname: str, desc: Optional[str]) -> None: 453 | gen.check_type(varname, desc, set) 454 | if not self.handler.is_any: 455 | gen.iter_and_check(varname, desc, self.handler) 456 | 457 | def __str__(self) -> str: 458 | if self.handler.is_any: 459 | return 'set' 460 | return 'Set[{}]'.format(self.handler) 461 | -------------------------------------------------------------------------------- /typo/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | 4 | def type_name(tp: type) -> str: 5 | if tp.__module__ in ('builtins', 'abc', 'typing'): 6 | return tp.__name__ 7 | return tp.__module__ + '.' + getattr(tp, '__qualname__', tp.__name__) 8 | --------------------------------------------------------------------------------