From 642ddb5015a4c5bdd5f3a399c088881285e2d2aa Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Thu, 28 Sep 2017 16:31:25 -0700 Subject: [PATCH] Add tests for stubs and address a few issues. --- conftest.py | 6 ++++ dev-requirements.txt | 1 + src/attr/__init__.pyi | 20 ++++++----- src/attr/exceptions.pyi | 3 +- src/attr/validators.pyi | 6 ++-- tests/test_stubs.py | 78 +++++++++++++++++++++++++++++++++++++++++ tests/test_stubs.test | 20 +++++++++++ 7 files changed, 119 insertions(+), 15 deletions(-) create mode 100644 tests/test_stubs.py create mode 100644 tests/test_stubs.test diff --git a/conftest.py b/conftest.py index 39ccbc406..91416773f 100644 --- a/conftest.py +++ b/conftest.py @@ -20,8 +20,14 @@ class C(object): collect_ignore = [] +pytest_plugins = [] + if sys.version_info[:2] < (3, 6): collect_ignore.extend([ "tests/test_annotations.py", "tests/test_init_subclass.py", ]) +if sys.version_info[:2] < (3, 5): + collect_ignore.append("tests/test_stubs.py") +else: + pytest_plugins.append('mypy.test.data') diff --git a/dev-requirements.txt b/dev-requirements.txt index ce39a4807..6a77a4533 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -4,3 +4,4 @@ zope.interface pympler hypothesis six +git+git://github.com/python/mypy.git#egg=mypy; python_version >= '3.5' \ No newline at end of file diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index d6825b9c0..85b72b17a 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -7,11 +7,13 @@ from . import validators # typing -- _T = TypeVar('_T') +T = TypeVar('T') _C = TypeVar('_C', bound=type) _M = TypeVar('_M', bound=Mapping) -_TT = TypeVar('_TT', bound=tuple) +_I = TypeVar('_I', bound=Iterable) ValidatorType = Callable[[object, 'Attribute', Any], Any] +# FIXME: Bind to attribute type? ConverterType = Callable[[Any], Any] FilterType = Callable[['Attribute', Any], bool] @@ -31,10 +33,10 @@ class Attribute: def __init__(self, name: str, default: Any, validator: Optional[Union[ValidatorType, List[ValidatorType]]], repr: bool, cmp: bool, hash: Optional[bool], init: bool, convert: Optional[ConverterType] = ..., metadata: Mapping = ..., type: Union[type, Factory] = ...) -> None: ... # NOTE: this overload for `attr` returns Any so that static analysis passes when used in the form: x : int = attr() -@overload -def attr(default: Any = ..., validator: Optional[Union[ValidatorType, List[ValidatorType]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[ConverterType] = ..., metadata: Mapping = ...) -> Any: ... -@overload -def attr(default: Any = ..., validator: Optional[Union[ValidatorType, List[ValidatorType]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[ConverterType] = ..., metadata: Mapping = ..., type: Union[Type[_T], Factory[_T]] = ...) -> _T: ... +# @overload +# def attr(default: Any = ..., validator: Optional[Union[ValidatorType, List[ValidatorType]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[ConverterType] = ..., metadata: Mapping = ...) -> Any: ... +# @overload +def attr(default: Any = ..., validator: Optional[Union[ValidatorType, List[ValidatorType]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[ConverterType] = ..., metadata: Mapping = ..., type: Type[T] = ...) -> T: ... @overload def attributes(maybe_cls: _C = ..., these: Optional[Dict[str, _CountingAttr]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> _C: ... @@ -46,13 +48,13 @@ def validate(inst: object) -> None: ... def make_class(name, attrs: Union[List[_CountingAttr], Dict[str, _CountingAttr]], bases: Tuple[type, ...] = ..., **attributes_arguments) -> type: ... -def and_(*validators: Iterable[ValidatorType]) -> ValidatorType: ... - # _funcs -- # FIXME: having problems assigning a default to the factory typevars -def asdict(inst: object, recurse: bool = ..., filter: Optional[FilterType] = ..., dict_factory: Callable[[], _M] = ..., retain_collection_types: bool = ...) -> _M: ... -def astuple(inst: object, recurse: bool = ..., filter: Optional[FilterType] = ..., tuple_factory: Callable[[Iterable], _TT] = ..., retain_collection_types: bool = ...) -> _TT: ... +# def asdict(inst: object, recurse: bool = ..., filter: Optional[FilterType] = ..., dict_factory: Type[_M] = dict, retain_collection_types: bool = ...) -> _M[str, Any]: ... +# def astuple(inst: object, recurse: bool = ..., filter: Optional[FilterType] = ..., tuple_factory: Type[_I] = tuple, retain_collection_types: bool = ...) -> _I: ... +def asdict(inst: object, recurse: bool = ..., filter: Optional[FilterType] = ..., dict_factory: Type[_M] = ..., retain_collection_types: bool = ...) -> _M: ... +def astuple(inst: object, recurse: bool = ..., filter: Optional[FilterType] = ..., tuple_factory: Type[_I] = ..., retain_collection_types: bool = ...) -> _I: ... def has(cls: type) -> bool: ... def assoc(inst: _T, **changes) -> _T: ... def evolve(inst: _T, **changes) -> _T: ... diff --git a/src/attr/exceptions.pyi b/src/attr/exceptions.pyi index f0edacbce..b86a1b46a 100644 --- a/src/attr/exceptions.pyi +++ b/src/attr/exceptions.pyi @@ -1,8 +1,7 @@ -from typing import List class FrozenInstanceError(AttributeError): msg : str = ... - args : List[str] = ... + args : tuple = ... class AttrsAttributeNotFoundError(ValueError): ... class NotAnAttrsClassError(ValueError): ... diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index ae328c819..71aa1d8d4 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -1,10 +1,8 @@ -from typing import Container, List, Union +from typing import Container, Iterable, List, Union from . import ValidatorType def instance_of(type: type) -> ValidatorType: ... - def provides(interface) -> ValidatorType: ... - def optional(validator: Union[ValidatorType, List[ValidatorType]]) -> ValidatorType: ... - def in_(options: Container) -> ValidatorType: ... +def and_(*validators: Iterable[ValidatorType]) -> ValidatorType: ... diff --git a/tests/test_stubs.py b/tests/test_stubs.py new file mode 100644 index 000000000..ec4c02e40 --- /dev/null +++ b/tests/test_stubs.py @@ -0,0 +1,78 @@ +# this file is adapted from mypy.test.testcmdline + +import os +import re +import subprocess +import sys + +from typing import Tuple, List, Dict, Set + +from mypy.test.data import parse_test_cases, DataDrivenTestCase, DataSuite +from mypy.test.helpers import (assert_string_arrays_equal, + normalize_error_messages) + +# Path to Python 3 interpreter +python3_path = sys.executable +test_temp_dir = 'tmp' +test_file = os.path.splitext(os.path.realpath(__file__))[0] + '.test' +prefix_dir = os.path.join(os.path.dirname(os.path.dirname(test_file)), 'src') + + +class PythonEvaluationSuite(DataSuite): + + @classmethod + def cases(cls) -> List[DataDrivenTestCase]: + return parse_test_cases(test_file, + _test_python_evaluation, + base_path=test_temp_dir, + optional_out=True, + native_sep=True) + + def run_case(self, testcase: DataDrivenTestCase): + _test_python_evaluation(testcase) + + +def _test_python_evaluation(testcase: DataDrivenTestCase) -> None: + assert testcase.old_cwd is not None, "test was not properly set up" + # Write the program to a file. + program = '_program.py' + program_path = os.path.join(test_temp_dir, program) + with open(program_path, 'w') as file: + for s in testcase.input: + file.write('{}\n'.format(s)) + args = parse_args(testcase.input[0]) + args.append('--show-traceback') + # Type check the program. + fixed = [python3_path, '-m', 'mypy'] + process = subprocess.Popen(fixed + args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env={'MYPYPATH': prefix_dir}, + cwd=test_temp_dir) + outb = process.stdout.read() + # Split output into lines. + out = [s.rstrip('\n\r') for s in str(outb, 'utf8').splitlines()] + # Remove temp file. + os.remove(program_path) + # Compare actual output to expected. + out = normalize_error_messages(out) + assert_string_arrays_equal(testcase.output, out, + 'Invalid output ({}, line {})'.format( + testcase.file, testcase.line)) + + +def parse_args(line: str) -> List[str]: + """Parse the first line of the program for the command line. + + This should have the form + + # cmd: mypy + + For example: + + # cmd: mypy pkg/ + """ + m = re.match('# cmd: mypy (.*)$', line) + if not m: + return [] # No args; mypy will spit out an error. + return m.group(1).split() diff --git a/tests/test_stubs.test b/tests/test_stubs.test new file mode 100644 index 000000000..7af9cfaa4 --- /dev/null +++ b/tests/test_stubs.test @@ -0,0 +1,20 @@ +[case test_type_annotations] +# cmd: mypy a.py +[file a.py] +import attr + +@attr.s +class C(object): + a : int = attr.ib() + b = attr.ib(type=int) + +c = C() +reveal_type(c.a) +reveal_type(c.b) +reveal_type(C.a) +reveal_type(C.b) +[out] +a.py:9: error: Revealed type is 'builtins.int' +a.py:10: error: Revealed type is 'builtins.int' +a.py:11: error: Revealed type is 'builtins.int' +a.py:12: error: Revealed type is 'builtins.int'