From e3d43a99db64c889f022ce5bb50ff80e1940d4ab Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sat, 26 Aug 2017 21:44:16 -0700 Subject: [PATCH 01/64] Add PEP484 stubs --- src/attr/__init__.pyi | 67 +++++++++++++++++++++++++++++++++++++++++ src/attr/converters.pyi | 3 ++ src/attr/exceptions.pyi | 9 ++++++ src/attr/filters.pyi | 5 +++ src/attr/validators.pyi | 10 ++++++ 5 files changed, 94 insertions(+) create mode 100644 src/attr/__init__.pyi create mode 100644 src/attr/converters.pyi create mode 100644 src/attr/exceptions.pyi create mode 100644 src/attr/filters.pyi create mode 100644 src/attr/validators.pyi diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi new file mode 100644 index 000000000..719664966 --- /dev/null +++ b/src/attr/__init__.pyi @@ -0,0 +1,67 @@ +from typing import Any, Callable, Dict, Iterable, List, Optional, Mapping, Tuple, Type, TypeVar, Union, overload +from . import exceptions +from . import filters +from . import converters +from . import validators + +# typing -- + +C = TypeVar('C', bound=type) +M = TypeVar('M', bound=Mapping) +T = TypeVar('T', bound=tuple) +I = TypeVar('I') + +ValidatorType = Callable[[object, 'Attribute', Any], Any] +ConverterType = Callable[[Any], Any] +FilterType = Callable[['Attribute', Any], bool] + +# _make -- + +class _CountingAttr: ... + +NOTHING : object + +class Attribute: + __slots__ = ( + "name", "default", "validator", "repr", "cmp", "hash", "init", + "convert", "metadata", + ) + 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 = ...) -> None: ... + +# NOTE: the stub for `attr` returns Any so that static analysis passes when used in the form: x : int = attr() +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 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: ... +@overload +def attributes(maybe_cls: None = ..., these: Optional[Dict[str, _CountingAttr]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> Callable[[C], C]: ... + +def fields(cls: type) -> Tuple[Attribute, ...]: ... +def validate(inst: object) -> None: ... + +class Factory: + factory : Union[Callable[[Any], Any], Callable[[object, Any], Any]] + takes_self : bool + def __init__(self, factory: Union[Callable[[Any], Any], Callable[[object, Any], Any]], takes_self: bool = ...) -> 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], T] = ..., retain_collection_types: bool = ...) -> T: ... +def has(cls: type) -> bool: ... +def assoc(inst: I, **changes) -> I: ... +def evolve(inst: I, **changes) -> I: ... + +# _config -- + +def set_run_validators(run: bool) -> None: ... +def get_run_validators() -> bool: ... + +# aliases +s = attrs = attributes +ib = attrib = attr diff --git a/src/attr/converters.pyi b/src/attr/converters.pyi new file mode 100644 index 000000000..1a52f42f9 --- /dev/null +++ b/src/attr/converters.pyi @@ -0,0 +1,3 @@ +from . import ConverterType + +def optional(converter: ConverterType) -> ConverterType: ... diff --git a/src/attr/exceptions.pyi b/src/attr/exceptions.pyi new file mode 100644 index 000000000..f0edacbce --- /dev/null +++ b/src/attr/exceptions.pyi @@ -0,0 +1,9 @@ +from typing import List + +class FrozenInstanceError(AttributeError): + msg : str = ... + args : List[str] = ... + +class AttrsAttributeNotFoundError(ValueError): ... +class NotAnAttrsClassError(ValueError): ... +class DefaultAlreadySetError(RuntimeError): ... diff --git a/src/attr/filters.pyi b/src/attr/filters.pyi new file mode 100644 index 000000000..9865767a8 --- /dev/null +++ b/src/attr/filters.pyi @@ -0,0 +1,5 @@ +from typing import Union +from . import Attribute, FilterType + +def include(*what: Union[type, Attribute]) -> FilterType: ... +def exclude(*what: Union[type, Attribute]) -> FilterType: ... diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi new file mode 100644 index 000000000..ae328c819 --- /dev/null +++ b/src/attr/validators.pyi @@ -0,0 +1,10 @@ +from typing import Container, 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: ... From a725e3754ef49cc64aa5eac3249d450184947300 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Wed, 30 Aug 2017 09:16:33 -0700 Subject: [PATCH 02/64] Deploy .pyi stubs alongside .py files. This is the recommended approach for 3rd party stubs. See: https://github.com/python/typing/issues/84#issuecomment-317217346 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 76aae5060..add2b4699 100644 --- a/setup.py +++ b/setup.py @@ -89,6 +89,7 @@ def find_meta(meta): long_description=LONG, packages=PACKAGES, package_dir={"": "src"}, + package_data={'attr': ['*.pyi']}, zip_safe=False, classifiers=CLASSIFIERS, install_requires=INSTALL_REQUIRES, From c60d2f1071042a8eaa659cb988bd6c728f93dd0f Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Mon, 25 Sep 2017 11:01:49 -0700 Subject: [PATCH 03/64] Add support for the new type argument. --- src/attr/__init__.pyi | 44 +++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 719664966..d6825b9c0 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -1,4 +1,4 @@ -from typing import Any, Callable, Dict, Iterable, List, Optional, Mapping, Tuple, Type, TypeVar, Union, overload +from typing import Any, Callable, Dict, Generic, Iterable, List, Optional, Mapping, Tuple, Type, TypeVar, Union, overload from . import exceptions from . import filters from . import converters @@ -6,10 +6,10 @@ from . import validators # typing -- -C = TypeVar('C', bound=type) -M = TypeVar('M', bound=Mapping) -T = TypeVar('T', bound=tuple) -I = TypeVar('I') +_T = TypeVar('_T') +_C = TypeVar('_C', bound=type) +_M = TypeVar('_M', bound=Mapping) +_TT = TypeVar('_TT', bound=tuple) ValidatorType = Callable[[object, 'Attribute', Any], Any] ConverterType = Callable[[Any], Any] @@ -21,29 +21,29 @@ class _CountingAttr: ... NOTHING : object +class Factory(Generic[_T]): + factory : Union[Callable[[], _T], Callable[[object], _T]] + takes_self : bool + def __init__(self, factory: Union[Callable[[], _T], Callable[[object], _T]], takes_self: bool = ...) -> None: ... + class Attribute: - __slots__ = ( - "name", "default", "validator", "repr", "cmp", "hash", "init", - "convert", "metadata", - ) - 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 = ...) -> None: ... + __slots__ = ("name", "default", "validator", "repr", "cmp", "hash", "init", "convert", "metadata", "type") + 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: the stub for `attr` returns Any so that static analysis passes when used in the form: x : int = attr() +# 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 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: ... +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: ... @overload -def attributes(maybe_cls: None = ..., these: Optional[Dict[str, _CountingAttr]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> Callable[[C], C]: ... +def attributes(maybe_cls: None = ..., these: Optional[Dict[str, _CountingAttr]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... def fields(cls: type) -> Tuple[Attribute, ...]: ... def validate(inst: object) -> None: ... -class Factory: - factory : Union[Callable[[Any], Any], Callable[[object, Any], Any]] - takes_self : bool - def __init__(self, factory: Union[Callable[[Any], Any], Callable[[object, Any], Any]], takes_self: bool = ...) -> None: ... - def make_class(name, attrs: Union[List[_CountingAttr], Dict[str, _CountingAttr]], bases: Tuple[type, ...] = ..., **attributes_arguments) -> type: ... def and_(*validators: Iterable[ValidatorType]) -> ValidatorType: ... @@ -51,11 +51,11 @@ 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], T] = ..., retain_collection_types: bool = ...) -> T: ... +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 has(cls: type) -> bool: ... -def assoc(inst: I, **changes) -> I: ... -def evolve(inst: I, **changes) -> I: ... +def assoc(inst: _T, **changes) -> _T: ... +def evolve(inst: _T, **changes) -> _T: ... # _config -- From 642ddb5015a4c5bdd5f3a399c088881285e2d2aa Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Thu, 28 Sep 2017 16:31:25 -0700 Subject: [PATCH 04/64] 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' From 0df5b9a2e3eb5e6734f966beb358834567233c32 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Fri, 3 Nov 2017 12:22:45 -0700 Subject: [PATCH 05/64] Improve declaration of private vs public objects in stubs --- src/attr/__init__.pyi | 40 ++++++++++++++++++++-------------------- src/attr/converters.pyi | 4 ++-- src/attr/filters.pyi | 6 +++--- src/attr/validators.pyi | 12 ++++++------ 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 85b72b17a..44ab25d0c 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -1,21 +1,21 @@ from typing import Any, Callable, Dict, Generic, Iterable, List, Optional, Mapping, Tuple, Type, TypeVar, Union, overload -from . import exceptions -from . import filters -from . import converters -from . import validators +# `import X as X` is required to expose these to mypy. otherwise they are invisible +from . import exceptions as exceptions +from . import filters as filters +from . import converters as converters +from . import validators as validators # typing -- _T = TypeVar('_T') -T = TypeVar('T') _C = TypeVar('_C', bound=type) _M = TypeVar('_M', bound=Mapping) _I = TypeVar('_I', bound=Iterable) -ValidatorType = Callable[[object, 'Attribute', Any], Any] +_ValidatorType = Callable[[Any, 'Attribute', Any], Any] # FIXME: Bind to attribute type? -ConverterType = Callable[[Any], Any] -FilterType = Callable[['Attribute', Any], bool] +_ConverterType = Callable[[Any], Any] +_FilterType = Callable[['Attribute', Any], bool] # _make -- @@ -24,19 +24,19 @@ class _CountingAttr: ... NOTHING : object class Factory(Generic[_T]): - factory : Union[Callable[[], _T], Callable[[object], _T]] + factory : Union[Callable[[], _T], Callable[[Any], _T]] takes_self : bool - def __init__(self, factory: Union[Callable[[], _T], Callable[[object], _T]], takes_self: bool = ...) -> None: ... + def __init__(self, factory: Union[Callable[[], _T], Callable[[Any], _T]], takes_self: bool = ...) -> None: ... class Attribute: __slots__ = ("name", "default", "validator", "repr", "cmp", "hash", "init", "convert", "metadata", "type") - 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: ... + def __init__(self, name: str, default: Any, validator: Optional[Union[_ValidatorType, List[_ValidatorType], Tuple[_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: Type[T] = ...) -> T: ... +@overload +def attr(default: Any = ..., validator: Optional[Union[_ValidatorType, List[_ValidatorType], Tuple[_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], Tuple[_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: ... @@ -44,17 +44,17 @@ def attributes(maybe_cls: _C = ..., these: Optional[Dict[str, _CountingAttr]] = def attributes(maybe_cls: None = ..., these: Optional[Dict[str, _CountingAttr]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... def fields(cls: type) -> Tuple[Attribute, ...]: ... -def validate(inst: object) -> None: ... +def validate(inst: Any) -> None: ... def make_class(name, attrs: Union[List[_CountingAttr], Dict[str, _CountingAttr]], bases: Tuple[type, ...] = ..., **attributes_arguments) -> type: ... # _funcs -- # FIXME: having problems assigning a default to the factory typevars -# 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 asdict(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[_M] = dict, retain_collection_types: bool = ...) -> _M[str, Any]: ... +# def astuple(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[_I] = tuple, retain_collection_types: bool = ...) -> _I: ... +def asdict(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[_M] = ..., retain_collection_types: bool = ...) -> _M: ... +def astuple(inst: Any, 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/converters.pyi b/src/attr/converters.pyi index 1a52f42f9..0629940bd 100644 --- a/src/attr/converters.pyi +++ b/src/attr/converters.pyi @@ -1,3 +1,3 @@ -from . import ConverterType +from . import _ConverterType -def optional(converter: ConverterType) -> ConverterType: ... +def optional(converter: _ConverterType) -> _ConverterType: ... diff --git a/src/attr/filters.pyi b/src/attr/filters.pyi index 9865767a8..a618140c2 100644 --- a/src/attr/filters.pyi +++ b/src/attr/filters.pyi @@ -1,5 +1,5 @@ from typing import Union -from . import Attribute, FilterType +from . import Attribute, _FilterType -def include(*what: Union[type, Attribute]) -> FilterType: ... -def exclude(*what: Union[type, Attribute]) -> FilterType: ... +def include(*what: Union[type, Attribute]) -> _FilterType: ... +def exclude(*what: Union[type, Attribute]) -> _FilterType: ... diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index 71aa1d8d4..95cd2105b 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -1,8 +1,8 @@ from typing import Container, Iterable, List, Union -from . import ValidatorType +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: ... +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: ... From d0b9253075ace71b9be326b92932910b7666aedc Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Fri, 3 Nov 2017 12:23:06 -0700 Subject: [PATCH 06/64] More stub tests --- tests/test_stubs.py | 12 +++++------- tests/test_stubs.test | 43 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/tests/test_stubs.py b/tests/test_stubs.py index ec4c02e40..970d8c957 100644 --- a/tests/test_stubs.py +++ b/tests/test_stubs.py @@ -5,9 +5,7 @@ import subprocess import sys -from typing import Tuple, List, Dict, Set - -from mypy.test.data import parse_test_cases, DataDrivenTestCase, DataSuite +from mypy.test.data import parse_test_cases, DataSuite from mypy.test.helpers import (assert_string_arrays_equal, normalize_error_messages) @@ -21,18 +19,18 @@ class PythonEvaluationSuite(DataSuite): @classmethod - def cases(cls) -> List[DataDrivenTestCase]: + def cases(cls): 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): + def run_case(self, testcase): _test_python_evaluation(testcase) -def _test_python_evaluation(testcase: DataDrivenTestCase) -> None: +def _test_python_evaluation(testcase): assert testcase.old_cwd is not None, "test was not properly set up" # Write the program to a file. program = '_program.py' @@ -61,7 +59,7 @@ def _test_python_evaluation(testcase: DataDrivenTestCase) -> None: testcase.file, testcase.line)) -def parse_args(line: str) -> List[str]: +def parse_args(line): """Parse the first line of the program for the command line. This should have the form diff --git a/tests/test_stubs.test b/tests/test_stubs.test index 7af9cfaa4..027503cee 100644 --- a/tests/test_stubs.test +++ b/tests/test_stubs.test @@ -1,4 +1,4 @@ -[case test_type_annotations] +[case test_type_annotations_pep526] # cmd: mypy a.py [file a.py] import attr @@ -6,15 +6,44 @@ 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:8: error: Revealed type is 'builtins.int' 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' + + +[case test_type_annotations_arg] +# cmd: mypy a.py +[file a.py] +import attr + +@attr.s +class C(object): + a = attr.ib(type=int) + +c = C() +reveal_type(c.a) +reveal_type(C.a) +[out] +a.py:8: error: Revealed type is 'builtins.int*' +a.py:9: error: Revealed type is 'builtins.int*' + + +[case test_type_annotations_missing] +# cmd: mypy a.py +[file a.py] +import attr + +@attr.s +class C(object): + a = attr.ib() + +c = C() +reveal_type(c.a) +reveal_type(C.a) +[out] +a.py:8: error: Revealed type is 'Any' +a.py:9: error: Revealed type is 'Any' From 9030de3c1d29cbdb70c75e606ede9296cdb93267 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Fri, 3 Nov 2017 18:01:54 -0700 Subject: [PATCH 07/64] Separate the stub tests into their own tox env it does not make sense to test the stubs in multiple python *runtime* environments (e.g. python 3.5, 3.6, pypy3) because the results of static analysis wrt attrs is not dependent on the runtime. Moreover, mypy is not installing correctly in pypy3 which has nothing to do with attrs. --- conftest.py | 7 ++----- tests/test_stubs.py | 2 ++ tox.ini | 9 ++++++++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/conftest.py b/conftest.py index 91416773f..1fe560fa9 100644 --- a/conftest.py +++ b/conftest.py @@ -20,14 +20,11 @@ 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') + +collect_ignore.append("tests/test_stubs.py") diff --git a/tests/test_stubs.py b/tests/test_stubs.py index 970d8c957..5b7f237cc 100644 --- a/tests/test_stubs.py +++ b/tests/test_stubs.py @@ -9,6 +9,8 @@ from mypy.test.helpers import (assert_string_arrays_equal, normalize_error_messages) +pytest_plugins = ['mypy.test.data'] + # Path to Python 3 interpreter python3_path = sys.executable test_temp_dir = 'tmp' diff --git a/tox.ini b/tox.ini index 567a7859d..5f10f30be 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py27,py34,py35,py36,pypy,pypy3,flake8,manifest,docs,readme,changelog,coverage-report +envlist = py27,py34,py35,py36,pypy,pypy3,flake8,manifest,docs,readme,changelog,coverage-report,stubs [testenv] @@ -66,3 +66,10 @@ skip_install = true commands = coverage combine coverage report + +[testenv:stubs] +basepython = python3.6 +deps = + pytest + mypy +commands = pytest tests/test_stubs.py From cedf2c8213642710060a2df3a18fb91094cda144 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Fri, 3 Nov 2017 18:02:04 -0700 Subject: [PATCH 08/64] Update the manifest with stub files --- MANIFEST.in | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MANIFEST.in b/MANIFEST.in index 03a948ef2..7d8aa21b0 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,6 +3,10 @@ include LICENSE *.rst *.toml # Don't package GitHub-specific files. exclude *.md .travis.yml +# Stubs +recursive-include src *.pyi +recursive-include tests *.test + # Tests include tox.ini .coveragerc conftest.py dev-requirements.txt docs-requirements.txt recursive-include tests *.py From a822cee4d1a32606d8a3041b8f0057f7981e060e Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Fri, 3 Nov 2017 18:13:54 -0700 Subject: [PATCH 09/64] Remove mypy from the dev requirements --- dev-requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dev-requirements.txt b/dev-requirements.txt index 6a77a4533..388e9ef58 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -3,5 +3,4 @@ pytest zope.interface pympler hypothesis -six -git+git://github.com/python/mypy.git#egg=mypy; python_version >= '3.5' \ No newline at end of file +six \ No newline at end of file From d58379eea727de99de160b2ef5f1aebd5ef1a42d Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sat, 4 Nov 2017 13:49:30 -0700 Subject: [PATCH 10/64] Allow _CountingAttr to be instantiated, but not Attribute. --- src/attr/__init__.pyi | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 44ab25d0c..90a91086d 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -19,7 +19,8 @@ _FilterType = Callable[['Attribute', Any], bool] # _make -- -class _CountingAttr: ... +class _CountingAttr: + def __init__(self, default: Any = ..., validator: Optional[Union[_ValidatorType, List[_ValidatorType], Tuple[_ValidatorType, ...]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[_ConverterType] = ..., metadata: Mapping = ..., type: type = ...) -> None: ... NOTHING : object @@ -30,7 +31,16 @@ class Factory(Generic[_T]): class Attribute: __slots__ = ("name", "default", "validator", "repr", "cmp", "hash", "init", "convert", "metadata", "type") - def __init__(self, name: str, default: Any, validator: Optional[Union[_ValidatorType, List[_ValidatorType], Tuple[_ValidatorType, ...]]], repr: bool, cmp: bool, hash: Optional[bool], init: bool, convert: Optional[_ConverterType] = ..., metadata: Mapping = ..., type: Union[type, Factory] = ...) -> None: ... + name: str + default: Any + validator: Optional[Union[_ValidatorType, List[_ValidatorType], Tuple[_ValidatorType, ...]]] + repr: bool + cmp: bool + hash: Optional[bool] + init: bool + convert: Optional[_ConverterType] + metadata: Mapping + type: Any # NOTE: this overload for `attr` returns Any so that static analysis passes when used in the form: x : int = attr() @overload From ec7d29c37ed9628e73e59ca49de9f8d7ea9032a7 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sat, 4 Nov 2017 13:50:23 -0700 Subject: [PATCH 11/64] Incorporate defaults into attr.ib typing --- src/attr/__init__.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 90a91086d..be91484d0 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -44,9 +44,9 @@ class Attribute: # 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], Tuple[_ValidatorType, ...]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[_ConverterType] = ..., metadata: Mapping = ...) -> Any: ... +def attr(default: Union[_T, Factory[_T]] = ..., validator: Optional[Union[_ValidatorType, List[_ValidatorType], Tuple[_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], Tuple[_ValidatorType, ...]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[_ConverterType] = ..., metadata: Mapping = ..., type: Type[_T] = ...) -> _T: ... +def attr(default: Union[_T, Factory[_T]] = ..., validator: Optional[Union[_ValidatorType, List[_ValidatorType], Tuple[_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: ... From 8cb8000a9e5e1c70da87814f5b1f33718319026b Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sat, 4 Nov 2017 13:50:48 -0700 Subject: [PATCH 12/64] Fix a bug with validators.and_ --- src/attr/validators.pyi | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index 95cd2105b..a7bf3d1ff 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -1,8 +1,8 @@ -from typing import Container, Iterable, List, Union +from typing import Container, 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: ... +def and_(*validators: _ValidatorType) -> _ValidatorType: ... From ebd6c175eb045e0cb2421c69fa7c35a754793593 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sat, 4 Nov 2017 13:51:02 -0700 Subject: [PATCH 13/64] Add more tests --- tests/test_stubs.test | 61 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/tests/test_stubs.test b/tests/test_stubs.test index 027503cee..0b6d954e9 100644 --- a/tests/test_stubs.test +++ b/tests/test_stubs.test @@ -32,6 +32,51 @@ a.py:8: error: Revealed type is 'builtins.int*' a.py:9: error: Revealed type is 'builtins.int*' +[case test_defaults] +# cmd: mypy a.py +[file a.py] +import attr +from typing import List + +def int_factory() -> int: + return 0 + +a = attr.ib(type=int) +reveal_type(a) # int + +b = attr.ib(default=0, type=int) +reveal_type(b) # int + +c = attr.ib(default=attr.Factory(int_factory), type=int) +reveal_type(c) # int + +d = attr.ib(default=0) +reveal_type(d) # Any. ideally this would be int. not sure why it's not working. + +e: int = attr.ib(default=0) +reveal_type(e) # int + +f = attr.ib(default='bad', type=int) +reveal_type(f) # object, the common base of str and int + +g: int = attr.ib(default='bad', type=int) # as above, but results in assignment error: object <> int + +h: List[int] = attr.ib(default=attr.Factory(list)) +reveal_type(h) + +i: List[int] = attr.Factory(list) + +[out] +a.py:8: error: Revealed type is 'builtins.int*' +a.py:11: error: Revealed type is 'builtins.int*' +a.py:14: error: Revealed type is 'builtins.int*' +a.py:17: error: Revealed type is 'Any' +a.py:20: error: Revealed type is 'builtins.int' +a.py:23: error: Revealed type is 'builtins.object*' +a.py:25: error: Incompatible types in assignment (expression has type "object", variable has type "int") +a.py:28: error: Revealed type is 'builtins.list[builtins.int]' +a.py:30: error: Incompatible types in assignment (expression has type "Factory[List[_T]]", variable has type "List[int]") + [case test_type_annotations_missing] # cmd: mypy a.py [file a.py] @@ -47,3 +92,19 @@ reveal_type(C.a) [out] a.py:8: error: Revealed type is 'Any' a.py:9: error: Revealed type is 'Any' + + +[case test_validators] +# cmd: mypy a.py +[file a.py] +import attr +from attr.validators import in_, and_, instance_of + +a = attr.ib(type=int, validator=in_([1, 2, 3])) +b = attr.ib(type=int, validator=[in_([1, 2, 3]), instance_of(int)]) +c = attr.ib(type=int, validator=(in_([1, 2, 3]), instance_of(int))) +d = attr.ib(type=int, validator=and_(in_([1, 2, 3]), instance_of(int))) +e = attr.ib(type=int, validator=1) + +[out] +a.py:8: error: No overload variant matches argument types [Overload(def (x: Union[builtins.str, builtins.bytes, typing.SupportsInt] =) -> builtins.int, def (x: Union[builtins.str, builtins.bytes], base: builtins.int) -> builtins.int), builtins.int] From 19857ecc7f356b8fc396d20c323183c23051db94 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Tue, 7 Nov 2017 10:41:06 -0800 Subject: [PATCH 14/64] Remove _CountingAttr from public interface It is crucial to ensure that make_class() works with attr.ib(), as a result we no longer have any functions that care about _CountingAttr. --- src/attr/__init__.pyi | 9 +++------ tests/test_stubs.test | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index be91484d0..bc9b0c080 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -19,9 +19,6 @@ _FilterType = Callable[['Attribute', Any], bool] # _make -- -class _CountingAttr: - def __init__(self, default: Any = ..., validator: Optional[Union[_ValidatorType, List[_ValidatorType], Tuple[_ValidatorType, ...]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[_ConverterType] = ..., metadata: Mapping = ..., type: type = ...) -> None: ... - NOTHING : object class Factory(Generic[_T]): @@ -49,14 +46,14 @@ def attr(default: Union[_T, Factory[_T]] = ..., validator: Optional[Union[_Valid def attr(default: Union[_T, Factory[_T]] = ..., validator: Optional[Union[_ValidatorType, List[_ValidatorType], Tuple[_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: ... +def attributes(maybe_cls: _C = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> _C: ... @overload -def attributes(maybe_cls: None = ..., these: Optional[Dict[str, _CountingAttr]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... +def attributes(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... def fields(cls: type) -> Tuple[Attribute, ...]: ... def validate(inst: Any) -> None: ... -def make_class(name, attrs: Union[List[_CountingAttr], Dict[str, _CountingAttr]], bases: Tuple[type, ...] = ..., **attributes_arguments) -> type: ... +def make_class(name, attrs: Union[List[Any], Dict[str, Any]], bases: Tuple[type, ...] = ..., **attributes_arguments) -> type: ... # _funcs -- diff --git a/tests/test_stubs.test b/tests/test_stubs.test index 0b6d954e9..059d71b2f 100644 --- a/tests/test_stubs.test +++ b/tests/test_stubs.test @@ -108,3 +108,25 @@ e = attr.ib(type=int, validator=1) [out] a.py:8: error: No overload variant matches argument types [Overload(def (x: Union[builtins.str, builtins.bytes, typing.SupportsInt] =) -> builtins.int, def (x: Union[builtins.str, builtins.bytes], base: builtins.int) -> builtins.int), builtins.int] + + +[case test_make_from_dict] +# cmd: mypy a.py +[file a.py] +import attr +C = attr.make_class("C", { + "x": attr.Attr(type=int), + "y": attr.Attr() +}) +[out] + + +[case test_make_from_attrib] +# cmd: mypy a.py +[file a.py] +import attr +C = attr.make_class("C", [ + attr.ib(type=int), + attr.ib() +]) +[out] From 7b953d23004b795defe6b2ba3b2936debd8bd8d1 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Wed, 8 Nov 2017 10:07:41 -0800 Subject: [PATCH 15/64] Lie about return type of Factory this allows for an abbreviated idiom: `x: List[int] = Factory(list)` --- src/attr/__init__.pyi | 19 ++--- tests/test_stubs.test | 176 +++++++++++++++++++++++++++++++----------- 2 files changed, 139 insertions(+), 56 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index bc9b0c080..adfd17e84 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -13,7 +13,6 @@ _M = TypeVar('_M', bound=Mapping) _I = TypeVar('_I', bound=Iterable) _ValidatorType = Callable[[Any, 'Attribute', Any], Any] -# FIXME: Bind to attribute type? _ConverterType = Callable[[Any], Any] _FilterType = Callable[['Attribute', Any], bool] @@ -21,10 +20,8 @@ _FilterType = Callable[['Attribute', Any], bool] NOTHING : object -class Factory(Generic[_T]): - factory : Union[Callable[[], _T], Callable[[Any], _T]] - takes_self : bool - def __init__(self, factory: Union[Callable[[], _T], Callable[[Any], _T]], takes_self: bool = ...) -> None: ... +# Factory lies about its return type to make this possible: `x: List[int] = Factory(list)` +def Factory(factory: Union[Callable[[], _T], Callable[[Any], _T]], takes_self: bool = ...) -> _T: ... class Attribute: __slots__ = ("name", "default", "validator", "repr", "cmp", "hash", "init", "convert", "metadata", "type") @@ -37,13 +34,17 @@ class Attribute: init: bool convert: Optional[_ConverterType] metadata: Mapping - type: Any + type: Optional[Any] -# NOTE: this overload for `attr` returns Any so that static analysis passes when used in the form: x : int = attr() + +# order here matters: if default is provided but not type, we want the first overload chosen so that the type is based on default @overload -def attr(default: Union[_T, Factory[_T]] = ..., validator: Optional[Union[_ValidatorType, List[_ValidatorType], Tuple[_ValidatorType, ...]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[_ConverterType] = ..., metadata: Mapping = ...) -> Any: ... +def attr(default: _T = ..., validator: Optional[Union[_ValidatorType, List[_ValidatorType], Tuple[_ValidatorType, ...]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[_ConverterType] = ..., metadata: Mapping = ..., type: Type[_T] = ...) -> _T: ... +# NOTE: this overload for `attr` returns Any so that static analysis passes when used in the form: x : int = attr() @overload -def attr(default: Union[_T, Factory[_T]] = ..., validator: Optional[Union[_ValidatorType, List[_ValidatorType], Tuple[_ValidatorType, ...]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[_ConverterType] = ..., metadata: Mapping = ..., type: Type[_T] = ...) -> _T: ... +def attr(default: _T = ..., validator: Optional[Union[_ValidatorType, List[_ValidatorType], Tuple[_ValidatorType, ...]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[_ConverterType] = ..., metadata: Mapping = ...) -> Any: ... + +# we use Any instead of _CountingAttr so that e.g. `make_class('Foo', [attr.ib()])` is valid @overload def attributes(maybe_cls: _C = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> _C: ... diff --git a/tests/test_stubs.test b/tests/test_stubs.test index 059d71b2f..11126b910 100644 --- a/tests/test_stubs.test +++ b/tests/test_stubs.test @@ -1,21 +1,27 @@ -[case test_type_annotations_pep526] +# --------------------------- +# Basics +# --------------------------- + +[case test_no_type] # cmd: mypy a.py [file a.py] import attr @attr.s class C(object): - a : int = attr.ib() + a = attr.ib() # error: need annotation c = C() -reveal_type(c.a) -reveal_type(C.a) +reveal_type(c.a) # Any +reveal_type(C.a) # Any [out] -a.py:8: error: Revealed type is 'builtins.int' -a.py:9: error: Revealed type is 'builtins.int' +a.py:5: error: Need type annotation for variable +a.py:8: error: Revealed type is 'Any' +a.py:9: error: Revealed type is 'Any' +a.py:9: error: Cannot determine type of 'a' -[case test_type_annotations_arg] +[case test_type_arg] # cmd: mypy a.py [file a.py] import attr @@ -25,21 +31,49 @@ class C(object): a = attr.ib(type=int) c = C() -reveal_type(c.a) -reveal_type(C.a) +reveal_type(c.a) # int +reveal_type(C.a) # int [out] a.py:8: error: Revealed type is 'builtins.int*' a.py:9: error: Revealed type is 'builtins.int*' -[case test_defaults] +[case test_type_annotations] # cmd: mypy a.py [file a.py] import attr -from typing import List -def int_factory() -> int: - return 0 +@attr.s +class C(object): + a : int = attr.ib() + +c = C() +reveal_type(c.a) # int +reveal_type(C.a) # int +[out] +a.py:8: error: Revealed type is 'builtins.int' +a.py:9: error: Revealed type is 'builtins.int' + +# --------------------------- +# Defaults +# --------------------------- + +[case test_defaults_no_type] +# cmd: mypy a.py +[file a.py] +import attr + +a = attr.ib(default=0) +reveal_type(a) # int + +[out] +a.py:4: error: Revealed type is 'builtins.int*' + + +[case test_defaults_type_arg] +# cmd: mypy a.py +[file a.py] +import attr a = attr.ib(type=int) reveal_type(a) # int @@ -47,53 +81,97 @@ reveal_type(a) # int b = attr.ib(default=0, type=int) reveal_type(b) # int -c = attr.ib(default=attr.Factory(int_factory), type=int) -reveal_type(c) # int +c = attr.ib(default='bad', type=int) +reveal_type(c) # object, the common base of str and int + +[out] +a.py:4: error: Revealed type is 'builtins.int*' +a.py:7: error: Revealed type is 'builtins.int*' +a.py:10: error: Revealed type is 'builtins.object*' + + +[case test_defaults_type_annotations] +# cmd: mypy a.py +[file a.py] +import attr + +a: int = attr.ib() +reveal_type(a) # int + +b: int = attr.ib(default=0) +reveal_type(b) # int + +c: int = attr.ib(default=0, type=str) # error: object <> int + +[out] +a.py:4: error: Revealed type is 'builtins.int' +a.py:7: error: Revealed type is 'builtins.int' +a.py:9: error: Incompatible types in assignment (expression has type "object", variable has type "int") -d = attr.ib(default=0) -reveal_type(d) # Any. ideally this would be int. not sure why it's not working. -e: int = attr.ib(default=0) -reveal_type(e) # int +# --------------------------- +# Factory Defaults +# --------------------------- -f = attr.ib(default='bad', type=int) -reveal_type(f) # object, the common base of str and int +[case test_factory_defaults_type_arg] +# cmd: mypy a.py +[file a.py] +import attr +from typing import List -g: int = attr.ib(default='bad', type=int) # as above, but results in assignment error: object <> int +a = attr.ib(type=List[int]) +reveal_type(a) # List[int] -h: List[int] = attr.ib(default=attr.Factory(list)) -reveal_type(h) +b = attr.ib(default=attr.Factory(list), type=List[int]) +reveal_type(b) # object: FIXME: shouldn't the `default` type be upgraded from `list` to `List[int]``? make issue on mypy about this + +c = attr.ib(default=attr.Factory(list), type=int) +reveal_type(c) # object, the common base of list and int + +def int_factory() -> int: + return 0 -i: List[int] = attr.Factory(list) +d = attr.ib(default=attr.Factory(int_factory), type=int) +reveal_type(d) # int [out] -a.py:8: error: Revealed type is 'builtins.int*' -a.py:11: error: Revealed type is 'builtins.int*' -a.py:14: error: Revealed type is 'builtins.int*' -a.py:17: error: Revealed type is 'Any' -a.py:20: error: Revealed type is 'builtins.int' -a.py:23: error: Revealed type is 'builtins.object*' -a.py:25: error: Incompatible types in assignment (expression has type "object", variable has type "int") -a.py:28: error: Revealed type is 'builtins.list[builtins.int]' -a.py:30: error: Incompatible types in assignment (expression has type "Factory[List[_T]]", variable has type "List[int]") - -[case test_type_annotations_missing] +a.py:5: error: Revealed type is 'builtins.list*[builtins.int*]' +a.py:8: error: Revealed type is 'builtins.object*' +a.py:11: error: Revealed type is 'builtins.object*' +a.py:17: error: Revealed type is 'builtins.int*' + + +[case test_factory_defaults_type_annotations] # cmd: mypy a.py [file a.py] import attr +from typing import List -@attr.s -class C(object): - a = attr.ib() +a: List[int] = attr.ib() +reveal_type(a) # List[int] + +b: List[int] = attr.ib(default=attr.Factory(list), type=List[int]) +reveal_type(b) # List[int] + +c: List[int] = attr.ib(default=attr.Factory(list), type=str) # error: str <> List[int] + +def int_factory() -> int: + return 0 + +d: int = attr.ib(default=attr.Factory(int_factory)) +reveal_type(d) # int -c = C() -reveal_type(c.a) -reveal_type(C.a) [out] -a.py:8: error: Revealed type is 'Any' -a.py:9: error: Revealed type is 'Any' +a.py:5: error: Revealed type is 'builtins.list[builtins.int]' +a.py:8: error: Revealed type is 'builtins.list[builtins.int]' +a.py:10: error: Argument 2 has incompatible type "Type[str]"; expected "Type[List[int]]" +a.py:16: error: Revealed type is 'builtins.int' +# --------------------------- +# Validators +# --------------------------- + [case test_validators] # cmd: mypy a.py [file a.py] @@ -104,19 +182,23 @@ a = attr.ib(type=int, validator=in_([1, 2, 3])) b = attr.ib(type=int, validator=[in_([1, 2, 3]), instance_of(int)]) c = attr.ib(type=int, validator=(in_([1, 2, 3]), instance_of(int))) d = attr.ib(type=int, validator=and_(in_([1, 2, 3]), instance_of(int))) -e = attr.ib(type=int, validator=1) +e = attr.ib(type=int, validator=1) # error [out] a.py:8: error: No overload variant matches argument types [Overload(def (x: Union[builtins.str, builtins.bytes, typing.SupportsInt] =) -> builtins.int, def (x: Union[builtins.str, builtins.bytes], base: builtins.int) -> builtins.int), builtins.int] +# --------------------------- +# Make +# --------------------------- + [case test_make_from_dict] # cmd: mypy a.py [file a.py] import attr C = attr.make_class("C", { - "x": attr.Attr(type=int), - "y": attr.Attr() + "x": attr.ib(type=int), + "y": attr.ib() }) [out] From b68657f5197eb2be5c30520e1683f6833c6c884d Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Wed, 8 Nov 2017 11:38:08 -0800 Subject: [PATCH 16/64] Add tox stubs env to travis --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 4f1cf2f60..e10a94655 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,7 +33,8 @@ matrix: env: TOXENV=readme - python: "3.6" env: TOXENV=changelog - + - python: "3.6" + env: TOXENV=stubs install: - pip install tox From 4a2d58942b95ff79b48271531d20c8093a31c9f4 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Wed, 8 Nov 2017 11:53:17 -0800 Subject: [PATCH 17/64] used the wrong comment character in mypy tests --- tests/test_stubs.test | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/test_stubs.test b/tests/test_stubs.test index 11126b910..50c8aa146 100644 --- a/tests/test_stubs.test +++ b/tests/test_stubs.test @@ -1,6 +1,6 @@ -# --------------------------- -# Basics -# --------------------------- +-- --------------------------- +-- Basics +-- --------------------------- [case test_no_type] # cmd: mypy a.py @@ -54,9 +54,9 @@ reveal_type(C.a) # int a.py:8: error: Revealed type is 'builtins.int' a.py:9: error: Revealed type is 'builtins.int' -# --------------------------- -# Defaults -# --------------------------- +-- --------------------------- +-- Defaults +-- --------------------------- [case test_defaults_no_type] # cmd: mypy a.py @@ -109,9 +109,9 @@ a.py:7: error: Revealed type is 'builtins.int' a.py:9: error: Incompatible types in assignment (expression has type "object", variable has type "int") -# --------------------------- -# Factory Defaults -# --------------------------- +-- --------------------------- +-- Factory Defaults +-- --------------------------- [case test_factory_defaults_type_arg] # cmd: mypy a.py @@ -168,9 +168,9 @@ a.py:10: error: Argument 2 has incompatible type "Type[str]"; expected "Type[Lis a.py:16: error: Revealed type is 'builtins.int' -# --------------------------- -# Validators -# --------------------------- +-- --------------------------- +-- Validators +-- --------------------------- [case test_validators] # cmd: mypy a.py @@ -188,9 +188,9 @@ e = attr.ib(type=int, validator=1) # error a.py:8: error: No overload variant matches argument types [Overload(def (x: Union[builtins.str, builtins.bytes, typing.SupportsInt] =) -> builtins.int, def (x: Union[builtins.str, builtins.bytes], base: builtins.int) -> builtins.int), builtins.int] -# --------------------------- -# Make -# --------------------------- +-- --------------------------- +-- Make +-- --------------------------- [case test_make_from_dict] # cmd: mypy a.py From f254513897d8cf40b65c7f323ce8cdb34f1bd30e Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sat, 11 Nov 2017 14:47:01 -0800 Subject: [PATCH 18/64] Improve overloads using PyCharm order-based approach overloads are pretty broken in mypy. the best we can do for now is target PyCharm, which is much more forgiving. --- src/attr/__init__.pyi | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index adfd17e84..df94123b2 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -12,8 +12,8 @@ _C = TypeVar('_C', bound=type) _M = TypeVar('_M', bound=Mapping) _I = TypeVar('_I', bound=Iterable) -_ValidatorType = Callable[[Any, 'Attribute', Any], Any] -_ConverterType = Callable[[Any], Any] +_ValidatorType = Callable[[Any, 'Attribute', _T], Any] +_ConverterType = Callable[[Any], _T] _FilterType = Callable[['Attribute', Any], bool] # _make -- @@ -23,28 +23,22 @@ NOTHING : object # Factory lies about its return type to make this possible: `x: List[int] = Factory(list)` def Factory(factory: Union[Callable[[], _T], Callable[[Any], _T]], takes_self: bool = ...) -> _T: ... -class Attribute: +class Attribute(Generic[_T]): __slots__ = ("name", "default", "validator", "repr", "cmp", "hash", "init", "convert", "metadata", "type") name: str default: Any - validator: Optional[Union[_ValidatorType, List[_ValidatorType], Tuple[_ValidatorType, ...]]] + validator: Optional[Union[_ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T], ...]]] repr: bool cmp: bool hash: Optional[bool] init: bool - convert: Optional[_ConverterType] + convert: Optional[_ConverterType[_T]] metadata: Mapping - type: Optional[Any] + type: Optional[Type[_T]] -# order here matters: if default is provided but not type, we want the first overload chosen so that the type is based on default -@overload -def attr(default: _T = ..., validator: Optional[Union[_ValidatorType, List[_ValidatorType], Tuple[_ValidatorType, ...]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[_ConverterType] = ..., metadata: Mapping = ..., type: Type[_T] = ...) -> _T: ... -# NOTE: this overload for `attr` returns Any so that static analysis passes when used in the form: x : int = attr() -@overload -def attr(default: _T = ..., validator: Optional[Union[_ValidatorType, List[_ValidatorType], Tuple[_ValidatorType, ...]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[_ConverterType] = ..., metadata: Mapping = ...) -> Any: ... +def attr(default: _T = ..., validator: Optional[Union[_ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T], ...]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[_ConverterType[_T]] = ..., metadata: Mapping = ..., type: Type[_T] = ...) -> _T: ... -# we use Any instead of _CountingAttr so that e.g. `make_class('Foo', [attr.ib()])` is valid @overload def attributes(maybe_cls: _C = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> _C: ... @@ -54,15 +48,21 @@ def attributes(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., rep def fields(cls: type) -> Tuple[Attribute, ...]: ... def validate(inst: Any) -> None: ... +# we use Any instead of _CountingAttr so that e.g. `make_class('Foo', [attr.ib()])` is valid def make_class(name, attrs: Union[List[Any], Dict[str, Any]], bases: Tuple[type, ...] = ..., **attributes_arguments) -> type: ... # _funcs -- -# FIXME: having problems assigning a default to the factory typevars -# def asdict(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[_M] = dict, retain_collection_types: bool = ...) -> _M[str, Any]: ... -# def astuple(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[_I] = tuple, retain_collection_types: bool = ...) -> _I: ... -def asdict(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[_M] = ..., retain_collection_types: bool = ...) -> _M: ... -def astuple(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[_I] = ..., retain_collection_types: bool = ...) -> _I: ... +@overload +def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[_M], retain_collection_types: bool = ...) -> _M: ... +@overload +def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... + +@overload +def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[_I], retain_collection_types: bool = ...) -> _I: ... +@overload +def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., retain_collection_types: bool = ...) -> tuple: ... + def has(cls: type) -> bool: ... def assoc(inst: _T, **changes) -> _T: ... def evolve(inst: _T, **changes) -> _T: ... From 121b742a1c763100dc2a977063ead918df00857f Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Thu, 14 Dec 2017 09:54:55 -0800 Subject: [PATCH 19/64] Remove features not yet working in mypy. Document remaining issues. --- src/attr/__init__.pyi | 33 ++++++++++++------ tests/test_stubs.test | 79 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 89 insertions(+), 23 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index df94123b2..6f2706d45 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -1,4 +1,4 @@ -from typing import Any, Callable, Dict, Generic, Iterable, List, Optional, Mapping, Tuple, Type, TypeVar, Union, overload +from typing import Any, Callable, Collection, Dict, Generic, List, Optional, Mapping, Tuple, Type, TypeVar, Union, overload # `import X as X` is required to expose these to mypy. otherwise they are invisible from . import exceptions as exceptions from . import filters as filters @@ -10,7 +10,7 @@ from . import validators as validators _T = TypeVar('_T') _C = TypeVar('_C', bound=type) _M = TypeVar('_M', bound=Mapping) -_I = TypeVar('_I', bound=Iterable) +_I = TypeVar('_I', bound=Collection) _ValidatorType = Callable[[Any, 'Attribute', _T], Any] _ConverterType = Callable[[Any], _T] @@ -37,6 +37,10 @@ class Attribute(Generic[_T]): type: Optional[Type[_T]] +# FIXME: if no type arg or annotation is provided when using `attr` it will result in an error: +# error: Need type annotation for variable +# See discussion here: https://github.com/python/mypy/issues/4227 +# tl;dr: Waiting on a fix to https://github.com/python/typing/issues/253 def attr(default: _T = ..., validator: Optional[Union[_ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T], ...]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[_ConverterType[_T]] = ..., metadata: Mapping = ..., type: Type[_T] = ...) -> _T: ... @@ -49,19 +53,26 @@ def fields(cls: type) -> Tuple[Attribute, ...]: ... def validate(inst: Any) -> None: ... # we use Any instead of _CountingAttr so that e.g. `make_class('Foo', [attr.ib()])` is valid -def make_class(name, attrs: Union[List[Any], Dict[str, Any]], bases: Tuple[type, ...] = ..., **attributes_arguments) -> type: ... +def make_class(name, attrs: Union[List[str], Dict[str, Any]], bases: Tuple[type, ...] = ..., **attributes_arguments) -> type: ... # _funcs -- -@overload -def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[_M], retain_collection_types: bool = ...) -> _M: ... -@overload -def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... +# FIXME: asdict/astuple do not honor their factory args. waiting on one of these: +# https://github.com/python/mypy/issues/4236 +# https://github.com/python/typing/issues/253 +def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[Mapping] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... -@overload -def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[_I], retain_collection_types: bool = ...) -> _I: ... -@overload -def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., retain_collection_types: bool = ...) -> tuple: ... +# @overload +# def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[_M], retain_collection_types: bool = ...) -> _M: ... +# @overload +# def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... + +def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[Collection] = ..., retain_collection_types: bool = ...) -> Tuple[Any, ...]: ... + +# @overload +# def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[_I], retain_collection_types: bool = ...) -> _I: ... +# @overload +# def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., retain_collection_types: bool = ...) -> tuple: ... def has(cls: type) -> bool: ... def assoc(inst: _T, **changes) -> _T: ... diff --git a/tests/test_stubs.test b/tests/test_stubs.test index 50c8aa146..b83f773b8 100644 --- a/tests/test_stubs.test +++ b/tests/test_stubs.test @@ -8,18 +8,23 @@ import attr @attr.s -class C(object): - a = attr.ib() # error: need annotation +class C: + a = attr.ib() + b = attr.ib(init=False, metadata={'foo': 1}) c = C() reveal_type(c.a) # Any reveal_type(C.a) # Any +reveal_type(c.b) # Any +reveal_type(C.b) # Any [out] a.py:5: error: Need type annotation for variable -a.py:8: error: Revealed type is 'Any' a.py:9: error: Revealed type is 'Any' -a.py:9: error: Cannot determine type of 'a' - +a.py:10: error: Revealed type is 'Any' +a.py:10: error: Cannot determine type of 'a' +a.py:11: error: Revealed type is 'Any' +a.py:12: error: Revealed type is 'Any' +a.py:12: error: Cannot determine type of 'b' [case test_type_arg] # cmd: mypy a.py @@ -66,8 +71,12 @@ import attr a = attr.ib(default=0) reveal_type(a) # int +b = attr.ib(0) +reveal_type(b) # int + [out] a.py:4: error: Revealed type is 'builtins.int*' +a.py:7: error: Revealed type is 'builtins.int*' [case test_defaults_type_arg] @@ -123,7 +132,7 @@ a = attr.ib(type=List[int]) reveal_type(a) # List[int] b = attr.ib(default=attr.Factory(list), type=List[int]) -reveal_type(b) # object: FIXME: shouldn't the `default` type be upgraded from `list` to `List[int]``? make issue on mypy about this +reveal_type(b) # object: FIXME: shouldn't the `default` type be upgraded from `list` to `List[int]``? make a mypy github issue c = attr.ib(default=attr.Factory(list), type=int) reveal_type(c) # object, the common base of list and int @@ -185,8 +194,27 @@ d = attr.ib(type=int, validator=and_(in_([1, 2, 3]), instance_of(int))) e = attr.ib(type=int, validator=1) # error [out] -a.py:8: error: No overload variant matches argument types [Overload(def (x: Union[builtins.str, builtins.bytes, typing.SupportsInt] =) -> builtins.int, def (x: Union[builtins.str, builtins.bytes], base: builtins.int) -> builtins.int), builtins.int] +a.py:8: error: Argument 2 has incompatible type "int"; expected "Union[Callable[[Any, Attribute[Any], int], Any], List[Callable[[Any, Attribute[Any], int], Any]], Tuple[Callable[[Any, Attribute[Any], int], Any], ...]]" + +[case test_custom_validators] +# cmd: mypy a.py +[file a.py] +import attr + +def validate_int(inst, at, val: int): + pass + +def validate_str(inst, at, val: str): + pass + +a = attr.ib(type=int, validator=validate_int) # int +b = attr.ib(type=int, validator=validate_str) # error +reveal_type(a) + +[out] +a.py:10: error: Argument 2 has incompatible type "Callable[[Any, Any, str], Any]"; expected "Union[Callable[[Any, Attribute[Any], int], Any], List[Callable[[Any, Attribute[Any], int], Any]], Tuple[Callable[[Any, Attribute[Any], int], Any], ...]]" +a.py:12: error: Revealed type is 'builtins.int' -- --------------------------- -- Make @@ -203,12 +231,39 @@ C = attr.make_class("C", { [out] -[case test_make_from_attrib] +[case test_make_from_str] +# cmd: mypy a.py +[file a.py] +import attr +C = attr.make_class("C", ["x", "y"]) +[out] + + +[case test_astuple] +# cmd: mypy a.py +[file a.py] +import attr +@attr.s +class C: + a: int = attr.ib() + +t1 = attr.astuple(C) +reveal_type(t1) + +[out] +a.py:7: error: Revealed type is 'builtins.tuple[Any]' + + +[case test_asdict] # cmd: mypy a.py [file a.py] import attr -C = attr.make_class("C", [ - attr.ib(type=int), - attr.ib() -]) +@attr.s +class C: + a: int = attr.ib() + +t1 = attr.asdict(C) +reveal_type(t1) + [out] +a.py:7: error: Revealed type is 'builtins.dict[builtins.str, Any]' From c42bbd21b19e266cf17dbca960e4a258c9907223 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Wed, 20 Dec 2017 18:39:34 -0800 Subject: [PATCH 20/64] Test stubs against euresti fork of mypy with attrs plugin Copied the pytest plugin from mypy for testing annotations: It is not an officially supported API and using the plugin from mypy could break after any update. --- stubs-requirements.txt | 2 + tests/mypy_pytest_plugin.py | 934 ++++++++++++++++++++++++++++++++++++ tests/test_stubs.py | 19 +- tests/test_stubs.test | 7 +- tox.ini | 4 +- 5 files changed, 948 insertions(+), 18 deletions(-) create mode 100644 stubs-requirements.txt create mode 100644 tests/mypy_pytest_plugin.py diff --git a/stubs-requirements.txt b/stubs-requirements.txt new file mode 100644 index 000000000..547eaf77d --- /dev/null +++ b/stubs-requirements.txt @@ -0,0 +1,2 @@ +pytest +git+git://github.com/euresti/mypy.git@attrs_plugin#egg=mypy \ No newline at end of file diff --git a/tests/mypy_pytest_plugin.py b/tests/mypy_pytest_plugin.py new file mode 100644 index 000000000..15bb2d7d8 --- /dev/null +++ b/tests/mypy_pytest_plugin.py @@ -0,0 +1,934 @@ +"""Utilities for processing .test files containing test case descriptions.""" + +import os.path +import os +import posixpath +import re +import tempfile +from os import remove, rmdir +import shutil +import sys +from abc import abstractmethod + +import pytest # type: ignore # no pytest in typeshed +from typing import List, Tuple, Set, Optional, Iterator, Any, Dict, NamedTuple, Union + +# AssertStringArraysEqual displays special line alignment helper messages if +# the first different line has at least this many characters, +MIN_LINE_LENGTH_FOR_ALIGNMENT = 5 + +test_temp_dir = 'tmp' +test_data_prefix = os.path.dirname(__file__) + +root_dir = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..')) + +# File modify/create operation: copy module contents from source_path. +UpdateFile = NamedTuple('UpdateFile', [('module', str), + ('source_path', str), + ('target_path', str)]) + +# File delete operation: delete module file. +DeleteFile = NamedTuple('DeleteFile', [('module', str), + ('path', str)]) + +FileOperation = Union[UpdateFile, DeleteFile] + + +class AssertionFailure(Exception): + """Exception used to signal failed test cases.""" + def __init__(self, s: Optional[str] = None) -> None: + if s: + super().__init__(s) + else: + super().__init__() + + +class BaseTestCase: + """Common base class for _MyUnitTestCase and DataDrivenTestCase. + + Handles temporary folder creation and deletion. + """ + def __init__(self, name: str) -> None: + self.name = name + self.old_cwd = None # type: Optional[str] + self.tmpdir = None # type: Optional[tempfile.TemporaryDirectory[str]] + + def setup(self) -> None: + self.old_cwd = os.getcwd() + self.tmpdir = tempfile.TemporaryDirectory(prefix='mypy-test-') + os.chdir(self.tmpdir.name) + os.mkdir('tmp') + + def teardown(self) -> None: + assert self.old_cwd is not None and self.tmpdir is not None, \ + "test was not properly set up" + os.chdir(self.old_cwd) + try: + self.tmpdir.cleanup() + except OSError: + pass + self.old_cwd = None + self.tmpdir = None + + + +def assert_string_arrays_equal(expected: List[str], actual: List[str], + msg: str) -> None: + """Assert that two string arrays are equal. + + Display any differences in a human-readable form. + """ + + actual = clean_up(actual) + + if actual != expected: + num_skip_start = num_skipped_prefix_lines(expected, actual) + num_skip_end = num_skipped_suffix_lines(expected, actual) + + sys.stderr.write('Expected:\n') + + # If omit some lines at the beginning, indicate it by displaying a line + # with '...'. + if num_skip_start > 0: + sys.stderr.write(' ...\n') + + # Keep track of the first different line. + first_diff = -1 + + # Display only this many first characters of identical lines. + width = 75 + + for i in range(num_skip_start, len(expected) - num_skip_end): + if i >= len(actual) or expected[i] != actual[i]: + if first_diff < 0: + first_diff = i + sys.stderr.write(' {:<45} (diff)'.format(expected[i])) + else: + e = expected[i] + sys.stderr.write(' ' + e[:width]) + if len(e) > width: + sys.stderr.write('...') + sys.stderr.write('\n') + if num_skip_end > 0: + sys.stderr.write(' ...\n') + + sys.stderr.write('Actual:\n') + + if num_skip_start > 0: + sys.stderr.write(' ...\n') + + for j in range(num_skip_start, len(actual) - num_skip_end): + if j >= len(expected) or expected[j] != actual[j]: + sys.stderr.write(' {:<45} (diff)'.format(actual[j])) + else: + a = actual[j] + sys.stderr.write(' ' + a[:width]) + if len(a) > width: + sys.stderr.write('...') + sys.stderr.write('\n') + if actual == []: + sys.stderr.write(' (empty)\n') + if num_skip_end > 0: + sys.stderr.write(' ...\n') + + sys.stderr.write('\n') + + if first_diff >= 0 and first_diff < len(actual) and ( + len(expected[first_diff]) >= MIN_LINE_LENGTH_FOR_ALIGNMENT + or len(actual[first_diff]) >= MIN_LINE_LENGTH_FOR_ALIGNMENT): + # Display message that helps visualize the differences between two + # long lines. + show_align_message(expected[first_diff], actual[first_diff]) + + raise AssertionFailure(msg) + + +def show_align_message(s1: str, s2: str) -> None: + """Align s1 and s2 so that the their first difference is highlighted. + + For example, if s1 is 'foobar' and s2 is 'fobar', display the + following lines: + + E: foobar + A: fobar + ^ + + If s1 and s2 are long, only display a fragment of the strings around the + first difference. If s1 is very short, do nothing. + """ + + # Seeing what went wrong is trivial even without alignment if the expected + # string is very short. In this case do nothing to simplify output. + if len(s1) < 4: + return + + maxw = 72 # Maximum number of characters shown + + sys.stderr.write('Alignment of first line difference:\n') + + trunc = False + while s1[:30] == s2[:30]: + s1 = s1[10:] + s2 = s2[10:] + trunc = True + + if trunc: + s1 = '...' + s1 + s2 = '...' + s2 + + max_len = max(len(s1), len(s2)) + extra = '' + if max_len > maxw: + extra = '...' + + # Write a chunk of both lines, aligned. + sys.stderr.write(' E: {}{}\n'.format(s1[:maxw], extra)) + sys.stderr.write(' A: {}{}\n'.format(s2[:maxw], extra)) + # Write an indicator character under the different columns. + sys.stderr.write(' ') + for j in range(min(maxw, max(len(s1), len(s2)))): + if s1[j:j + 1] != s2[j:j + 1]: + sys.stderr.write('^') # Difference + break + else: + sys.stderr.write(' ') # Equal + sys.stderr.write('\n') + + +def assert_string_arrays_equal_wildcards(expected: List[str], + actual: List[str], + msg: str) -> None: + # Like above, but let a line with only '...' in expected match any number + # of lines in actual. + actual = clean_up(actual) + + while actual != [] and actual[-1] == '': + actual = actual[:-1] + + # Expand "..." wildcards away. + expected = match_array(expected, actual) + assert_string_arrays_equal(expected, actual, msg) + + +def clean_up(a: List[str]) -> List[str]: + """Remove common directory prefix from all strings in a. + + This uses a naive string replace; it seems to work well enough. Also + remove trailing carriage returns. + """ + res = [] + for s in a: + prefix = os.sep + ss = s + for p in prefix, prefix.replace(os.sep, '/'): + if p != '/' and p != '//' and p != '\\' and p != '\\\\': + ss = ss.replace(p, '') + # Ignore spaces at end of line. + ss = re.sub(' +$', '', ss) + res.append(re.sub('\\r$', '', ss)) + return res + + +def match_array(pattern: List[str], target: List[str]) -> List[str]: + """Expand '...' wildcards in pattern by matching against target.""" + + res = [] # type: List[str] + i = 0 + j = 0 + + while i < len(pattern): + if pattern[i] == '...': + # Wildcard in pattern. + if i + 1 == len(pattern): + # Wildcard at end of pattern; match the rest of target. + res.extend(target[j:]) + # Finished. + break + else: + # Must find the instance of the next pattern line in target. + jj = j + while jj < len(target): + if target[jj] == pattern[i + 1]: + break + jj += 1 + if jj == len(target): + # No match. Get out. + res.extend(pattern[i:]) + break + res.extend(target[j:jj]) + i += 1 + j = jj + elif (j < len(target) and (pattern[i] == target[j] + or (i + 1 < len(pattern) + and j + 1 < len(target) + and pattern[i + 1] == target[j + 1]))): + # In sync; advance one line. The above condition keeps sync also if + # only a single line is different, but loses it if two consecutive + # lines fail to match. + res.append(pattern[i]) + i += 1 + j += 1 + else: + # Out of sync. Get out. + res.extend(pattern[i:]) + break + return res + + +def num_skipped_prefix_lines(a1: List[str], a2: List[str]) -> int: + num_eq = 0 + while num_eq < min(len(a1), len(a2)) and a1[num_eq] == a2[num_eq]: + num_eq += 1 + return max(0, num_eq - 4) + + +def num_skipped_suffix_lines(a1: List[str], a2: List[str]) -> int: + num_eq = 0 + while (num_eq < min(len(a1), len(a2)) + and a1[-num_eq - 1] == a2[-num_eq - 1]): + num_eq += 1 + return max(0, num_eq - 4) + + +def normalize_error_messages(messages: List[str]) -> List[str]: + """Translate an array of error messages to use / as path separator.""" + + a = [] + for m in messages: + a.append(m.replace(os.sep, '/')) + return a + + + +# -- + + +def parse_test_cases( + path: str, + base_path: str = '.', + optional_out: bool = False, + native_sep: bool = False) -> List['DataDrivenTestCase']: + """Parse a file with test case descriptions. + + Return an array of test cases. + + NB: this function and DataDrivenTestCase were shared between the + myunit and pytest codepaths -- if something looks redundant, + that's likely the reason. + """ + if native_sep: + join = os.path.join + else: + join = posixpath.join # type: ignore + include_path = os.path.dirname(path) + with open(path, encoding='utf-8') as f: + lst = f.readlines() + for i in range(len(lst)): + lst[i] = lst[i].rstrip('\n') + p = parse_test_data(lst, path) + out = [] # type: List[DataDrivenTestCase] + + # Process the parsed items. Each item has a header of form [id args], + # optionally followed by lines of text. + i = 0 + while i < len(p): + ok = False + i0 = i + if p[i].id == 'case': + i += 1 + + files = [] # type: List[Tuple[str, str]] # path and contents + output_files = [] # type: List[Tuple[str, str]] # path and contents for output files + tcout = [] # type: List[str] # Regular output errors + tcout2 = {} # type: Dict[int, List[str]] # Output errors for incremental, runs 2+ + deleted_paths = {} # type: Dict[int, Set[str]] # from run number of paths + stale_modules = {} # type: Dict[int, Set[str]] # from run number to module names + rechecked_modules = {} # type: Dict[ int, Set[str]] # from run number module names + triggered = [] # type: List[str] # Active triggers (one line per incremental step) + while i < len(p) and p[i].id != 'case': + if p[i].id == 'file' or p[i].id == 'outfile': + # Record an extra file needed for the test case. + arg = p[i].arg + assert arg is not None + contents = '\n'.join(p[i].data) + contents = expand_variables(contents) + file_entry = (join(base_path, arg), contents) + if p[i].id == 'file': + files.append(file_entry) + elif p[i].id == 'outfile': + output_files.append(file_entry) + elif p[i].id in ('builtins', 'builtins_py2'): + # Use an alternative stub file for the builtins module. + arg = p[i].arg + assert arg is not None + mpath = join(os.path.dirname(path), arg) + if p[i].id == 'builtins': + fnam = 'builtins.pyi' + else: + # Python 2 + fnam = '__builtin__.pyi' + with open(mpath) as f: + files.append((join(base_path, fnam), f.read())) + elif p[i].id == 'typing': + # Use an alternative stub file for the typing module. + arg = p[i].arg + assert arg is not None + src_path = join(os.path.dirname(path), arg) + with open(src_path) as f: + files.append((join(base_path, 'typing.pyi'), f.read())) + elif re.match(r'stale[0-9]*$', p[i].id): + if p[i].id == 'stale': + passnum = 1 + else: + passnum = int(p[i].id[len('stale'):]) + assert passnum > 0 + arg = p[i].arg + if arg is None: + stale_modules[passnum] = set() + else: + stale_modules[passnum] = {item.strip() for item in arg.split(',')} + elif re.match(r'rechecked[0-9]*$', p[i].id): + if p[i].id == 'rechecked': + passnum = 1 + else: + passnum = int(p[i].id[len('rechecked'):]) + arg = p[i].arg + if arg is None: + rechecked_modules[passnum] = set() + else: + rechecked_modules[passnum] = {item.strip() for item in arg.split(',')} + elif p[i].id == 'delete': + # File to delete during a multi-step test case + arg = p[i].arg + assert arg is not None + m = re.match(r'(.*)\.([0-9]+)$', arg) + assert m, 'Invalid delete section: {}'.format(arg) + num = int(m.group(2)) + assert num >= 2, "Can't delete during step {}".format(num) + full = join(base_path, m.group(1)) + deleted_paths.setdefault(num, set()).add(full) + elif p[i].id == 'out' or p[i].id == 'out1': + tcout = p[i].data + tcout = [expand_variables(line) for line in tcout] + if os.path.sep == '\\': + tcout = [fix_win_path(line) for line in tcout] + ok = True + elif re.match(r'out[0-9]*$', p[i].id): + passnum = int(p[i].id[3:]) + assert passnum > 1 + output = p[i].data + output = [expand_variables(line) for line in output] + if native_sep and os.path.sep == '\\': + output = [fix_win_path(line) for line in output] + tcout2[passnum] = output + ok = True + elif p[i].id == 'triggered' and p[i].arg is None: + triggered = p[i].data + else: + raise ValueError( + 'Invalid section header {} in {} at line {}'.format( + p[i].id, path, p[i].line)) + i += 1 + + for passnum in stale_modules.keys(): + if passnum not in rechecked_modules: + # If the set of rechecked modules isn't specified, make it the same as the set + # of modules with a stale public interface. + rechecked_modules[passnum] = stale_modules[passnum] + if (passnum in stale_modules + and passnum in rechecked_modules + and not stale_modules[passnum].issubset(rechecked_modules[passnum])): + raise ValueError( + ('Stale modules after pass {} must be a subset of rechecked ' + 'modules ({}:{})').format(passnum, path, p[i0].line)) + + if optional_out: + ok = True + + if ok: + input = expand_includes(p[i0].data, include_path) + expand_errors(input, tcout, 'main') + for file_path, contents in files: + expand_errors(contents.split('\n'), tcout, file_path) + lastline = p[i].line if i < len(p) else p[i - 1].line + 9999 + arg0 = p[i0].arg + assert arg0 is not None + tc = DataDrivenTestCase(arg0, input, tcout, tcout2, path, + p[i0].line, lastline, + files, output_files, stale_modules, + rechecked_modules, deleted_paths, native_sep, + triggered) + out.append(tc) + if not ok: + raise ValueError( + '{}, line {}: Error in test case description'.format( + path, p[i0].line)) + + return out + + +class DataDrivenTestCase(BaseTestCase): + """Holds parsed data and handles directory setup and teardown for MypyDataCase.""" + + # TODO: rename to ParsedTestCase or merge with MypyDataCase (yet avoid multiple inheritance) + # TODO: only create files on setup, not during parsing + + input = None # type: List[str] + output = None # type: List[str] # Output for the first pass + output2 = None # type: Dict[int, List[str]] # Output for runs 2+, indexed by run number + + file = '' + line = 0 + + # (file path, file content) tuples + files = None # type: List[Tuple[str, str]] + expected_stale_modules = None # type: Dict[int, Set[str]] + expected_rechecked_modules = None # type: Dict[int, Set[str]] + + # Files/directories to clean up after test case; (is directory, path) tuples + clean_up = None # type: List[Tuple[bool, str]] + + def __init__(self, + name: str, + input: List[str], + output: List[str], + output2: Dict[int, List[str]], + file: str, + line: int, + lastline: int, + files: List[Tuple[str, str]], + output_files: List[Tuple[str, str]], + expected_stale_modules: Dict[int, Set[str]], + expected_rechecked_modules: Dict[int, Set[str]], + deleted_paths: Dict[int, Set[str]], + native_sep: bool = False, + triggered: Optional[List[str]] = None, + ) -> None: + super().__init__(name) + self.input = input + self.output = output + self.output2 = output2 + self.lastline = lastline + self.file = file + self.line = line + self.files = files + self.output_files = output_files + self.expected_stale_modules = expected_stale_modules + self.expected_rechecked_modules = expected_rechecked_modules + self.deleted_paths = deleted_paths + self.native_sep = native_sep + self.triggered = triggered or [] + + def setup(self) -> None: + super().setup() + encountered_files = set() + self.clean_up = [] + for paths in self.deleted_paths.values(): + for path in paths: + self.clean_up.append((False, path)) + encountered_files.add(path) + for path, content in self.files: + dir = os.path.dirname(path) + for d in self.add_dirs(dir): + self.clean_up.append((True, d)) + with open(path, 'w') as f: + f.write(content) + if path not in encountered_files: + self.clean_up.append((False, path)) + encountered_files.add(path) + if re.search(r'\.[2-9]$', path): + # Make sure new files introduced in the second and later runs are accounted for + renamed_path = path[:-2] + if renamed_path not in encountered_files: + encountered_files.add(renamed_path) + self.clean_up.append((False, renamed_path)) + for path, _ in self.output_files: + # Create directories for expected output and mark them to be cleaned up at the end + # of the test case. + dir = os.path.dirname(path) + for d in self.add_dirs(dir): + self.clean_up.append((True, d)) + self.clean_up.append((False, path)) + + def add_dirs(self, dir: str) -> List[str]: + """Add all subdirectories required to create dir. + + Return an array of the created directories in the order of creation. + """ + if dir == '' or os.path.isdir(dir): + return [] + else: + dirs = self.add_dirs(os.path.dirname(dir)) + [dir] + os.mkdir(dir) + return dirs + + def teardown(self) -> None: + # First remove files. + for is_dir, path in reversed(self.clean_up): + if not is_dir: + try: + remove(path) + except FileNotFoundError: + # Breaking early using Ctrl+C may happen before file creation. Also, some + # files may be deleted by a test case. + pass + # Then remove directories. + for is_dir, path in reversed(self.clean_up): + if is_dir: + pycache = os.path.join(path, '__pycache__') + if os.path.isdir(pycache): + shutil.rmtree(pycache) + try: + rmdir(path) + except OSError as error: + print(' ** Error removing directory %s -- contents:' % path) + for item in os.listdir(path): + print(' ', item) + # Most likely, there are some files in the + # directory. Use rmtree to nuke the directory, but + # fail the test case anyway, since this seems like + # a bug in a test case -- we shouldn't leave + # garbage lying around. By nuking the directory, + # the next test run hopefully passes. + path = error.filename + # Be defensive -- only call rmtree if we're sure we aren't removing anything + # valuable. + if path.startswith(test_temp_dir + '/') and os.path.isdir(path): + shutil.rmtree(path) + raise + super().teardown() + + def find_steps(self) -> List[List[FileOperation]]: + """Return a list of descriptions of file operations for each incremental step. + + The first list item corresponds to the first incremental step, the second for the + second step, etc. Each operation can either be a file modification/creation (UpdateFile) + or deletion (DeleteFile). + """ + steps = {} # type: Dict[int, List[FileOperation]] + for path, _ in self.files: + m = re.match(r'.*\.([0-9]+)$', path) + if m: + num = int(m.group(1)) + assert num >= 2 + target_path = re.sub(r'\.[0-9]+$', '', path) + module = module_from_path(target_path) + operation = UpdateFile(module, path, target_path) + steps.setdefault(num, []).append(operation) + for num, paths in self.deleted_paths.items(): + assert num >= 2 + for path in paths: + module = module_from_path(path) + steps.setdefault(num, []).append(DeleteFile(module, path)) + max_step = max(steps) + return [steps[num] for num in range(2, max_step + 1)] + + +def module_from_path(path: str) -> str: + path = re.sub(r'\.py$', '', path) + # We can have a mix of Unix-style and Windows-style separators. + parts = re.split(r'[/\\]', path) + assert parts[0] == test_temp_dir + del parts[0] + module = '.'.join(parts) + module = re.sub(r'\.__init__$', '', module) + return module + + +class TestItem: + """Parsed test caseitem. + + An item is of the form + [id arg] + .. data .. + """ + + id = '' + arg = '' # type: Optional[str] + + # Text data, array of 8-bit strings + data = None # type: List[str] + + file = '' + line = 0 # Line number in file + + def __init__(self, id: str, arg: Optional[str], data: List[str], file: str, + line: int) -> None: + self.id = id + self.arg = arg + self.data = data + self.file = file + self.line = line + + +def parse_test_data(l: List[str], fnam: str) -> List[TestItem]: + """Parse a list of lines that represent a sequence of test items.""" + + ret = [] # type: List[TestItem] + data = [] # type: List[str] + + id = None # type: Optional[str] + arg = None # type: Optional[str] + + i = 0 + i0 = 0 + while i < len(l): + s = l[i].strip() + + if l[i].startswith('[') and s.endswith(']') and not s.startswith('[['): + if id: + data = collapse_line_continuation(data) + data = strip_list(data) + ret.append(TestItem(id, arg, strip_list(data), fnam, i0 + 1)) + i0 = i + id = s[1:-1] + arg = None + if ' ' in id: + arg = id[id.index(' ') + 1:] + id = id[:id.index(' ')] + data = [] + elif l[i].startswith('[['): + data.append(l[i][1:]) + elif not l[i].startswith('--'): + data.append(l[i]) + elif l[i].startswith('----'): + data.append(l[i][2:]) + i += 1 + + # Process the last item. + if id: + data = collapse_line_continuation(data) + data = strip_list(data) + ret.append(TestItem(id, arg, data, fnam, i0 + 1)) + + return ret + + +def strip_list(l: List[str]) -> List[str]: + """Return a stripped copy of l. + + Strip whitespace at the end of all lines, and strip all empty + lines from the end of the array. + """ + + r = [] # type: List[str] + for s in l: + # Strip spaces at end of line + r.append(re.sub(r'\s+$', '', s)) + + while len(r) > 0 and r[-1] == '': + r.pop() + + return r + + +def collapse_line_continuation(l: List[str]) -> List[str]: + r = [] # type: List[str] + cont = False + for s in l: + ss = re.sub(r'\\$', '', s) + if cont: + r[-1] += re.sub('^ +', '', ss) + else: + r.append(ss) + cont = s.endswith('\\') + return r + + +def expand_includes(a: List[str], base_path: str) -> List[str]: + """Expand @includes within a list of lines. + + Replace all lies starting with @include with the contents of the + file name following the prefix. Look for the files in base_path. + """ + + res = [] # type: List[str] + for s in a: + if s.startswith('@include '): + fn = s.split(' ', 1)[1].strip() + with open(os.path.join(base_path, fn)) as f: + res.extend(f.readlines()) + else: + res.append(s) + return res + + +def expand_variables(s: str) -> str: + return s.replace('', root_dir) + + +def expand_errors(input: List[str], output: List[str], fnam: str) -> None: + """Transform comments such as '# E: message' or + '# E:3: message' in input. + + The result is lines like 'fnam:line: error: message'. + """ + + for i in range(len(input)): + # The first in the split things isn't a comment + for possible_err_comment in input[i].split(' # ')[1:]: + m = re.search( + '^([ENW]):((?P\d+):)? (?P.*)$', + possible_err_comment.strip()) + if m: + if m.group(1) == 'E': + severity = 'error' + elif m.group(1) == 'N': + severity = 'note' + elif m.group(1) == 'W': + severity = 'warning' + col = m.group('col') + if col is None: + output.append( + '{}:{}: {}: {}'.format(fnam, i + 1, severity, m.group('message'))) + else: + output.append('{}:{}:{}: {}: {}'.format( + fnam, i + 1, col, severity, m.group('message'))) + + +def fix_win_path(line: str) -> str: + r"""Changes Windows paths to Linux paths in error messages. + + E.g. foo\bar.py -> foo/bar.py. + """ + line = line.replace(root_dir, root_dir.replace('\\', '/')) + m = re.match(r'^([\S/]+):(\d+:)?(\s+.*)', line) + if not m: + return line + else: + filename, lineno, message = m.groups() + return '{}:{}{}'.format(filename.replace('\\', '/'), + lineno or '', message) + + +def fix_cobertura_filename(line: str) -> str: + r"""Changes filename paths to Linux paths in Cobertura output files. + + E.g. filename="pkg\subpkg\a.py" -> filename="pkg/subpkg/a.py". + """ + m = re.search(r' None: + group = parser.getgroup('mypy') + group.addoption('--update-data', action='store_true', default=False, + help='Update test data to reflect actual output' + ' (supported only for certain tests)') + + +# This function name is special to pytest. See +# http://doc.pytest.org/en/latest/writing_plugins.html#collection-hooks +def pytest_pycollect_makeitem(collector: Any, name: str, + obj: object) -> 'Optional[Any]': + """Called by pytest on each object in modules configured in conftest.py files. + + collector is pytest.Collector, returns Optional[pytest.Class] + """ + if isinstance(obj, type): + # Only classes derived from DataSuite contain test cases, not the DataSuite class itself + if issubclass(obj, DataSuite) and obj is not DataSuite: + # Non-None result means this obj is a test case. + # The collect method of the returned MypyDataSuite instance will be called later, + # with self.obj being obj. + return MypyDataSuite(name, parent=collector) + return None + + +class MypyDataSuite(pytest.Class): # type: ignore # inheriting from Any + def collect(self) -> Iterator[pytest.Item]: # type: ignore + """Called by pytest on each of the object returned from pytest_pycollect_makeitem""" + + # obj is the object for which pytest_pycollect_makeitem returned self. + suite = self.obj # type: DataSuite + for f in suite.files: + for case in parse_test_cases(os.path.join(test_data_prefix, f), + base_path=suite.base_path, + optional_out=suite.optional_out, + native_sep=suite.native_sep): + if suite.filter(case): + yield MypyDataCase(case.name, self, case) + + +def is_incremental(testcase: DataDrivenTestCase) -> bool: + return 'incremental' in testcase.name.lower() or 'incremental' in testcase.file + + +def has_stable_flags(testcase: DataDrivenTestCase) -> bool: + if any(re.match(r'# flags[2-9]:', line) for line in testcase.input): + return False + for filename, contents in testcase.files: + if os.path.basename(filename).startswith('mypy.ini.'): + return False + return True + + +class MypyDataCase(pytest.Item): # type: ignore # inheriting from Any + def __init__(self, name: str, parent: MypyDataSuite, case: DataDrivenTestCase) -> None: + self.skip = False + if name.endswith('-skip'): + self.skip = True + name = name[:-len('-skip')] + + super().__init__(name, parent) + self.case = case + + def runtest(self) -> None: + if self.skip: + pytest.skip() + update_data = self.config.getoption('--update-data', False) + self.parent.obj(update_data=update_data).run_case(self.case) + + def setup(self) -> None: + self.case.setup() + + def teardown(self) -> None: + self.case.teardown() + + def reportinfo(self) -> Tuple[str, int, str]: + return self.case.file, self.case.line, self.case.name + + def repr_failure(self, excinfo: Any) -> str: + if excinfo.errisinstance(SystemExit): + # We assume that before doing exit() (which raises SystemExit) we've printed + # enough context about what happened so that a stack trace is not useful. + # In particular, uncaught exceptions during semantic analysis or type checking + # call exit() and they already print out a stack trace. + excrepr = excinfo.exconly() + else: + self.parent._prunetraceback(excinfo) + excrepr = excinfo.getrepr(style='short') + + return "data: {}:{}:\n{}".format(self.case.file, self.case.line, excrepr) + + +class DataSuite: + # option fields - class variables + files = None # type: List[str] + base_path = '.' # type: str + optional_out = False # type: bool + native_sep = False # type: bool + + def __init__(self, *, update_data: bool) -> None: + self.update_data = update_data + + @abstractmethod + def run_case(self, testcase: DataDrivenTestCase) -> None: + raise NotImplementedError + + @classmethod + def filter(cls, testcase: DataDrivenTestCase) -> bool: + return True diff --git a/tests/test_stubs.py b/tests/test_stubs.py index 5b7f237cc..270bb08bc 100644 --- a/tests/test_stubs.py +++ b/tests/test_stubs.py @@ -5,11 +5,10 @@ import subprocess import sys -from mypy.test.data import parse_test_cases, DataSuite -from mypy.test.helpers import (assert_string_arrays_equal, - normalize_error_messages) +from tests.mypy_pytest_plugin import (DataSuite, assert_string_arrays_equal, + normalize_error_messages) -pytest_plugins = ['mypy.test.data'] +pytest_plugins = ['tests.mypy_pytest_plugin'] # Path to Python 3 interpreter python3_path = sys.executable @@ -19,14 +18,10 @@ class PythonEvaluationSuite(DataSuite): - - @classmethod - def cases(cls): - return parse_test_cases(test_file, - _test_python_evaluation, - base_path=test_temp_dir, - optional_out=True, - native_sep=True) + files = [test_file] + base_path = test_temp_dir + optional_out = True + native_sep = True def run_case(self, testcase): _test_python_evaluation(testcase) diff --git a/tests/test_stubs.test b/tests/test_stubs.test index b83f773b8..c72b9b10a 100644 --- a/tests/test_stubs.test +++ b/tests/test_stubs.test @@ -12,13 +12,14 @@ class C: a = attr.ib() b = attr.ib(init=False, metadata={'foo': 1}) -c = C() +c = C(1) reveal_type(c.a) # Any reveal_type(C.a) # Any reveal_type(c.b) # Any reveal_type(C.b) # Any [out] a.py:5: error: Need type annotation for variable +a.py:6: error: Need type annotation for variable a.py:9: error: Revealed type is 'Any' a.py:10: error: Revealed type is 'Any' a.py:10: error: Cannot determine type of 'a' @@ -35,7 +36,7 @@ import attr class C(object): a = attr.ib(type=int) -c = C() +c = C(1) reveal_type(c.a) # int reveal_type(C.a) # int [out] @@ -52,7 +53,7 @@ import attr class C(object): a : int = attr.ib() -c = C() +c = C(1) reveal_type(c.a) # int reveal_type(C.a) # int [out] diff --git a/tox.ini b/tox.ini index 5f10f30be..92fd4f6dd 100644 --- a/tox.ini +++ b/tox.ini @@ -69,7 +69,5 @@ commands = [testenv:stubs] basepython = python3.6 -deps = - pytest - mypy +deps = -rstubs-requirements.txt commands = pytest tests/test_stubs.py From db25517559608c986e41395d1fb9f7d4ebfc5f24 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Fri, 22 Dec 2017 07:15:24 -0800 Subject: [PATCH 21/64] Add some types and TypeVars to some types. Make tests pass --- src/attr/__init__.pyi | 74 ++++++++++++++++++++++++----------------- src/attr/converters.pyi | 5 ++- src/attr/exceptions.pyi | 6 ++-- src/attr/validators.pyi | 14 ++++---- tests/test_stubs.test | 6 +--- 5 files changed, 58 insertions(+), 47 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 6f2706d45..d2cfa549b 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -1,4 +1,4 @@ -from typing import Any, Callable, Collection, Dict, Generic, List, Optional, Mapping, Tuple, Type, TypeVar, Union, overload +from typing import Any, Callable, Dict, Generic, List, Optional, Sequence, Mapping, Tuple, Type, TypeVar, Union, overload # `import X as X` is required to expose these to mypy. otherwise they are invisible from . import exceptions as exceptions from . import filters as filters @@ -9,12 +9,11 @@ from . import validators as validators _T = TypeVar('_T') _C = TypeVar('_C', bound=type) -_M = TypeVar('_M', bound=Mapping) -_I = TypeVar('_I', bound=Collection) _ValidatorType = Callable[[Any, 'Attribute', _T], Any] _ConverterType = Callable[[Any], _T] _FilterType = Callable[['Attribute', Any], bool] +_ValidatorArgType = Union[_ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T], ...]] # _make -- @@ -24,30 +23,53 @@ NOTHING : object def Factory(factory: Union[Callable[[], _T], Callable[[Any], _T]], takes_self: bool = ...) -> _T: ... class Attribute(Generic[_T]): - __slots__ = ("name", "default", "validator", "repr", "cmp", "hash", "init", "convert", "metadata", "type") name: str - default: Any - validator: Optional[Union[_ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T], ...]]] + default: Optional[_T] + validator: Optional[_ValidatorArgType[_T]] repr: bool cmp: bool hash: Optional[bool] init: bool convert: Optional[_ConverterType[_T]] - metadata: Mapping + metadata: Dict[Any, Any] type: Optional[Type[_T]] + def __lt__(self, x: Attribute) -> bool: ... + def __le__(self, x: Attribute) -> bool: ... + def __gt__(self, x: Attribute) -> bool: ... + def __ge__(self, x: Attribute) -> bool: ... -# FIXME: if no type arg or annotation is provided when using `attr` it will result in an error: -# error: Need type annotation for variable -# See discussion here: https://github.com/python/mypy/issues/4227 -# tl;dr: Waiting on a fix to https://github.com/python/typing/issues/253 -def attr(default: _T = ..., validator: Optional[Union[_ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T], ...]]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: Optional[_ConverterType[_T]] = ..., metadata: Mapping = ..., type: Type[_T] = ...) -> _T: ... + +# `attr` also lies about its return type to make the following possible: +# attr() -> Any +# attr(8) -> int +# attr(validator=) -> Whatever the callable expects. +# This makes this type of assignments possible: +# x: int = attr(8) +# +# 1st form catches a default value set. Can't use = ... or you get "overloaded overlap" error. +@overload +def attrib(default: _T, validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., + convert: _ConverterType[_T] = ..., metadata: Mapping = ..., + type: Type[_T] = ...) -> _T: ... +@overload +def attrib(default: None = ..., validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., + convert: Optional[_ConverterType[_T]] = ..., metadata: Mapping = ..., + type: Optional[Type[_T]] = ...) -> _T: ... +# 3rd form catches nothing set. So returns Any. +@overload +def attrib(default: None = ..., validator: None = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., + convert: None = ..., metadata: Mapping = ..., + type: None = ...) -> Any: ... @overload -def attributes(maybe_cls: _C = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> _C: ... +def attrs(maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> _C: ... @overload -def attributes(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... +def attrs(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... def fields(cls: type) -> Tuple[Attribute, ...]: ... def validate(inst: Any) -> None: ... @@ -60,23 +82,12 @@ def make_class(name, attrs: Union[List[str], Dict[str, Any]], bases: Tuple[type, # FIXME: asdict/astuple do not honor their factory args. waiting on one of these: # https://github.com/python/mypy/issues/4236 # https://github.com/python/typing/issues/253 -def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[Mapping] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... - -# @overload -# def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[_M], retain_collection_types: bool = ...) -> _M: ... -# @overload -# def asdict(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... - -def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[Collection] = ..., retain_collection_types: bool = ...) -> Tuple[Any, ...]: ... - -# @overload -# def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[_I], retain_collection_types: bool = ...) -> _I: ... -# @overload -# def astuple(inst: Any, *, recurse: bool = ..., filter: Optional[_FilterType] = ..., retain_collection_types: bool = ...) -> tuple: ... +def asdict(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[Mapping] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... +def astuple(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[Sequence] = ..., retain_collection_types: bool = ...) -> Tuple[Any, ...]: ... def has(cls: type) -> bool: ... -def assoc(inst: _T, **changes) -> _T: ... -def evolve(inst: _T, **changes) -> _T: ... +def assoc(inst: _T, **changes: Any) -> _T: ... +def evolve(inst: _T, **changes: Any) -> _T: ... # _config -- @@ -84,5 +95,6 @@ def set_run_validators(run: bool) -> None: ... def get_run_validators() -> bool: ... # aliases -s = attrs = attributes -ib = attrib = attr +s = attributes = attrs +ib = attr = attrib +dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) diff --git a/src/attr/converters.pyi b/src/attr/converters.pyi index 0629940bd..9e31677e7 100644 --- a/src/attr/converters.pyi +++ b/src/attr/converters.pyi @@ -1,3 +1,6 @@ +from typing import TypeVar, Optional from . import _ConverterType -def optional(converter: _ConverterType) -> _ConverterType: ... +_T = TypeVar('_T') + +def optional(converter: _ConverterType[_T]) -> _ConverterType[Optional[_T]]: ... diff --git a/src/attr/exceptions.pyi b/src/attr/exceptions.pyi index b86a1b46a..4a2904fc2 100644 --- a/src/attr/exceptions.pyi +++ b/src/attr/exceptions.pyi @@ -1,8 +1,6 @@ - class FrozenInstanceError(AttributeError): - msg : str = ... - args : tuple = ... - + msg: str = ... class AttrsAttributeNotFoundError(ValueError): ... class NotAnAttrsClassError(ValueError): ... class DefaultAlreadySetError(RuntimeError): ... +class UnannotatedAttributeError(RuntimeError): ... diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index a7bf3d1ff..477bac91c 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -1,8 +1,10 @@ -from typing import Container, List, Union +from typing import Container, List, Union, TypeVar, Type, Any, Optional 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: _ValidatorType) -> _ValidatorType: ... +_T = TypeVar('_T') + +def instance_of(type: Type[_T]) -> _ValidatorType[_T]: ... +def provides(interface: Any) -> _ValidatorType[Any]: ... +def optional(validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]]) -> _ValidatorType[Optional[_T]]: ... +def in_(options: Container[_T]) -> _ValidatorType[_T]: ... +def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... diff --git a/tests/test_stubs.test b/tests/test_stubs.test index c72b9b10a..ddc58d915 100644 --- a/tests/test_stubs.test +++ b/tests/test_stubs.test @@ -18,14 +18,10 @@ reveal_type(C.a) # Any reveal_type(c.b) # Any reveal_type(C.b) # Any [out] -a.py:5: error: Need type annotation for variable -a.py:6: error: Need type annotation for variable a.py:9: error: Revealed type is 'Any' a.py:10: error: Revealed type is 'Any' -a.py:10: error: Cannot determine type of 'a' a.py:11: error: Revealed type is 'Any' a.py:12: error: Revealed type is 'Any' -a.py:12: error: Cannot determine type of 'b' [case test_type_arg] # cmd: mypy a.py @@ -195,7 +191,7 @@ d = attr.ib(type=int, validator=and_(in_([1, 2, 3]), instance_of(int))) e = attr.ib(type=int, validator=1) # error [out] -a.py:8: error: Argument 2 has incompatible type "int"; expected "Union[Callable[[Any, Attribute[Any], int], Any], List[Callable[[Any, Attribute[Any], int], Any]], Tuple[Callable[[Any, Attribute[Any], int], Any], ...]]" +a.py:8: error: No overload variant matches argument types [Overload(def (x: Union[builtins.str, builtins.bytes, typing.SupportsInt] =) -> builtins.int, def (x: Union[builtins.str, builtins.bytes], base: builtins.int) -> builtins.int), builtins.int] [case test_custom_validators] # cmd: mypy a.py From ff7244178a7edc159fa92f99ef44aa4faa8835ad Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Fri, 22 Dec 2017 10:26:52 -0800 Subject: [PATCH 22/64] Suppress warnings about named attribute access from fields() e.g. fields(C).x Eventually it would be good to add support for returning NamedTuple from the mypy plugin --- src/attr/__init__.pyi | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index d2cfa549b..82ff891be 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -71,7 +71,12 @@ def attrs(maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., repr_ns: Optiona @overload def attrs(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... -def fields(cls: type) -> Tuple[Attribute, ...]: ... + +# TODO: add support for returning NamedTuple from the mypy plugin +class _Fields(Tuple[Attribute, ...]): + def __getattr__(self, name: str) -> Attribute: ... + +def fields(cls: type) -> _Fields: ... def validate(inst: Any) -> None: ... # we use Any instead of _CountingAttr so that e.g. `make_class('Foo', [attr.ib()])` is valid From 4fe065b19373c85904ff78eb0d330fb5dcfdd432 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Fri, 22 Dec 2017 10:27:41 -0800 Subject: [PATCH 23/64] Add WIP mypy-doctest plugin --- docs-requirements.txt | 3 +- docs/conf.py | 10 ++-- docs/doctest2.py | 112 ++++++++++++++++++++++++++++++++++++++++++ docs/examples.rst | 68 ++++++++++++++----------- docs/setup.py | 8 +++ 5 files changed, 167 insertions(+), 34 deletions(-) create mode 100644 docs/doctest2.py create mode 100644 docs/setup.py diff --git a/docs-requirements.txt b/docs-requirements.txt index c473e1e43..0003bbc1d 100644 --- a/docs-requirements.txt +++ b/docs-requirements.txt @@ -1,3 +1,4 @@ --e . +-e docs sphinx zope.interface +git+git://github.com/euresti/mypy.git@attrs_plugin#egg=mypy diff --git a/docs/conf.py b/docs/conf.py index 1cdb07c1f..36ef93b90 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -5,13 +5,15 @@ import re +HERE = os.path.abspath(os.path.dirname(__file__)) + + def read(*parts): """ Build an absolute path from *parts* and and return the contents of the resulting file. Assume UTF-8 encoding. """ - here = os.path.abspath(os.path.dirname(__file__)) - with codecs.open(os.path.join(here, *parts), "rb", "utf-8") as f: + with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: return f.read() @@ -35,12 +37,14 @@ def find_version(*file_paths): # ones. extensions = [ 'sphinx.ext.autodoc', - 'sphinx.ext.doctest', + 'doctest2', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', ] +doctest_path = [os.path.join(HERE, '..', 'src')] + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/doctest2.py b/docs/doctest2.py new file mode 100644 index 000000000..b8d1edb20 --- /dev/null +++ b/docs/doctest2.py @@ -0,0 +1,112 @@ +from __future__ import absolute_import, print_function + +import sys +import sphinx +import doctest +import subprocess +import tempfile +import os +import shutil + +import sphinx.ext.doctest +from sphinx.ext.doctest import (TestsetupDirective, TestcleanupDirective, + DoctestDirective, TestcodeDirective, + TestoutputDirective, DocTestBuilder) + +# Path to Python 3 interpreter +python3_path = sys.executable + +HERE = os.path.abspath(os.path.dirname(__file__)) + + +class SphinxDocTestRunner(sphinx.ext.doctest.SphinxDocTestRunner): + group_source = '' + + def reset_source(self): + self.group_source = '' + + def run(self, test, compileflags=None, out=None, clear_globs=True): + # add the source for this block to the group + result = doctest.DocTestRunner.run(self, test, compileflags, out, + clear_globs) + self.group_source += ''.join(example.source + for example in test.examples) + return result + + +# patch the runner +sphinx.ext.doctest.SphinxDocTestRunner = SphinxDocTestRunner + + +class DocTest2Builder(DocTestBuilder): + def test_group(self, group, filename): + self.setup_runner.reset_source() + self.test_runner.reset_source() + self.cleanup_runner.reset_source() + + result = DocTestBuilder.test_group(self, group, filename) + + source = (self.setup_runner.group_source + + self.test_runner.group_source + + self.cleanup_runner.group_source) + + got = run_mypy(source, self.config.doctest_path) + + if got: + test = doctest.DocTest([], {}, group.name, '', 0, None) + example = doctest.Example('', '') + # if not quiet: + self.test_runner.report_failure(self._warn_out, test, example, got) + # we hardwire no. of failures and no. of tries to 1 + self.test_runner._DocTestRunner__record_outcome(test, 1, 1) + + return result + + +def run_mypy(code, mypy_path): + program = '_program.py' + test_temp_dir = tempfile.mkdtemp() + program_path = os.path.join(test_temp_dir, program) + with open(program_path, 'w') as file: + file.write(code) + args = [ + program, + '--hide-error-context', # don't precede errors w/ notes about context + '--show-traceback', + ] + # Type check the program. + env = {'MYPYPATH': os.path.pathsep.join(mypy_path)} + fixed = [python3_path, '-m', 'mypy'] + process = subprocess.Popen(fixed + args, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + env=env, + cwd=test_temp_dir) + outb = process.stdout.read() + # Split output into lines. + out = [s.split(':', 1)[1] for s in str(outb, 'utf8').splitlines()] + # Remove temp file. + os.remove(program_path) + shutil.rmtree(test_temp_dir) + return '\n'.join(out) + + +def setup(app): + app.add_directive('testsetup', TestsetupDirective) + app.add_directive('testcleanup', TestcleanupDirective) + app.add_directive('doctest', DoctestDirective) + app.add_directive('testcode', TestcodeDirective) + app.add_directive('testoutput', TestoutputDirective) + app.add_builder(DocTest2Builder) + # this config value adds to sys.path + app.add_config_value('doctest_path', [], False) + app.add_config_value('doctest_test_doctest_blocks', 'default', False) + app.add_config_value('doctest_global_setup', '', False) + app.add_config_value('doctest_global_cleanup', '', False) + app.add_config_value( + 'doctest_default_flags', + (doctest.DONT_ACCEPT_TRUE_FOR_1 | + doctest.ELLIPSIS | + doctest.IGNORE_EXCEPTION_DETAIL), + False) + return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/docs/examples.rst b/docs/examples.rst index 4432e8fcb..f4153d81a 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -4,6 +4,10 @@ ==================== +.. testsetup:: * + + import attr + Basics ------ @@ -64,7 +68,7 @@ If playful naming turns you off, ``attrs`` comes with serious business aliases: For private attributes, ``attrs`` will strip the leading underscores for keyword arguments: -.. doctest:: +.. doctest:: private >>> @attr.s ... class C(object): @@ -74,14 +78,14 @@ For private attributes, ``attrs`` will strip the leading underscores for keyword If you want to initialize your private attributes yourself, you can do that too: -.. doctest:: +.. doctest:: private_init >>> @attr.s ... class C(object): ... _x = attr.ib(init=False, default=42) >>> C() C(_x=42) - >>> C(23) + >>> C(23) # type: ignore Traceback (most recent call last): ... TypeError: __init__() takes exactly 1 argument (2 given) @@ -89,12 +93,12 @@ If you want to initialize your private attributes yourself, you can do that too: An additional way of defining attributes is supported too. This is useful in times when you want to enhance classes that are not yours (nice ``__repr__`` for Django models anyone?): -.. doctest:: +.. doctest:: enhance >>> class SomethingFromSomeoneElse(object): ... def __init__(self, x): ... self.x = x - >>> SomethingFromSomeoneElse = attr.s( + >>> SomethingFromSomeoneElse = attr.s( # type: ignore ... these={ ... "x": attr.ib() ... }, init=False)(SomethingFromSomeoneElse) @@ -104,7 +108,7 @@ This is useful in times when you want to enhance classes that are not yours (nic `Subclassing is bad for you `_, but ``attrs`` will still do what you'd hope for: -.. doctest:: +.. doctest:: subclassing >>> @attr.s ... class A(object): @@ -131,7 +135,7 @@ In Python 3, classes defined within other classes are `detected >> @attr.s ... class C(object): @@ -160,7 +164,7 @@ When you have a class with data, it often is very convenient to transform that c Some fields cannot or should not be transformed. For that, :func:`attr.asdict` offers a callback that decides whether an attribute should be included: -.. doctest:: +.. doctest:: asdict_filtered >>> @attr.s ... class UserList(object): @@ -176,7 +180,7 @@ For that, :func:`attr.asdict` offers a callback that decides whether an attribut For the common case where you want to :func:`include ` or :func:`exclude ` certain types or attributes, ``attrs`` ships with a few helpers: -.. doctest:: +.. doctest:: asdict_include_exclude >>> @attr.s ... class User(object): @@ -198,7 +202,7 @@ For the common case where you want to :func:`include ` or Other times, all you want is a tuple and ``attrs`` won't let you down: -.. doctest:: +.. doctest:: astuple >>> import sqlite3 >>> import attr @@ -227,7 +231,7 @@ Sometimes you want to have default values for your initializer. And sometimes you even want mutable objects as default values (ever used accidentally ``def f(arg=[])``?). ``attrs`` has you covered in both cases: -.. doctest:: +.. doctest:: defaults >>> import collections >>> @attr.s @@ -269,7 +273,7 @@ More information on why class methods for constructing objects are awesome can b Default factories can also be set using a decorator. The method receives the partially initialized instance which enables you to base a default value on other attributes: -.. doctest:: +.. doctest:: default_decorator >>> @attr.s ... class C(object): @@ -304,7 +308,7 @@ The method has to accept three arguments: If the value does not pass the validator's standards, it just raises an appropriate exception. -.. doctest:: +.. doctest:: validator_decorator >>> @attr.s ... class C(object): @@ -330,7 +334,7 @@ It takes either a callable or a list of callables (usually functions) and treats Since the validators runs *after* the instance is initialized, you can refer to other attributes while validating: -.. doctest:: +.. doctest:: validators1 >>> def x_smaller_than_y(instance, attribute, value): ... if value >= instance.y: @@ -351,7 +355,7 @@ This example also shows of some syntactic sugar for using the :func:`attr.valida ``attrs`` won't intercept your changes to those attributes but you can always call :func:`attr.validate` on any instance to verify that it's still valid: -.. doctest:: +.. doctest:: validators1 >>> i = C(4, 5) >>> i.x = 5 # works, no magic here @@ -362,7 +366,7 @@ This example also shows of some syntactic sugar for using the :func:`attr.valida ``attrs`` ships with a bunch of validators, make sure to :ref:`check them out ` before writing your own: -.. doctest:: +.. doctest:: validators2 >>> @attr.s ... class C(object): @@ -376,7 +380,7 @@ This example also shows of some syntactic sugar for using the :func:`attr.valida Of course you can mix and match the two approaches at your convenience: -.. doctest:: +.. doctest:: validators3 >>> @attr.s ... class C(object): @@ -398,6 +402,8 @@ Of course you can mix and match the two approaches at your convenience: And finally you can disable validators globally: +.. doctest:: validators3 + >>> attr.set_run_validators(False) >>> C("128") C(x='128') @@ -414,7 +420,7 @@ Conversion Attributes can have a ``convert`` function specified, which will be called with the attribute's passed-in value to get a new value to use. This can be useful for doing type-conversions on values that you don't want to force your callers to do. -.. doctest:: +.. doctest:: converters1 >>> @attr.s ... class C(object): @@ -425,7 +431,7 @@ This can be useful for doing type-conversions on values that you don't want to f Converters are run *before* validators, so you can use validators to check the final form of the value. -.. doctest:: +.. doctest:: converters2 >>> def validate_x(instance, attribute, value): ... if value < 0: @@ -449,7 +455,7 @@ Metadata All ``attrs`` attributes may include arbitrary metadata in the form of a read-only dictionary. -.. doctest:: +.. doctest:: metadata >>> @attr.s ... class C(object): @@ -481,6 +487,8 @@ Types >>> attr.fields(C).y.type + >>> attr.fields(C)[1].type + If you don't mind annotating *all* attributes, you can even drop the :func:`attr.ib` and assign default values instead: @@ -528,7 +536,7 @@ The space consumption can become significant when creating large numbers of inst Normal Python classes can avoid using a separate dictionary for each instance of a class by `defining `_ ``__slots__``. For ``attrs`` classes it's enough to set ``slots=True``: -.. doctest:: +.. doctest:: slots1 >>> @attr.s(slots=True) ... class Coordinates(object): @@ -547,7 +555,7 @@ Slot classes are a little different than ordinary, dictionary-backed classes: Depending on your needs, this might be a good thing since it will let you catch typos early. This is not the case if your class inherits from any non-slot classes. - .. doctest:: + .. doctest:: slots2 >>> @attr.s(slots=True) ... class Coordinates(object): @@ -575,7 +583,7 @@ Slot classes are a little different than ordinary, dictionary-backed classes: - As always with slot classes, you must specify a ``__weakref__`` slot if you wish for the class to be weak-referenceable. Here's how it looks using ``attrs``: - .. doctest:: + .. doctest:: slots3 >>> import weakref >>> @attr.s(slots=True) @@ -596,7 +604,7 @@ Sometimes you have instances that shouldn't be changed after instantiation. Immutability is especially popular in functional programming and is generally a very good thing. If you'd like to enforce it, ``attrs`` will try to help: -.. doctest:: +.. doctest:: frozen1 >>> @attr.s(frozen=True) ... class C(object): @@ -615,7 +623,7 @@ By themselves, immutable classes are useful for long-lived objects that should n In order to use them in regular program flow, you'll need a way to easily create new instances with changed attributes. In Clojure that function is called `assoc `_ and ``attrs`` shamelessly imitates it: :func:`attr.evolve`: -.. doctest:: +.. doctest:: frozen2 >>> @attr.s(frozen=True) ... class C(object): @@ -637,7 +645,7 @@ Other Goodies Sometimes you may want to create a class programmatically. ``attrs`` won't let you down and gives you :func:`attr.make_class` : -.. doctest:: +.. doctest:: make_class >>> @attr.s ... class C1(object): @@ -649,7 +657,7 @@ Sometimes you may want to create a class programmatically. You can still have power over the attributes if you pass a dictionary of name: ``attr.ib`` mappings and can pass arguments to ``@attr.s``: -.. doctest:: +.. doctest:: make_class >>> C = attr.make_class("C", {"x": attr.ib(default=42), ... "y": attr.ib(default=attr.Factory(list))}, @@ -664,7 +672,7 @@ You can still have power over the attributes if you pass a dictionary of name: ` If you need to dynamically make a class with :func:`attr.make_class` and it needs to be a subclass of something else than ``object``, use the ``bases`` argument: -.. doctest:: +.. doctest:: make_subclass >>> class D(object): ... def __eq__(self, other): @@ -679,7 +687,7 @@ using ``@attr.s``. To do this, just define a ``__attrs_post_init__`` method in your class. It will get called at the end of the generated ``__init__`` method. -.. doctest:: +.. doctest:: post_init >>> @attr.s ... class C(object): @@ -695,7 +703,7 @@ It will get called at the end of the generated ``__init__`` method. Finally, you can exclude single attributes from certain methods: -.. doctest:: +.. doctest:: exclude >>> @attr.s ... class C(object): diff --git a/docs/setup.py b/docs/setup.py new file mode 100644 index 000000000..c083c802b --- /dev/null +++ b/docs/setup.py @@ -0,0 +1,8 @@ +from setuptools import setup +if __name__ == "__main__": + setup( + name='doctest2', + py_modules=['doctest2'], + version='0.1', + zip_safe=False, + ) From 762d9767c0786d3feb6262325d2f94001e2dbd3a Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Fri, 22 Dec 2017 14:57:47 -0800 Subject: [PATCH 24/64] Deal with a few remaining type issues in the docs --- docs/api.rst | 46 ++++++++++++++++++++++++---------------------- docs/doctest2.py | 10 ++++++++-- docs/examples.rst | 4 ++-- docs/extending.rst | 8 ++++++-- docs/why.rst | 6 +++++- 5 files changed, 45 insertions(+), 29 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index e2acb7400..2acffebeb 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -13,7 +13,9 @@ API Reference What follows is the API explanation, if you'd like a more hands-on introduction, have a look at :doc:`examples`. +.. testsetup:: * + import attr Core ---- @@ -26,7 +28,7 @@ Core For example: - .. doctest:: + .. doctest:: core >>> import attr >>> @attr.s @@ -39,7 +41,7 @@ Core ... self.x = x >>> D(1) - >>> D = attr.s(these={"x": attr.ib()}, init=False)(D) + >>> D = attr.s(these={"x": attr.ib()}, init=False)(D) # type: ignore >>> D(1) D(x=1) @@ -52,7 +54,7 @@ Core The object returned by :func:`attr.ib` also allows for setting the default and the validator using decorators: - .. doctest:: + .. doctest:: attrib >>> @attr.s ... class C(object): @@ -65,9 +67,9 @@ Core ... @y.default ... def name_does_not_matter(self): ... return self.x + 1 - >>> C(1) + >>> C(1) # type: ignore # default decorator not recognized C(x=1, y=2) - >>> C(-1) + >>> C(-1) # type: ignore # default decorator not recognized Traceback (most recent call last): ... ValueError: x must be positive @@ -83,7 +85,7 @@ Core You should never instantiate this class yourself! - .. doctest:: + .. doctest:: Attribute >>> import attr >>> @attr.s @@ -99,7 +101,7 @@ Core For example: - .. doctest:: + .. doctest:: make_class >>> C1 = attr.make_class("C1", ["x", "y"]) >>> C1(1, 2) @@ -114,7 +116,7 @@ Core For example: - .. doctest:: + .. doctest:: Factory >>> @attr.s ... class C(object): @@ -152,7 +154,7 @@ The moment you need a finer control over how your class is instantiated, it's us However, sometimes you need to do that one quick thing after your class is initialized. And for that ``attrs`` offers the ``__attrs_post_init__`` hook that is automatically detected and run after ``attrs`` is done initializing your instance: -.. doctest:: +.. doctest:: post_init1 >>> @attr.s ... class C(object): @@ -165,7 +167,7 @@ And for that ``attrs`` offers the ``__attrs_post_init__`` hook that is automatic Please note that you can't directly set attributes on frozen classes: -.. doctest:: +.. doctest:: post_init2 >>> @attr.s(frozen=True) ... class FrozenBroken(object): @@ -180,7 +182,7 @@ Please note that you can't directly set attributes on frozen classes: If you need to set attributes on a frozen class, you'll have to resort to the :ref:`same trick ` as ``attrs`` and use :meth:`object.__setattr__`: -.. doctest:: +.. doctest:: post_init3 >>> @attr.s(frozen=True) ... class Frozen(object): @@ -203,7 +205,7 @@ Helpers For example: - .. doctest:: + .. doctest:: fields >>> @attr.s ... class C(object): @@ -221,7 +223,7 @@ Helpers For example: - .. doctest:: + .. doctest:: has >>> @attr.s ... class C(object): @@ -236,7 +238,7 @@ Helpers For example: - .. doctest:: + .. doctest:: asdict >>> @attr.s ... class C(object): @@ -250,7 +252,7 @@ Helpers For example: - .. doctest:: + .. doctest:: astuple >>> @attr.s ... class C(object): @@ -271,7 +273,7 @@ See :ref:`asdict` for examples. For example: - .. doctest:: + .. doctest:: evolve >>> @attr.s ... class C(object): @@ -297,13 +299,13 @@ See :ref:`asdict` for examples. For example: - .. doctest:: + .. doctest:: validate >>> @attr.s ... class C(object): ... x = attr.ib(validator=attr.validators.instance_of(int)) >>> i = C(1) - >>> i.x = "1" + >>> i.x = "1" # type: ignore # this is a legit error >>> attr.validate(i) Traceback (most recent call last): ... @@ -330,7 +332,7 @@ Validators For example: - .. doctest:: + .. doctest:: validators.instance_of >>> @attr.s ... class C(object): @@ -350,7 +352,7 @@ Validators For example: - .. doctest:: + .. doctest:: validators.in_ >>> import enum >>> class State(enum.Enum): @@ -386,7 +388,7 @@ Validators For example: - .. doctest:: + .. doctest:: validators.optional >>> @attr.s ... class C(object): @@ -408,7 +410,7 @@ Converters For example: - .. doctest:: + .. doctest:: converters.optional >>> @attr.s ... class C(object): diff --git a/docs/doctest2.py b/docs/doctest2.py index b8d1edb20..884d899f2 100644 --- a/docs/doctest2.py +++ b/docs/doctest2.py @@ -83,8 +83,14 @@ def run_mypy(code, mypy_path): env=env, cwd=test_temp_dir) outb = process.stdout.read() - # Split output into lines. - out = [s.split(':', 1)[1] for s in str(outb, 'utf8').splitlines()] + # Split output into lines and strip the file name. + out = [] + for line in str(outb, 'utf8').splitlines(): + parts = line.split(':', 1) + if len(parts) == 2: + out.append(parts[1]) + else: + out.append(line) # Remove temp file. os.remove(program_path) shutil.rmtree(test_temp_dir) diff --git a/docs/examples.rst b/docs/examples.rst index f4153d81a..7cc7d18fc 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -385,7 +385,7 @@ Of course you can mix and match the two approaches at your convenience: >>> @attr.s ... class C(object): ... x = attr.ib(validator=attr.validators.instance_of(int)) - ... @x.validator + ... @x.validator # type: ignore ... def fits_byte(self, attribute, value): ... if not 0 < value < 256: ... raise ValueError("value out of bounds") @@ -563,7 +563,7 @@ Slot classes are a little different than ordinary, dictionary-backed classes: ... y = attr.ib() ... >>> c = Coordinates(x=1, y=2) - >>> c.z = 3 + >>> c.z = 3 # type: ignore Traceback (most recent call last): ... AttributeError: 'Coordinates' object has no attribute 'z' diff --git a/docs/extending.rst b/docs/extending.rst index d460ee9b8..291bfeef1 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -3,6 +3,10 @@ Extending ========= +.. testsetup:: * + + import attr + Each ``attrs``-decorated class has a ``__attrs_attrs__`` class attribute. It is a tuple of :class:`attr.Attribute` carrying meta-data about each attribute. @@ -56,7 +60,7 @@ Types This information is available to you: -.. doctest:: +.. doctest:: types >>> import attr >>> @attr.s @@ -96,7 +100,7 @@ Here are some tips for effective use of metadata: - Expose ``attr.ib`` wrappers for your specific metadata. This is a more graceful approach if your users don't require metadata from other libraries. - .. doctest:: + .. doctest:: metadata >>> MY_TYPE_METADATA = '__my_type_metadata' >>> diff --git a/docs/why.rst b/docs/why.rst index 9c64cb93c..96640a91d 100644 --- a/docs/why.rst +++ b/docs/why.rst @@ -3,6 +3,10 @@ Why not… ======== +.. testsetup:: * + + import attr + If you'd like third party's account why ``attrs`` is great, have a look at Glyph's `The One Python Library Everyone Needs `_! @@ -230,7 +234,7 @@ And who will guarantee you, that you don't accidentally flip the ``<`` in your t It also should be noted that ``attrs`` is not an all-or-nothing solution. You can freely choose which features you want and disable those that you want more control over: -.. doctest:: +.. doctest:: opt-in >>> @attr.s(repr=False) ... class SmartClass(object): From ecc34842d73f299c338c30b9e5606190b266f8d4 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Thu, 28 Dec 2017 16:04:28 -0800 Subject: [PATCH 25/64] sphinx doctest: don't turn warnings into errors. doing so makes the tests abort after the first failed group. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 8ecbd3456..c0950ce88 100644 --- a/tox.ini +++ b/tox.ini @@ -58,7 +58,7 @@ setenv = deps = -rdocs-requirements.txt commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html - sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html + sphinx-build -b doctest -d {envtmpdir}/doctrees docs docs/_build/html python -m doctest README.rst From 2cd9d53e528751d2b5a85cd87b51e9da80b7d40a Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Thu, 28 Dec 2017 16:07:02 -0800 Subject: [PATCH 26/64] Update "type: ignore" comments to reflect issues fixed in mypy plugin --- docs/api.rst | 6 +++--- docs/examples.rst | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 2acffebeb..c18327e72 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -41,7 +41,7 @@ Core ... self.x = x >>> D(1) - >>> D = attr.s(these={"x": attr.ib()}, init=False)(D) # type: ignore + >>> D = attr.s(these={"x": attr.ib()}, init=False)(D) # type: ignore # can't override a type >>> D(1) D(x=1) @@ -67,9 +67,9 @@ Core ... @y.default ... def name_does_not_matter(self): ... return self.x + 1 - >>> C(1) # type: ignore # default decorator not recognized + >>> C(1) C(x=1, y=2) - >>> C(-1) # type: ignore # default decorator not recognized + >>> C(-1) Traceback (most recent call last): ... ValueError: x must be positive diff --git a/docs/examples.rst b/docs/examples.rst index c625ffec7..42e7bee79 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -98,7 +98,7 @@ This is useful in times when you want to enhance classes that are not yours (nic >>> class SomethingFromSomeoneElse(object): ... def __init__(self, x): ... self.x = x - >>> SomethingFromSomeoneElse = attr.s( # type: ignore + >>> SomethingFromSomeoneElse = attr.s( # type: ignore # can't override a type ... these={ ... "x": attr.ib() ... }, init=False)(SomethingFromSomeoneElse) @@ -385,7 +385,7 @@ Of course you can mix and match the two approaches at your convenience: >>> @attr.s ... class C(object): ... x = attr.ib(validator=attr.validators.instance_of(int)) - ... @x.validator # type: ignore + ... @x.validator ... def fits_byte(self, attribute, value): ... if not 0 < value < 256: ... raise ValueError("value out of bounds") @@ -610,7 +610,7 @@ If you'd like to enforce it, ``attrs`` will try to help: ... class C(object): ... x = attr.ib() >>> i = C(1) - >>> i.x = 2 + >>> i.x = 2 # type: ignore Traceback (most recent call last): ... attr.exceptions.FrozenInstanceError: can't set attribute From 5bf15645775a5d189ce64f203155efdea2a46a63 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Thu, 28 Dec 2017 16:07:14 -0800 Subject: [PATCH 27/64] doctest2: improve output formatting --- docs/doctest2.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/doctest2.py b/docs/doctest2.py index 884d899f2..4c730a9eb 100644 --- a/docs/doctest2.py +++ b/docs/doctest2.py @@ -83,6 +83,9 @@ def run_mypy(code, mypy_path): env=env, cwd=test_temp_dir) outb = process.stdout.read() + # Remove temp file. + os.remove(program_path) + shutil.rmtree(test_temp_dir) # Split output into lines and strip the file name. out = [] for line in str(outb, 'utf8').splitlines(): @@ -91,10 +94,10 @@ def run_mypy(code, mypy_path): out.append(parts[1]) else: out.append(line) - # Remove temp file. - os.remove(program_path) - shutil.rmtree(test_temp_dir) - return '\n'.join(out) + if out: + return '\n'.join(out) + '\n' + else: + return '' def setup(app): From db2d2699967560a7c76b1952c301426b544302c0 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Thu, 28 Dec 2017 16:07:33 -0800 Subject: [PATCH 28/64] Update manifest --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 7d8aa21b0..cb2feb13f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,7 +8,7 @@ recursive-include src *.pyi recursive-include tests *.test # Tests -include tox.ini .coveragerc conftest.py dev-requirements.txt docs-requirements.txt +include tox.ini .coveragerc conftest.py dev-requirements.txt docs-requirements.txt stubs-requirements.txt recursive-include tests *.py # Documentation From 850a36d08f1ed626b0196be2229731ecc7c2e2b7 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Fri, 29 Dec 2017 09:19:10 -0800 Subject: [PATCH 29/64] static tests: use inline error declarations --- docs/doctest2.py | 73 +++++++++++++----- docs/examples.rst | 6 +- tests/mypy_pytest_plugin.py | 147 ++++++++++++++++++++++-------------- tests/test_stubs.py | 15 ++-- tests/test_stubs.test | 141 +++++++++------------------------- 5 files changed, 192 insertions(+), 190 deletions(-) diff --git a/docs/doctest2.py b/docs/doctest2.py index 4c730a9eb..f5f6607d2 100644 --- a/docs/doctest2.py +++ b/docs/doctest2.py @@ -18,6 +18,39 @@ HERE = os.path.abspath(os.path.dirname(__file__)) +MAIN = 'main' + +import re +# FIXME: pull this from mypy_test_plugin +def expand_errors(input, output, fnam: str): + """Transform comments such as '# E: message' or + '# E:3: message' in input. + + The result is lines like 'fnam:line: error: message'. + """ + + for i in range(len(input)): + # The first in the split things isn't a comment + for possible_err_comment in input[i].split(' # ')[1:]: + m = re.search( + '^([ENW]):((?P\d+):)? (?P.*)$', + possible_err_comment.strip()) + if m: + if m.group(1) == 'E': + severity = 'error' + elif m.group(1) == 'N': + severity = 'note' + elif m.group(1) == 'W': + severity = 'warning' + col = m.group('col') + if col is None: + output.append( + '{}:{}: {}: {}'.format(fnam, i + 1, severity, + m.group('message'))) + else: + output.append('{}:{}:{}: {}: {}'.format( + fnam, i + 1, col, severity, m.group('message'))) + class SphinxDocTestRunner(sphinx.ext.doctest.SphinxDocTestRunner): group_source = '' @@ -50,13 +83,16 @@ def test_group(self, group, filename): self.test_runner.group_source + self.cleanup_runner.group_source) + want_lines = [] + expand_errors(source.splitlines(), want_lines, MAIN) + want = '\n'.join(want_lines) + '\n' if want_lines else '' got = run_mypy(source, self.config.doctest_path) - - if got: + if want != got: test = doctest.DocTest([], {}, group.name, '', 0, None) - example = doctest.Example('', '') + example = doctest.Example(source, want) # if not quiet: - self.test_runner.report_failure(self._warn_out, test, example, got) + self.test_runner.report_failure(self._warn_out, test, example, + got) # we hardwire no. of failures and no. of tries to 1 self.test_runner._DocTestRunner__record_outcome(test, 1, 1) @@ -64,13 +100,15 @@ def test_group(self, group, filename): def run_mypy(code, mypy_path): - program = '_program.py' + """ + Returns error output + """ test_temp_dir = tempfile.mkdtemp() - program_path = os.path.join(test_temp_dir, program) + program_path = os.path.join(test_temp_dir, MAIN) with open(program_path, 'w') as file: file.write(code) args = [ - program, + MAIN, '--hide-error-context', # don't precede errors w/ notes about context '--show-traceback', ] @@ -86,18 +124,17 @@ def run_mypy(code, mypy_path): # Remove temp file. os.remove(program_path) shutil.rmtree(test_temp_dir) + return str(outb, 'utf8') + # return str(outb, 'utf8').splitlines() # Split output into lines and strip the file name. - out = [] - for line in str(outb, 'utf8').splitlines(): - parts = line.split(':', 1) - if len(parts) == 2: - out.append(parts[1]) - else: - out.append(line) - if out: - return '\n'.join(out) + '\n' - else: - return '' + # out = [] + # for line in str(outb, 'utf8').splitlines(): + # parts = line.split(':', 1) + # if len(parts) == 2: + # out.append(parts[1]) + # else: + # out.append(line) + # return out def setup(app): diff --git a/docs/examples.rst b/docs/examples.rst index 42e7bee79..8d5fdcffc 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -85,7 +85,7 @@ If you want to initialize your private attributes yourself, you can do that too: ... _x = attr.ib(init=False, default=42) >>> C() C(_x=42) - >>> C(23) # type: ignore + >>> C(23) # E: Too many arguments for "C" Traceback (most recent call last): ... TypeError: __init__() takes exactly 1 argument (2 given) @@ -563,7 +563,7 @@ Slot classes are a little different than ordinary, dictionary-backed classes: ... y = attr.ib() ... >>> c = Coordinates(x=1, y=2) - >>> c.z = 3 # type: ignore + >>> c.z = 3 # E: "Coordinates" has no attribute "z" Traceback (most recent call last): ... AttributeError: 'Coordinates' object has no attribute 'z' @@ -610,7 +610,7 @@ If you'd like to enforce it, ``attrs`` will try to help: ... class C(object): ... x = attr.ib() >>> i = C(1) - >>> i.x = 2 # type: ignore + >>> i.x = 2 # E: Property "x" defined in "C" is read-only Traceback (most recent call last): ... attr.exceptions.FrozenInstanceError: can't set attribute diff --git a/tests/mypy_pytest_plugin.py b/tests/mypy_pytest_plugin.py index 15bb2d7d8..9f0567518 100644 --- a/tests/mypy_pytest_plugin.py +++ b/tests/mypy_pytest_plugin.py @@ -1,17 +1,21 @@ """Utilities for processing .test files containing test case descriptions.""" -import os.path import os +import os.path import posixpath import re -import tempfile -from os import remove, rmdir import shutil import sys +import tempfile + from abc import abstractmethod +from os import remove, rmdir +from typing import ( + List, Tuple, Set, Optional, Iterator, Any, Dict, NamedTuple, Union +) import pytest # type: ignore # no pytest in typeshed -from typing import List, Tuple, Set, Optional, Iterator, Any, Dict, NamedTuple, Union + # AssertStringArraysEqual displays special line alignment helper messages if # the first different line has at least this many characters, @@ -20,7 +24,8 @@ test_temp_dir = 'tmp' test_data_prefix = os.path.dirname(__file__) -root_dir = os.path.normpath(os.path.join(os.path.dirname(__file__), '..', '..')) +root_dir = os.path.normpath(os.path.join( + os.path.dirname(__file__), '..', '..')) # File modify/create operation: copy module contents from source_path. UpdateFile = NamedTuple('UpdateFile', [('module', str), @@ -71,7 +76,6 @@ def teardown(self) -> None: self.tmpdir = None - def assert_string_arrays_equal(expected: List[str], actual: List[str], msg: str) -> None: """Assert that two string arrays are equal. @@ -299,10 +303,6 @@ def normalize_error_messages(messages: List[str]) -> List[str]: return a - -# -- - - def parse_test_cases( path: str, base_path: str = '.', @@ -337,14 +337,22 @@ def parse_test_cases( if p[i].id == 'case': i += 1 - files = [] # type: List[Tuple[str, str]] # path and contents - output_files = [] # type: List[Tuple[str, str]] # path and contents for output files - tcout = [] # type: List[str] # Regular output errors - tcout2 = {} # type: Dict[int, List[str]] # Output errors for incremental, runs 2+ - deleted_paths = {} # type: Dict[int, Set[str]] # from run number of paths - stale_modules = {} # type: Dict[int, Set[str]] # from run number to module names - rechecked_modules = {} # type: Dict[ int, Set[str]] # from run number module names - triggered = [] # type: List[str] # Active triggers (one line per incremental step) + # path and contents + files = [] # type: List[Tuple[str, str]] + # path and contents for output files + output_files = [] # type: List[Tuple[str, str]] + # Regular output errors + tcout = [] # type: List[str] + # Output errors for incremental, runs 2+ + tcout2 = {} # type: Dict[int, List[str]] + # from run number of paths + deleted_paths = {} # type: Dict[int, Set[str]] + # from run number to module names + stale_modules = {} # type: Dict[int, Set[str]] + # from run number module names + rechecked_modules = {} # type: Dict[ int, Set[str]] + # Active triggers (one line per incremental step) + triggered = [] # type: List[str] while i < len(p) and p[i].id != 'case': if p[i].id == 'file' or p[i].id == 'outfile': # Record an extra file needed for the test case. @@ -386,7 +394,8 @@ def parse_test_cases( if arg is None: stale_modules[passnum] = set() else: - stale_modules[passnum] = {item.strip() for item in arg.split(',')} + stale_modules[passnum] = {item.strip() + for item in arg.split(',')} elif re.match(r'rechecked[0-9]*$', p[i].id): if p[i].id == 'rechecked': passnum = 1 @@ -396,7 +405,9 @@ def parse_test_cases( if arg is None: rechecked_modules[passnum] = set() else: - rechecked_modules[passnum] = {item.strip() for item in arg.split(',')} + rechecked_modules[passnum] = {item.strip() + for item + in arg.split(',')} elif p[i].id == 'delete': # File to delete during a multi-step test case arg = p[i].arg @@ -432,15 +443,18 @@ def parse_test_cases( for passnum in stale_modules.keys(): if passnum not in rechecked_modules: - # If the set of rechecked modules isn't specified, make it the same as the set + # If the set of rechecked modules isn't specified, make it + # the same as the set # of modules with a stale public interface. rechecked_modules[passnum] = stale_modules[passnum] if (passnum in stale_modules and passnum in rechecked_modules - and not stale_modules[passnum].issubset(rechecked_modules[passnum])): + and not stale_modules[passnum].issubset( + rechecked_modules[passnum])): raise ValueError( - ('Stale modules after pass {} must be a subset of rechecked ' - 'modules ({}:{})').format(passnum, path, p[i0].line)) + ('Stale modules after pass {} must be a subset of ' + 'rechecked modules ({}:{})').format(passnum, path, + p[i0].line)) if optional_out: ok = True @@ -456,8 +470,8 @@ def parse_test_cases( tc = DataDrivenTestCase(arg0, input, tcout, tcout2, path, p[i0].line, lastline, files, output_files, stale_modules, - rechecked_modules, deleted_paths, native_sep, - triggered) + rechecked_modules, deleted_paths, + native_sep, triggered) out.append(tc) if not ok: raise ValueError( @@ -468,14 +482,18 @@ def parse_test_cases( class DataDrivenTestCase(BaseTestCase): - """Holds parsed data and handles directory setup and teardown for MypyDataCase.""" + """Holds parsed data and handles directory setup and teardown for + MypyDataCase.""" - # TODO: rename to ParsedTestCase or merge with MypyDataCase (yet avoid multiple inheritance) + # TODO: rename to ParsedTestCase or merge with MypyDataCase (yet avoid + # multiple inheritance) # TODO: only create files on setup, not during parsing input = None # type: List[str] - output = None # type: List[str] # Output for the first pass - output2 = None # type: Dict[int, List[str]] # Output for runs 2+, indexed by run number + # Output for the first pass + output = None # type: List[str] + # Output for runs 2+, indexed by run number + output2 = None # type: Dict[int, List[str]] file = '' line = 0 @@ -485,7 +503,8 @@ class DataDrivenTestCase(BaseTestCase): expected_stale_modules = None # type: Dict[int, Set[str]] expected_rechecked_modules = None # type: Dict[int, Set[str]] - # Files/directories to clean up after test case; (is directory, path) tuples + # Files/directories to clean up after test case; (is directory, path) + # tuples clean_up = None # type: List[Tuple[bool, str]] def __init__(self, @@ -537,14 +556,15 @@ def setup(self) -> None: self.clean_up.append((False, path)) encountered_files.add(path) if re.search(r'\.[2-9]$', path): - # Make sure new files introduced in the second and later runs are accounted for + # Make sure new files introduced in the second and later runs + # are accounted for renamed_path = path[:-2] if renamed_path not in encountered_files: encountered_files.add(renamed_path) self.clean_up.append((False, renamed_path)) for path, _ in self.output_files: - # Create directories for expected output and mark them to be cleaned up at the end - # of the test case. + # Create directories for expected output and mark them to be + # cleaned up at the end of the test case. dir = os.path.dirname(path) for d in self.add_dirs(dir): self.clean_up.append((True, d)) @@ -569,8 +589,8 @@ def teardown(self) -> None: try: remove(path) except FileNotFoundError: - # Breaking early using Ctrl+C may happen before file creation. Also, some - # files may be deleted by a test case. + # Breaking early using Ctrl+C may happen before file + # creation. Also, some files may be deleted by a test case. pass # Then remove directories. for is_dir, path in reversed(self.clean_up): @@ -581,7 +601,8 @@ def teardown(self) -> None: try: rmdir(path) except OSError as error: - print(' ** Error removing directory %s -- contents:' % path) + print(' ** Error removing directory %s -- contents:' % + path) for item in os.listdir(path): print(' ', item) # Most likely, there are some files in the @@ -591,19 +612,21 @@ def teardown(self) -> None: # garbage lying around. By nuking the directory, # the next test run hopefully passes. path = error.filename - # Be defensive -- only call rmtree if we're sure we aren't removing anything - # valuable. - if path.startswith(test_temp_dir + '/') and os.path.isdir(path): + # Be defensive -- only call rmtree if we're sure we aren't + # removing anything valuable. + if (path.startswith(test_temp_dir + '/') and + os.path.isdir(path)): shutil.rmtree(path) raise super().teardown() def find_steps(self) -> List[List[FileOperation]]: - """Return a list of descriptions of file operations for each incremental step. + """Return a list of descriptions of file operations for each + incremental step. - The first list item corresponds to the first incremental step, the second for the - second step, etc. Each operation can either be a file modification/creation (UpdateFile) - or deletion (DeleteFile). + The first list item corresponds to the first incremental step, the + second for the second step, etc. Each operation can either be a file + modification/creation (UpdateFile) or deletion (DeleteFile). """ steps = {} # type: Dict[int, List[FileOperation]] for path, _ in self.files: @@ -780,7 +803,8 @@ def expand_errors(input: List[str], output: List[str], fnam: str) -> None: col = m.group('col') if col is None: output.append( - '{}:{}: {}: {}'.format(fnam, i + 1, severity, m.group('message'))) + '{}:{}: {}: {}'.format(fnam, i + 1, severity, + m.group('message'))) else: output.append('{}:{}:{}: {}: {}'.format( fnam, i + 1, col, severity, m.group('message'))) @@ -822,7 +846,8 @@ def fix_cobertura_filename(line: str) -> str: # This function name is special to pytest. See -# http://doc.pytest.org/en/latest/writing_plugins.html#initialization-command-line-and-configuration-hooks +# http://doc.pytest.org/en/latest/writing_plugins.html#initialization- +# command-line-and-configuration-hooks def pytest_addoption(parser: Any) -> None: group = parser.getgroup('mypy') group.addoption('--update-data', action='store_true', default=False, @@ -834,15 +859,18 @@ def pytest_addoption(parser: Any) -> None: # http://doc.pytest.org/en/latest/writing_plugins.html#collection-hooks def pytest_pycollect_makeitem(collector: Any, name: str, obj: object) -> 'Optional[Any]': - """Called by pytest on each object in modules configured in conftest.py files. + """Called by pytest on each object in modules configured in conftest.py + files. collector is pytest.Collector, returns Optional[pytest.Class] """ if isinstance(obj, type): - # Only classes derived from DataSuite contain test cases, not the DataSuite class itself + # Only classes derived from DataSuite contain test cases, not the + # DataSuite class itself if issubclass(obj, DataSuite) and obj is not DataSuite: # Non-None result means this obj is a test case. - # The collect method of the returned MypyDataSuite instance will be called later, + # The collect method of the returned MypyDataSuite instance + # will be called later, # with self.obj being obj. return MypyDataSuite(name, parent=collector) return None @@ -850,7 +878,8 @@ def pytest_pycollect_makeitem(collector: Any, name: str, class MypyDataSuite(pytest.Class): # type: ignore # inheriting from Any def collect(self) -> Iterator[pytest.Item]: # type: ignore - """Called by pytest on each of the object returned from pytest_pycollect_makeitem""" + """Called by pytest on each of the object returned from + pytest_pycollect_makeitem""" # obj is the object for which pytest_pycollect_makeitem returned self. suite = self.obj # type: DataSuite @@ -864,7 +893,8 @@ def collect(self) -> Iterator[pytest.Item]: # type: ignore def is_incremental(testcase: DataDrivenTestCase) -> bool: - return 'incremental' in testcase.name.lower() or 'incremental' in testcase.file + return ('incremental' in testcase.name.lower() or + 'incremental' in testcase.file) def has_stable_flags(testcase: DataDrivenTestCase) -> bool: @@ -877,7 +907,8 @@ def has_stable_flags(testcase: DataDrivenTestCase) -> bool: class MypyDataCase(pytest.Item): # type: ignore # inheriting from Any - def __init__(self, name: str, parent: MypyDataSuite, case: DataDrivenTestCase) -> None: + def __init__(self, name: str, parent: MypyDataSuite, + case: DataDrivenTestCase) -> None: self.skip = False if name.endswith('-skip'): self.skip = True @@ -903,16 +934,18 @@ def reportinfo(self) -> Tuple[str, int, str]: def repr_failure(self, excinfo: Any) -> str: if excinfo.errisinstance(SystemExit): - # We assume that before doing exit() (which raises SystemExit) we've printed - # enough context about what happened so that a stack trace is not useful. - # In particular, uncaught exceptions during semantic analysis or type checking - # call exit() and they already print out a stack trace. + # We assume that before doing exit() (which raises SystemExit) + # we've printed enough context about what happened so that a stack + # trace is not useful. In particular, uncaught exceptions during + # semantic analysis or type checking call exit() and they already + # print out a stack trace. excrepr = excinfo.exconly() else: self.parent._prunetraceback(excinfo) excrepr = excinfo.getrepr(style='short') - return "data: {}:{}:\n{}".format(self.case.file, self.case.line, excrepr) + return "data: {}:{}:\n{}".format(self.case.file, self.case.line, + excrepr) class DataSuite: diff --git a/tests/test_stubs.py b/tests/test_stubs.py index 270bb08bc..befe6b9e8 100644 --- a/tests/test_stubs.py +++ b/tests/test_stubs.py @@ -5,8 +5,10 @@ import subprocess import sys -from tests.mypy_pytest_plugin import (DataSuite, assert_string_arrays_equal, - normalize_error_messages) +from tests.mypy_pytest_plugin import ( + DataSuite, assert_string_arrays_equal, normalize_error_messages +) + pytest_plugins = ['tests.mypy_pytest_plugin'] @@ -30,15 +32,18 @@ def run_case(self, testcase): def _test_python_evaluation(testcase): assert testcase.old_cwd is not None, "test was not properly set up" # Write the program to a file. - program = '_program.py' + # we omit .py extension to be compatible with called to + # expand_errors parse_test_cases. + program = 'main' 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'] + fixed = [python3_path, '-m', 'mypy', program] process = subprocess.Popen(fixed + args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -69,5 +74,5 @@ def parse_args(line): """ m = re.match('# cmd: mypy (.*)$', line) if not m: - return [] # No args; mypy will spit out an error. + return [] # No args return m.group(1).split() diff --git a/tests/test_stubs.test b/tests/test_stubs.test index ddc58d915..2971c0670 100644 --- a/tests/test_stubs.test +++ b/tests/test_stubs.test @@ -3,8 +3,6 @@ -- --------------------------- [case test_no_type] -# cmd: mypy a.py -[file a.py] import attr @attr.s @@ -13,19 +11,12 @@ class C: b = attr.ib(init=False, metadata={'foo': 1}) c = C(1) -reveal_type(c.a) # Any -reveal_type(C.a) # Any -reveal_type(c.b) # Any -reveal_type(C.b) # Any -[out] -a.py:9: error: Revealed type is 'Any' -a.py:10: error: Revealed type is 'Any' -a.py:11: error: Revealed type is 'Any' -a.py:12: error: Revealed type is 'Any' +reveal_type(c.a) # E: Revealed type is 'Any' +reveal_type(C.a) # E: Revealed type is 'Any' +reveal_type(c.b) # E: Revealed type is 'Any' +reveal_type(C.b) # E: Revealed type is 'Any' [case test_type_arg] -# cmd: mypy a.py -[file a.py] import attr @attr.s @@ -33,16 +24,11 @@ class C(object): a = attr.ib(type=int) c = C(1) -reveal_type(c.a) # int -reveal_type(C.a) # int -[out] -a.py:8: error: Revealed type is 'builtins.int*' -a.py:9: error: Revealed type is 'builtins.int*' +reveal_type(c.a) # E: Revealed type is 'builtins.int*' +reveal_type(C.a) # E: Revealed type is 'builtins.int*' [case test_type_annotations] -# cmd: mypy a.py -[file a.py] import attr @attr.s @@ -50,69 +36,48 @@ class C(object): a : int = attr.ib() c = C(1) -reveal_type(c.a) # int -reveal_type(C.a) # int -[out] -a.py:8: error: Revealed type is 'builtins.int' -a.py:9: error: Revealed type is 'builtins.int' +reveal_type(c.a) # E: Revealed type is 'builtins.int' +reveal_type(C.a) # E: Revealed type is 'builtins.int' + -- --------------------------- -- Defaults -- --------------------------- [case test_defaults_no_type] -# cmd: mypy a.py -[file a.py] import attr a = attr.ib(default=0) -reveal_type(a) # int +reveal_type(a) # E: Revealed type is 'builtins.int*' b = attr.ib(0) -reveal_type(b) # int - -[out] -a.py:4: error: Revealed type is 'builtins.int*' -a.py:7: error: Revealed type is 'builtins.int*' +reveal_type(b) # E: Revealed type is 'builtins.int*' [case test_defaults_type_arg] -# cmd: mypy a.py -[file a.py] import attr a = attr.ib(type=int) -reveal_type(a) # int +reveal_type(a) # E: Revealed type is 'builtins.int*' b = attr.ib(default=0, type=int) -reveal_type(b) # int +reveal_type(b) # E: Revealed type is 'builtins.int*' c = attr.ib(default='bad', type=int) -reveal_type(c) # object, the common base of str and int - -[out] -a.py:4: error: Revealed type is 'builtins.int*' -a.py:7: error: Revealed type is 'builtins.int*' -a.py:10: error: Revealed type is 'builtins.object*' +# object, the common base of str and int: +reveal_type(c) # E: Revealed type is 'builtins.object*' [case test_defaults_type_annotations] -# cmd: mypy a.py -[file a.py] import attr a: int = attr.ib() -reveal_type(a) # int +reveal_type(a) # E: Revealed type is 'builtins.int' b: int = attr.ib(default=0) -reveal_type(b) # int +reveal_type(b) # E: Revealed type is 'builtins.int' -c: int = attr.ib(default=0, type=str) # error: object <> int - -[out] -a.py:4: error: Revealed type is 'builtins.int' -a.py:7: error: Revealed type is 'builtins.int' -a.py:9: error: Incompatible types in assignment (expression has type "object", variable has type "int") +c: int = attr.ib(default=0, type=str) # E: Incompatible types in assignment (expression has type "object", variable has type "int") -- --------------------------- @@ -120,58 +85,45 @@ a.py:9: error: Incompatible types in assignment (expression has type "object", v -- --------------------------- [case test_factory_defaults_type_arg] -# cmd: mypy a.py -[file a.py] import attr from typing import List a = attr.ib(type=List[int]) -reveal_type(a) # List[int] +reveal_type(a) # E: Revealed type is 'builtins.list*[builtins.int*]' b = attr.ib(default=attr.Factory(list), type=List[int]) -reveal_type(b) # object: FIXME: shouldn't the `default` type be upgraded from `list` to `List[int]``? make a mypy github issue +# FIXME: shouldn't the `default` type be upgraded from `list` to `List[int]``? make a mypy github issue +reveal_type(b) # E: Revealed type is 'builtins.object*' c = attr.ib(default=attr.Factory(list), type=int) -reveal_type(c) # object, the common base of list and int + +# object, the common base of list and int: +reveal_type(c) # E: Revealed type is 'builtins.object*' def int_factory() -> int: return 0 d = attr.ib(default=attr.Factory(int_factory), type=int) -reveal_type(d) # int - -[out] -a.py:5: error: Revealed type is 'builtins.list*[builtins.int*]' -a.py:8: error: Revealed type is 'builtins.object*' -a.py:11: error: Revealed type is 'builtins.object*' -a.py:17: error: Revealed type is 'builtins.int*' +reveal_type(d) # E: Revealed type is 'builtins.int*' [case test_factory_defaults_type_annotations] -# cmd: mypy a.py -[file a.py] import attr from typing import List a: List[int] = attr.ib() -reveal_type(a) # List[int] +reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' b: List[int] = attr.ib(default=attr.Factory(list), type=List[int]) -reveal_type(b) # List[int] +reveal_type(b) # E: Revealed type is 'builtins.list[builtins.int]' -c: List[int] = attr.ib(default=attr.Factory(list), type=str) # error: str <> List[int] +c: List[int] = attr.ib(default=attr.Factory(list), type=str) # E: Argument 2 has incompatible type "Type[str]"; expected "Type[List[int]]" def int_factory() -> int: return 0 d: int = attr.ib(default=attr.Factory(int_factory)) -reveal_type(d) # int - -[out] -a.py:5: error: Revealed type is 'builtins.list[builtins.int]' -a.py:8: error: Revealed type is 'builtins.list[builtins.int]' -a.py:10: error: Argument 2 has incompatible type "Type[str]"; expected "Type[List[int]]" -a.py:16: error: Revealed type is 'builtins.int' +reveal_type(d) # E: Revealed type is 'builtins.int' -- --------------------------- @@ -179,8 +131,6 @@ a.py:16: error: Revealed type is 'builtins.int' -- --------------------------- [case test_validators] -# cmd: mypy a.py -[file a.py] import attr from attr.validators import in_, and_, instance_of @@ -188,14 +138,10 @@ a = attr.ib(type=int, validator=in_([1, 2, 3])) b = attr.ib(type=int, validator=[in_([1, 2, 3]), instance_of(int)]) c = attr.ib(type=int, validator=(in_([1, 2, 3]), instance_of(int))) d = attr.ib(type=int, validator=and_(in_([1, 2, 3]), instance_of(int))) -e = attr.ib(type=int, validator=1) # error +e = attr.ib(type=int, validator=1) # E: No overload variant matches argument types [Overload(def (x: Union[builtins.str, builtins.bytes, typing.SupportsInt] =) -> builtins.int, def (x: Union[builtins.str, builtins.bytes], base: builtins.int) -> builtins.int), builtins.int] -[out] -a.py:8: error: No overload variant matches argument types [Overload(def (x: Union[builtins.str, builtins.bytes, typing.SupportsInt] =) -> builtins.int, def (x: Union[builtins.str, builtins.bytes], base: builtins.int) -> builtins.int), builtins.int] [case test_custom_validators] -# cmd: mypy a.py -[file a.py] import attr def validate_int(inst, at, val: int): @@ -205,62 +151,43 @@ def validate_str(inst, at, val: str): pass a = attr.ib(type=int, validator=validate_int) # int -b = attr.ib(type=int, validator=validate_str) # error +b = attr.ib(type=int, validator=validate_str) # E: Argument 2 has incompatible type "Callable[[Any, Any, str], Any]"; expected "Union[Callable[[Any, Attribute[Any], int], Any], List[Callable[[Any, Attribute[Any], int], Any]], Tuple[Callable[[Any, Attribute[Any], int], Any], ...]]" -reveal_type(a) +reveal_type(a) # E: Revealed type is 'builtins.int' -[out] -a.py:10: error: Argument 2 has incompatible type "Callable[[Any, Any, str], Any]"; expected "Union[Callable[[Any, Attribute[Any], int], Any], List[Callable[[Any, Attribute[Any], int], Any]], Tuple[Callable[[Any, Attribute[Any], int], Any], ...]]" -a.py:12: error: Revealed type is 'builtins.int' -- --------------------------- -- Make -- --------------------------- [case test_make_from_dict] -# cmd: mypy a.py -[file a.py] import attr C = attr.make_class("C", { "x": attr.ib(type=int), "y": attr.ib() }) -[out] [case test_make_from_str] -# cmd: mypy a.py -[file a.py] import attr C = attr.make_class("C", ["x", "y"]) -[out] [case test_astuple] -# cmd: mypy a.py -[file a.py] import attr @attr.s class C: a: int = attr.ib() t1 = attr.astuple(C) -reveal_type(t1) - -[out] -a.py:7: error: Revealed type is 'builtins.tuple[Any]' +reveal_type(t1) # E: Revealed type is 'builtins.tuple[Any]' [case test_asdict] -# cmd: mypy a.py -[file a.py] import attr @attr.s class C: a: int = attr.ib() t1 = attr.asdict(C) -reveal_type(t1) - -[out] -a.py:7: error: Revealed type is 'builtins.dict[builtins.str, Any]' +reveal_type(t1) # E: Revealed type is 'builtins.dict[builtins.str, Any]' From 7e03c2fe75ca29e0b57efa5568094f2f0dd4184e Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Fri, 29 Dec 2017 10:05:19 -0800 Subject: [PATCH 30/64] More tests --- tests/test_stubs.test | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/test_stubs.test b/tests/test_stubs.test index 2971c0670..26a7289e9 100644 --- a/tests/test_stubs.test +++ b/tests/test_stubs.test @@ -27,6 +27,11 @@ c = C(1) reveal_type(c.a) # E: Revealed type is 'builtins.int*' reveal_type(C.a) # E: Revealed type is 'builtins.int*' +C("1") # E: Argument 1 to "C" has incompatible type "str"; expected "int" +C(a="1") # E: Argument 1 to "C" has incompatible type "str"; expected "int" +C(None) +C(a=None) +C(a=1) [case test_type_annotations] import attr @@ -133,12 +138,37 @@ reveal_type(d) # E: Revealed type is 'builtins.int' [case test_validators] import attr from attr.validators import in_, and_, instance_of +import enum + +class State(enum.Enum): + ON = "on" + OFF = "off" a = attr.ib(type=int, validator=in_([1, 2, 3])) +aa = attr.ib(validator=in_([1, 2, 3])) b = attr.ib(type=int, validator=[in_([1, 2, 3]), instance_of(int)]) c = attr.ib(type=int, validator=(in_([1, 2, 3]), instance_of(int))) d = attr.ib(type=int, validator=and_(in_([1, 2, 3]), instance_of(int))) e = attr.ib(type=int, validator=1) # E: No overload variant matches argument types [Overload(def (x: Union[builtins.str, builtins.bytes, typing.SupportsInt] =) -> builtins.int, def (x: Union[builtins.str, builtins.bytes], base: builtins.int) -> builtins.int), builtins.int] +f = attr.ib(type=State, validator=in_(State)) +# mypy does not know how to get the contained type from an enum: +ff = attr.ib(validator=in_(State)) # E: Need type annotation for variable + + +[case test_init_with_validators] +import attr +from attr.validators import instance_of + +@attr.s +class C: + x = attr.ib(validator=instance_of(int)) + +reveal_type(C.x) # E: Revealed type is 'builtins.int*' + +C(42) +C(x=42) +C("42") # E: Argument 1 to "C" has incompatible type "str"; expected "int" +C(None) [case test_custom_validators] From 229307047111c95e6dcaa24fad144e3d7aa9c984 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sun, 31 Dec 2017 09:55:47 -0800 Subject: [PATCH 31/64] Tests passing (with notes about remaining issues) --- docs/api.rst | 2 +- docs/doctest2.py | 17 +++- stubs-requirements.txt | 2 +- tests/test_stubs.py | 4 +- .../{test_stubs.test => test_stubs.tests.py} | 96 +++++++++++++++---- 5 files changed, 96 insertions(+), 25 deletions(-) rename tests/{test_stubs.test => test_stubs.tests.py} (65%) diff --git a/docs/api.rst b/docs/api.rst index c18327e72..413eae945 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -360,7 +360,7 @@ Validators ... OFF = "off" >>> @attr.s ... class C(object): - ... state = attr.ib(validator=attr.validators.in_(State)) + ... state = attr.ib(validator=attr.validators.in_(State)) # E: Need type annotation for variable ... val = attr.ib(validator=attr.validators.in_([1, 2, 3])) >>> C(State.ON, 1) C(state=, val=1) diff --git a/docs/doctest2.py b/docs/doctest2.py index f5f6607d2..d03003a9e 100644 --- a/docs/doctest2.py +++ b/docs/doctest2.py @@ -20,6 +20,19 @@ MAIN = 'main' + +doctest.register_optionflag('MYPY_ERROR') +doctest.register_optionflag('MYPY_IGNORE') + + +def convert_source(input): + for i in range(len(input)): + # FIXME: convert to regex + input[i] = input[i].replace('#doctest: +MYPY_ERROR', '') + input[i] = input[i].replace('#doctest: +MYPY_IGNORE', '# type: ignore') + return input + + import re # FIXME: pull this from mypy_test_plugin def expand_errors(input, output, fnam: str): @@ -84,7 +97,9 @@ def test_group(self, group, filename): self.cleanup_runner.group_source) want_lines = [] - expand_errors(source.splitlines(), want_lines, MAIN) + lines = convert_source(source.splitlines(keepends=True)) + expand_errors(lines, want_lines, MAIN) + source = ''.join(lines) want = '\n'.join(want_lines) + '\n' if want_lines else '' got = run_mypy(source, self.config.doctest_path) if want != got: diff --git a/stubs-requirements.txt b/stubs-requirements.txt index 547eaf77d..267ab1b6f 100644 --- a/stubs-requirements.txt +++ b/stubs-requirements.txt @@ -1,2 +1,2 @@ pytest -git+git://github.com/euresti/mypy.git@attrs_plugin#egg=mypy \ No newline at end of file +git+git://github.com/euresti/mypy.git@99bc87efffea07176d8da04963d2bda7bed0c85d#egg=mypy \ No newline at end of file diff --git a/tests/test_stubs.py b/tests/test_stubs.py index befe6b9e8..72d89962e 100644 --- a/tests/test_stubs.py +++ b/tests/test_stubs.py @@ -15,7 +15,7 @@ # Path to Python 3 interpreter python3_path = sys.executable test_temp_dir = 'tmp' -test_file = os.path.splitext(os.path.realpath(__file__))[0] + '.test' +test_file = os.path.splitext(os.path.realpath(__file__))[0] + '.tests.py' prefix_dir = os.path.join(os.path.dirname(os.path.dirname(test_file)), 'src') @@ -49,7 +49,7 @@ def _test_python_evaluation(testcase): stderr=subprocess.STDOUT, env={'MYPYPATH': prefix_dir}, cwd=test_temp_dir) - outb = process.stdout.read() + outb, errb = process.communicate() # Split output into lines. out = [s.rstrip('\n\r') for s in str(outb, 'utf8').splitlines()] # Remove temp file. diff --git a/tests/test_stubs.test b/tests/test_stubs.tests.py similarity index 65% rename from tests/test_stubs.test rename to tests/test_stubs.tests.py index 26a7289e9..20d2410b8 100644 --- a/tests/test_stubs.test +++ b/tests/test_stubs.tests.py @@ -17,7 +17,9 @@ class C: reveal_type(C.b) # E: Revealed type is 'Any' [case test_type_arg] +# cmd: mypy --strict-optional import attr +from typing import List @attr.s class C(object): @@ -27,14 +29,20 @@ class C(object): reveal_type(c.a) # E: Revealed type is 'builtins.int*' reveal_type(C.a) # E: Revealed type is 'builtins.int*' -C("1") # E: Argument 1 to "C" has incompatible type "str"; expected "int" -C(a="1") # E: Argument 1 to "C" has incompatible type "str"; expected "int" -C(None) -C(a=None) +C("1") # E: Argument 1 to "C" has incompatible type "str"; expected "int" +C(a="1") # E: Argument 1 to "C" has incompatible type "str"; expected "int" +C(None) # E: Argument 1 to "C" has incompatible type "None"; expected "int" +C(a=None) # E: Argument 1 to "C" has incompatible type "None"; expected "int" C(a=1) +a = attr.ib(type=List[int]) +reveal_type(a) # E: Revealed type is 'builtins.list*[builtins.int*]' + + [case test_type_annotations] +# cmd: mypy --strict-optional import attr +from typing import List @attr.s class C(object): @@ -44,6 +52,15 @@ class C(object): reveal_type(c.a) # E: Revealed type is 'builtins.int' reveal_type(C.a) # E: Revealed type is 'builtins.int' +C("1") # E: Argument 1 to "C" has incompatible type "str"; expected "int" +C(a="1") # E: Argument 1 to "C" has incompatible type "str"; expected "int" +C(None) # E: Argument 1 to "C" has incompatible type "None"; expected "int" +C(a=None) # E: Argument 1 to "C" has incompatible type "None"; expected "int" +C(a=1) + +a: List[int] = attr.ib() +reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' + -- --------------------------- -- Defaults @@ -82,7 +99,9 @@ class C(object): b: int = attr.ib(default=0) reveal_type(b) # E: Revealed type is 'builtins.int' -c: int = attr.ib(default=0, type=str) # E: Incompatible types in assignment (expression has type "object", variable has type "int") +c: int = attr.ib(default='bad') # E: Incompatible types in assignment (expression has type "str", variable has type "int") + +d: int = attr.ib(default=0, type=str) # E: Incompatible types in assignment (expression has type "object", variable has type "int") -- --------------------------- @@ -93,15 +112,14 @@ class C(object): import attr from typing import List -a = attr.ib(type=List[int]) -reveal_type(a) # E: Revealed type is 'builtins.list*[builtins.int*]' +a = attr.ib(default=attr.Factory(list)) +reveal_type(a) # E: Revealed type is 'builtins.list*[_T`1]' b = attr.ib(default=attr.Factory(list), type=List[int]) -# FIXME: shouldn't the `default` type be upgraded from `list` to `List[int]``? make a mypy github issue +# FIXME: shouldn't this be some form of list? Open mypy github issue reveal_type(b) # E: Revealed type is 'builtins.object*' c = attr.ib(default=attr.Factory(list), type=int) - # object, the common base of list and int: reveal_type(c) # E: Revealed type is 'builtins.object*' @@ -116,13 +134,13 @@ def int_factory() -> int: import attr from typing import List -a: List[int] = attr.ib() -reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' +a = attr.ib(default=attr.Factory(list)) +reveal_type(a) # E: Revealed type is 'builtins.list*[_T`1]' -b: List[int] = attr.ib(default=attr.Factory(list), type=List[int]) +b: List[int] = attr.ib(default=attr.Factory(list)) reveal_type(b) # E: Revealed type is 'builtins.list[builtins.int]' -c: List[int] = attr.ib(default=attr.Factory(list), type=str) # E: Argument 2 has incompatible type "Type[str]"; expected "Type[List[int]]" +c: int = attr.ib(default=attr.Factory(list)) # E: Incompatible types in assignment (expression has type "List[_T]", variable has type "int") def int_factory() -> int: return 0 @@ -144,14 +162,18 @@ class State(enum.Enum): ON = "on" OFF = "off" -a = attr.ib(type=int, validator=in_([1, 2, 3])) +a = attr.ib(type=int, validator=in_([1, 2, 3])) aa = attr.ib(validator=in_([1, 2, 3])) -b = attr.ib(type=int, validator=[in_([1, 2, 3]), instance_of(int)]) -c = attr.ib(type=int, validator=(in_([1, 2, 3]), instance_of(int))) -d = attr.ib(type=int, validator=and_(in_([1, 2, 3]), instance_of(int))) + +# multiple: +b = attr.ib(type=int, validator=[in_([1, 2, 3]), instance_of(int)]) +bb = attr.ib(type=int, validator=(in_([1, 2, 3]), instance_of(int))) +bbb = attr.ib(type=int, validator=and_(in_([1, 2, 3]), instance_of(int))) + e = attr.ib(type=int, validator=1) # E: No overload variant matches argument types [Overload(def (x: Union[builtins.str, builtins.bytes, typing.SupportsInt] =) -> builtins.int, def (x: Union[builtins.str, builtins.bytes], base: builtins.int) -> builtins.int), builtins.int] -f = attr.ib(type=State, validator=in_(State)) + # mypy does not know how to get the contained type from an enum: +f = attr.ib(type=State, validator=in_(State)) ff = attr.ib(validator=in_(State)) # E: Need type annotation for variable @@ -167,11 +189,13 @@ class C: C(42) C(x=42) -C("42") # E: Argument 1 to "C" has incompatible type "str"; expected "int" +# NOTE: even though the type of C.x is known to be int, the following is not an error. +# The mypy plugin that generates __init__ runs at semantic analysis time, but type inference (which handles TypeVars happens later) +C("42") C(None) -[case test_custom_validators] +[case test_custom_validators_type_arg] import attr def validate_int(inst, at, val: int): @@ -186,6 +210,38 @@ def validate_str(inst, at, val: str): reveal_type(a) # E: Revealed type is 'builtins.int' +[case test_custom_validators_type_annotations] +import attr + +def validate_int(inst, at, val: int): + pass + +def validate_str(inst, at, val: str): + pass + +a: int = attr.ib(validator=validate_int) +b: int = attr.ib(validator=validate_str) # E: Incompatible types in assignment (expression has type "str", variable has type "int") + +reveal_type(a) # E: Revealed type is 'builtins.int' + +-- --------------------------- +-- Converters +-- --------------------------- + +[case test_converters] +import attr + +def str_to_int(s: str) -> int: + return int(s) + +@attr.s +class C: + x: int = attr.ib(convert=str_to_int) + +C(1) +# FIXME: this should not be an error +# C('1') + -- --------------------------- -- Make -- --------------------------- From 5ba0539e538716134e8cd90788e28f8e7a49f242 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sun, 31 Dec 2017 10:24:43 -0800 Subject: [PATCH 32/64] Attempt to get latest plugin from euresti working --- src/attr/__init__.pyi | 2 +- stubs-requirements.txt | 2 +- tests/test_stubs.tests.py | 13 +++++++------ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 82ff891be..c7abe047a 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -52,7 +52,7 @@ class Attribute(Generic[_T]): def attrib(default: _T, validator: Optional[_ValidatorArgType[_T]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: _ConverterType[_T] = ..., metadata: Mapping = ..., - type: Type[_T] = ...) -> _T: ... + type: type = ...) -> _T: ... @overload def attrib(default: None = ..., validator: Optional[_ValidatorArgType[_T]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., diff --git a/stubs-requirements.txt b/stubs-requirements.txt index 267ab1b6f..547eaf77d 100644 --- a/stubs-requirements.txt +++ b/stubs-requirements.txt @@ -1,2 +1,2 @@ pytest -git+git://github.com/euresti/mypy.git@99bc87efffea07176d8da04963d2bda7bed0c85d#egg=mypy \ No newline at end of file +git+git://github.com/euresti/mypy.git@attrs_plugin#egg=mypy \ No newline at end of file diff --git a/tests/test_stubs.tests.py b/tests/test_stubs.tests.py index 20d2410b8..3d1fd563e 100644 --- a/tests/test_stubs.tests.py +++ b/tests/test_stubs.tests.py @@ -86,7 +86,7 @@ class C(object): reveal_type(b) # E: Revealed type is 'builtins.int*' c = attr.ib(default='bad', type=int) -# object, the common base of str and int: +# FXIME: this is now str. should be error in line above. reveal_type(c) # E: Revealed type is 'builtins.object*' @@ -101,7 +101,8 @@ class C(object): c: int = attr.ib(default='bad') # E: Incompatible types in assignment (expression has type "str", variable has type "int") -d: int = attr.ib(default=0, type=str) # E: Incompatible types in assignment (expression has type "object", variable has type "int") +# type arg is ignored. should be error? +d: int = attr.ib(default=0, type=str) -- --------------------------- @@ -116,12 +117,12 @@ class C(object): reveal_type(a) # E: Revealed type is 'builtins.list*[_T`1]' b = attr.ib(default=attr.Factory(list), type=List[int]) -# FIXME: shouldn't this be some form of list? Open mypy github issue -reveal_type(b) # E: Revealed type is 'builtins.object*' +# FIXME: should be List[int] +reveal_type(b) # E: Revealed type is 'builtins.list*[_T`1]' c = attr.ib(default=attr.Factory(list), type=int) -# object, the common base of list and int: -reveal_type(c) # E: Revealed type is 'builtins.object*' +# FIXME: should be int, and error should be generated above +reveal_type(c) # E: Revealed type is 'builtins.list*[_T`1]' def int_factory() -> int: return 0 From 70e108f48a6b9f19c71940d5039eaa7491874e70 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sun, 31 Dec 2017 12:11:08 -0800 Subject: [PATCH 33/64] Issues fixed. Had to place calls to attr.ib under a class definition. --- src/attr/__init__.pyi | 2 +- tests/test_stubs.tests.py | 120 +++++++++++++++++++++----------------- 2 files changed, 66 insertions(+), 56 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index c7abe047a..82ff891be 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -52,7 +52,7 @@ class Attribute(Generic[_T]): def attrib(default: _T, validator: Optional[_ValidatorArgType[_T]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., convert: _ConverterType[_T] = ..., metadata: Mapping = ..., - type: type = ...) -> _T: ... + type: Type[_T] = ...) -> _T: ... @overload def attrib(default: None = ..., validator: Optional[_ValidatorArgType[_T]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., diff --git a/tests/test_stubs.tests.py b/tests/test_stubs.tests.py index 3d1fd563e..03f05f16e 100644 --- a/tests/test_stubs.tests.py +++ b/tests/test_stubs.tests.py @@ -2,6 +2,9 @@ -- Basics -- --------------------------- +-- Note: the plugin only affects calls to attr.ib if they are within a class definition + + [case test_no_type] import attr @@ -22,12 +25,12 @@ class C: from typing import List @attr.s -class C(object): +class C: a = attr.ib(type=int) c = C(1) -reveal_type(c.a) # E: Revealed type is 'builtins.int*' -reveal_type(C.a) # E: Revealed type is 'builtins.int*' +reveal_type(c.a) # E: Revealed type is 'builtins.int' +reveal_type(C.a) # E: Revealed type is 'builtins.int' C("1") # E: Argument 1 to "C" has incompatible type "str"; expected "int" C(a="1") # E: Argument 1 to "C" has incompatible type "str"; expected "int" @@ -45,7 +48,7 @@ class C(object): from typing import List @attr.s -class C(object): +class C: a : int = attr.ib() c = C(1) @@ -69,40 +72,43 @@ class C(object): [case test_defaults_no_type] import attr -a = attr.ib(default=0) -reveal_type(a) # E: Revealed type is 'builtins.int*' +@attr.s +class C: + a = attr.ib(default=0) + reveal_type(a) # E: Revealed type is 'builtins.int*' -b = attr.ib(0) -reveal_type(b) # E: Revealed type is 'builtins.int*' + b = attr.ib(0) + reveal_type(b) # E: Revealed type is 'builtins.int*' [case test_defaults_type_arg] import attr -a = attr.ib(type=int) -reveal_type(a) # E: Revealed type is 'builtins.int*' +@attr.s +class C: + a = attr.ib(type=int) + reveal_type(a) # E: Revealed type is 'builtins.int' -b = attr.ib(default=0, type=int) -reveal_type(b) # E: Revealed type is 'builtins.int*' + b = attr.ib(default=0, type=int) + reveal_type(b) # E: Revealed type is 'builtins.int' -c = attr.ib(default='bad', type=int) -# FXIME: this is now str. should be error in line above. -reveal_type(c) # E: Revealed type is 'builtins.object*' + c = attr.ib(default='bad', type=int) # E: Incompatible types in assignment (expression has type "object", variable has type "int") [case test_defaults_type_annotations] import attr -a: int = attr.ib() -reveal_type(a) # E: Revealed type is 'builtins.int' +@attr.s +class C: + a: int = attr.ib() + reveal_type(a) # E: Revealed type is 'builtins.int' -b: int = attr.ib(default=0) -reveal_type(b) # E: Revealed type is 'builtins.int' + b: int = attr.ib(default=0) + reveal_type(b) # E: Revealed type is 'builtins.int' -c: int = attr.ib(default='bad') # E: Incompatible types in assignment (expression has type "str", variable has type "int") + c: int = attr.ib(default='bad') # E: Incompatible types in assignment (expression has type "str", variable has type "int") -# type arg is ignored. should be error? -d: int = attr.ib(default=0, type=str) + d: int = attr.ib(default=0, type=str) # E: Incompatible types in assignment (expression has type "object", variable has type "int") -- --------------------------- @@ -113,41 +119,44 @@ class C(object): import attr from typing import List -a = attr.ib(default=attr.Factory(list)) -reveal_type(a) # E: Revealed type is 'builtins.list*[_T`1]' +def int_factory() -> int: + return 0 + -b = attr.ib(default=attr.Factory(list), type=List[int]) -# FIXME: should be List[int] -reveal_type(b) # E: Revealed type is 'builtins.list*[_T`1]' +@attr.s +class C: + a = attr.ib(default=attr.Factory(list)) + reveal_type(a) # E: Revealed type is 'builtins.list*[_T`1]' -c = attr.ib(default=attr.Factory(list), type=int) -# FIXME: should be int, and error should be generated above -reveal_type(c) # E: Revealed type is 'builtins.list*[_T`1]' + b = attr.ib(default=attr.Factory(list), type=List[int]) + reveal_type(b) # E: Revealed type is 'builtins.list[builtins.int]' -def int_factory() -> int: - return 0 + c = attr.ib(default=attr.Factory(list), type=int) # E: Incompatible types in assignment (expression has type "object", variable has type "int") -d = attr.ib(default=attr.Factory(int_factory), type=int) -reveal_type(d) # E: Revealed type is 'builtins.int*' + d = attr.ib(default=attr.Factory(int_factory), type=int) + reveal_type(d) # E: Revealed type is 'builtins.int' [case test_factory_defaults_type_annotations] import attr from typing import List -a = attr.ib(default=attr.Factory(list)) -reveal_type(a) # E: Revealed type is 'builtins.list*[_T`1]' - -b: List[int] = attr.ib(default=attr.Factory(list)) -reveal_type(b) # E: Revealed type is 'builtins.list[builtins.int]' - -c: int = attr.ib(default=attr.Factory(list)) # E: Incompatible types in assignment (expression has type "List[_T]", variable has type "int") def int_factory() -> int: return 0 -d: int = attr.ib(default=attr.Factory(int_factory)) -reveal_type(d) # E: Revealed type is 'builtins.int' +@attr.s +class C: + a = attr.ib(default=attr.Factory(list)) + reveal_type(a) # E: Revealed type is 'builtins.list*[_T`1]' + + b: List[int] = attr.ib(default=attr.Factory(list)) + reveal_type(b) # E: Revealed type is 'builtins.list[builtins.int]' + + c: int = attr.ib(default=attr.Factory(list)) # E: Incompatible types in assignment (expression has type "List[_T]", variable has type "int") + + d: int = attr.ib(default=attr.Factory(int_factory)) + reveal_type(d) # E: Revealed type is 'builtins.int' -- --------------------------- @@ -163,19 +172,21 @@ class State(enum.Enum): ON = "on" OFF = "off" -a = attr.ib(type=int, validator=in_([1, 2, 3])) -aa = attr.ib(validator=in_([1, 2, 3])) +@attr.s +class C: + a = attr.ib(type=int, validator=in_([1, 2, 3])) + aa = attr.ib(validator=in_([1, 2, 3])) -# multiple: -b = attr.ib(type=int, validator=[in_([1, 2, 3]), instance_of(int)]) -bb = attr.ib(type=int, validator=(in_([1, 2, 3]), instance_of(int))) -bbb = attr.ib(type=int, validator=and_(in_([1, 2, 3]), instance_of(int))) + # multiple: + b = attr.ib(type=int, validator=[in_([1, 2, 3]), instance_of(int)]) + bb = attr.ib(type=int, validator=(in_([1, 2, 3]), instance_of(int))) + bbb = attr.ib(type=int, validator=and_(in_([1, 2, 3]), instance_of(int))) -e = attr.ib(type=int, validator=1) # E: No overload variant matches argument types [Overload(def (x: Union[builtins.str, builtins.bytes, typing.SupportsInt] =) -> builtins.int, def (x: Union[builtins.str, builtins.bytes], base: builtins.int) -> builtins.int), builtins.int] + e = attr.ib(type=int, validator=1) # E: No overload variant matches argument types [Overload(def (x: Union[builtins.str, builtins.bytes, typing.SupportsInt] =) -> builtins.int, def (x: Union[builtins.str, builtins.bytes], base: builtins.int) -> builtins.int), builtins.int] -# mypy does not know how to get the contained type from an enum: -f = attr.ib(type=State, validator=in_(State)) -ff = attr.ib(validator=in_(State)) # E: Need type annotation for variable + # mypy does not know how to get the contained type from an enum: + f = attr.ib(type=State, validator=in_(State)) + ff = attr.ib(validator=in_(State)) # E: Need type annotation for variable [case test_init_with_validators] @@ -240,8 +251,7 @@ class C: x: int = attr.ib(convert=str_to_int) C(1) -# FIXME: this should not be an error -# C('1') +C('1') -- --------------------------- -- Make From c69ff428428266070baf5d4484c763c493483f5b Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sun, 31 Dec 2017 15:52:26 -0800 Subject: [PATCH 34/64] Deal with a PyCharm bug --- src/attr/__init__.pyi | 146 +++++++++++++++++++++++++++++++++++------- 1 file changed, 124 insertions(+), 22 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 82ff891be..6dc67cae4 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -49,27 +49,40 @@ class Attribute(Generic[_T]): # # 1st form catches a default value set. Can't use = ... or you get "overloaded overlap" error. @overload -def attrib(default: _T, validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: _ConverterType[_T] = ..., metadata: Mapping = ..., - type: Type[_T] = ...) -> _T: ... -@overload -def attrib(default: None = ..., validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., metadata: Mapping = ..., - type: Optional[Type[_T]] = ...) -> _T: ... +def attrib( + default: _T, validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., + init: bool = ..., convert: _ConverterType[_T] = ..., + metadata: Mapping = ..., type: Type[_T] = ...) -> _T: ... + +@overload +def attrib( + default: None = ..., validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., + init: bool = ..., convert: Optional[_ConverterType[_T]] = ..., + metadata: Mapping = ..., type: Optional[Type[_T]] = ...) -> _T: ... + # 3rd form catches nothing set. So returns Any. @overload -def attrib(default: None = ..., validator: None = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: None = ..., metadata: Mapping = ..., - type: None = ...) -> Any: ... +def attrib( + default: None = ..., validator: None = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., + init: bool = ..., convert: None = ..., metadata: Mapping = ..., + type: None = ...) -> Any: ... @overload -def attrs(maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> _C: ... +def attrs( + maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., + hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., + frozen: bool = ..., str: bool = ...) -> _C: ... @overload -def attrs(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... +def attrs( + maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., + hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., + frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... # TODO: add support for returning NamedTuple from the mypy plugin @@ -80,15 +93,21 @@ def fields(cls: type) -> _Fields: ... def validate(inst: Any) -> None: ... # we use Any instead of _CountingAttr so that e.g. `make_class('Foo', [attr.ib()])` is valid -def make_class(name, attrs: Union[List[str], Dict[str, Any]], bases: Tuple[type, ...] = ..., **attributes_arguments) -> type: ... +def make_class(name, attrs: Union[List[str], Dict[str, Any]], + bases: Tuple[type, ...] = ..., + **attributes_arguments) -> type: ... # _funcs -- # FIXME: asdict/astuple do not honor their factory args. waiting on one of these: # https://github.com/python/mypy/issues/4236 # https://github.com/python/typing/issues/253 -def asdict(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[Mapping] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... -def astuple(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[Sequence] = ..., retain_collection_types: bool = ...) -> Tuple[Any, ...]: ... +def asdict(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., + dict_factory: Type[Mapping] = ..., + retain_collection_types: bool = ...) -> Dict[str, Any]: ... +def astuple(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., + tuple_factory: Type[Sequence] = ..., + retain_collection_types: bool = ...) -> Tuple[Any, ...]: ... def has(cls: type) -> bool: ... def assoc(inst: _T, **changes: Any) -> _T: ... @@ -99,7 +118,90 @@ def evolve(inst: _T, **changes: Any) -> _T: ... def set_run_validators(run: bool) -> None: ... def get_run_validators() -> bool: ... -# aliases -s = attributes = attrs -ib = attr = attrib -dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) +# aliases -- +#s = attributes = attrs +#ib = attr = attrib +#dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) + +# FIXME: there is a bug in PyCharm with creating aliases to overloads. +# Remove these when the bug is fixed: +# https://youtrack.jetbrains.com/issue/PY-27788 + +@overload +def ib( + default: _T, validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., + init: bool = ..., convert: _ConverterType[_T] = ..., + metadata: Mapping = ..., type: Type[_T] = ...) -> _T: ... +@overload +def ib( + default: None = ..., validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., + init: bool = ..., convert: Optional[_ConverterType[_T]] = ..., + metadata: Mapping = ..., type: Optional[Type[_T]] = ...) -> _T: ... +@overload +def ib( + default: None = ..., validator: None = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., + init: bool = ..., convert: None = ..., metadata: Mapping = ..., + type: None = ...) -> Any: ... + +@overload +def attr( + default: _T, validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., + init: bool = ..., convert: _ConverterType[_T] = ..., + metadata: Mapping = ..., type: Type[_T] = ...) -> _T: ... +@overload +def attr( + default: None = ..., validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., + init: bool = ..., convert: Optional[_ConverterType[_T]] = ..., + metadata: Mapping = ..., type: Optional[Type[_T]] = ...) -> _T: ... +@overload +def attr( + default: None = ..., validator: None = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., + init: bool = ..., convert: None = ..., metadata: Mapping = ..., + type: None = ...) -> Any: ... + + +@overload +def attributes( + maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., + hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., + frozen: bool = ..., str: bool = ...) -> _C: ... +@overload +def attributes( + maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., + hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., + frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... + +@overload +def s( + maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., + hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., + frozen: bool = ..., str: bool = ...) -> _C: ... +@overload +def s( + maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., + hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., + frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... + +@overload +def dataclass( + maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., + hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., + frozen: bool = ..., str: bool = ...) -> _C: ... +@overload +def dataclass( + maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., + hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., + frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... +# -- \ No newline at end of file From c9a975ad67674b4f748c82a2f627bcb201ac936d Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sun, 31 Dec 2017 15:52:43 -0800 Subject: [PATCH 35/64] Minor test improvements --- tests/mypy_pytest_plugin.py | 2 +- tests/test_stubs.tests.py | 32 +++++++++++++++++++------------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/tests/mypy_pytest_plugin.py b/tests/mypy_pytest_plugin.py index 9f0567518..55223d6cc 100644 --- a/tests/mypy_pytest_plugin.py +++ b/tests/mypy_pytest_plugin.py @@ -791,7 +791,7 @@ def expand_errors(input: List[str], output: List[str], fnam: str) -> None: # The first in the split things isn't a comment for possible_err_comment in input[i].split(' # ')[1:]: m = re.search( - '^([ENW]):((?P\d+):)? (?P.*)$', + r'^([ENW]):((?P\d+):)? (?P.*)$', possible_err_comment.strip()) if m: if m.group(1) == 'E': diff --git a/tests/test_stubs.tests.py b/tests/test_stubs.tests.py index 03f05f16e..1c55690c7 100644 --- a/tests/test_stubs.tests.py +++ b/tests/test_stubs.tests.py @@ -38,8 +38,10 @@ class C: C(a=None) # E: Argument 1 to "C" has incompatible type "None"; expected "int" C(a=1) -a = attr.ib(type=List[int]) -reveal_type(a) # E: Revealed type is 'builtins.list*[builtins.int*]' +@attr.s +class D: + a = attr.ib(type=List[int]) + reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' [case test_type_annotations] @@ -61,8 +63,10 @@ class C: C(a=None) # E: Argument 1 to "C" has incompatible type "None"; expected "int" C(a=1) -a: List[int] = attr.ib() -reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' +@attr.s +class D: + a: List[int] = attr.ib() + reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' -- --------------------------- @@ -122,7 +126,6 @@ class C: def int_factory() -> int: return 0 - @attr.s class C: a = attr.ib(default=attr.Factory(list)) @@ -182,7 +185,7 @@ class C: bb = attr.ib(type=int, validator=(in_([1, 2, 3]), instance_of(int))) bbb = attr.ib(type=int, validator=and_(in_([1, 2, 3]), instance_of(int))) - e = attr.ib(type=int, validator=1) # E: No overload variant matches argument types [Overload(def (x: Union[builtins.str, builtins.bytes, typing.SupportsInt] =) -> builtins.int, def (x: Union[builtins.str, builtins.bytes], base: builtins.int) -> builtins.int), builtins.int] + e = attr.ib(type=int, validator=1) # E: No overload variant of "ib" matches argument types [Overload(def (x: Union[builtins.str, builtins.bytes, typing.SupportsInt] =) -> builtins.int, def (x: Union[builtins.str, builtins.bytes], base: builtins.int) -> builtins.int), builtins.int] # mypy does not know how to get the contained type from an enum: f = attr.ib(type=State, validator=in_(State)) @@ -204,7 +207,6 @@ class C: # NOTE: even though the type of C.x is known to be int, the following is not an error. # The mypy plugin that generates __init__ runs at semantic analysis time, but type inference (which handles TypeVars happens later) C("42") -C(None) [case test_custom_validators_type_arg] @@ -216,10 +218,12 @@ def validate_int(inst, at, val: int): def validate_str(inst, at, val: str): pass -a = attr.ib(type=int, validator=validate_int) # int -b = attr.ib(type=int, validator=validate_str) # E: Argument 2 has incompatible type "Callable[[Any, Any, str], Any]"; expected "Union[Callable[[Any, Attribute[Any], int], Any], List[Callable[[Any, Attribute[Any], int], Any]], Tuple[Callable[[Any, Attribute[Any], int], Any], ...]]" +@attr.s +class C: + a = attr.ib(type=int, validator=validate_int) # int + b = attr.ib(type=int, validator=validate_str) # E: Argument 2 to "ib" has incompatible type "Callable[[Any, Any, str], Any]"; expected "Union[Callable[[Any, Attribute[Any], int], Any], List[Callable[[Any, Attribute[Any], int], Any]], Tuple[Callable[[Any, Attribute[Any], int], Any], ...]]" -reveal_type(a) # E: Revealed type is 'builtins.int' + reveal_type(a) # E: Revealed type is 'builtins.int' [case test_custom_validators_type_annotations] @@ -231,10 +235,12 @@ def validate_int(inst, at, val: int): def validate_str(inst, at, val: str): pass -a: int = attr.ib(validator=validate_int) -b: int = attr.ib(validator=validate_str) # E: Incompatible types in assignment (expression has type "str", variable has type "int") +@attr.s +class C: + a: int = attr.ib(validator=validate_int) + b: int = attr.ib(validator=validate_str) # E: Incompatible types in assignment (expression has type "str", variable has type "int") -reveal_type(a) # E: Revealed type is 'builtins.int' + reveal_type(a) # E: Revealed type is 'builtins.int' -- --------------------------- -- Converters From a900a74f202ac59f943ac9c5479ccc444b2f08a0 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sun, 31 Dec 2017 17:49:44 -0800 Subject: [PATCH 36/64] Make tests prettier --- tests/mypy_pytest_plugin.py | 14 ++--- tests/test_stubs.tests.py | 112 ++++++++++++++++++++++++------------ 2 files changed, 81 insertions(+), 45 deletions(-) diff --git a/tests/mypy_pytest_plugin.py b/tests/mypy_pytest_plugin.py index 55223d6cc..4ae1d646d 100644 --- a/tests/mypy_pytest_plugin.py +++ b/tests/mypy_pytest_plugin.py @@ -698,24 +698,24 @@ def parse_test_data(l: List[str], fnam: str) -> List[TestItem]: while i < len(l): s = l[i].strip() - if l[i].startswith('[') and s.endswith(']') and not s.startswith('[['): + if l[i].startswith('# [') and s.endswith(']') and not s.startswith('# [['): if id: data = collapse_line_continuation(data) data = strip_list(data) ret.append(TestItem(id, arg, strip_list(data), fnam, i0 + 1)) i0 = i - id = s[1:-1] + id = s[3:-1] arg = None if ' ' in id: arg = id[id.index(' ') + 1:] id = id[:id.index(' ')] data = [] - elif l[i].startswith('[['): - data.append(l[i][1:]) - elif not l[i].startswith('--'): + # elif l[i].startswith('# [['): + # data.append(l[i][3:]) + elif not l[i].startswith('# :'): data.append(l[i]) - elif l[i].startswith('----'): - data.append(l[i][2:]) + # elif l[i].startswith('----'): + # data.append(l[i][2:]) i += 1 # Process the last item. diff --git a/tests/test_stubs.tests.py b/tests/test_stubs.tests.py index 1c55690c7..5ba43e961 100644 --- a/tests/test_stubs.tests.py +++ b/tests/test_stubs.tests.py @@ -1,11 +1,13 @@ --- --------------------------- --- Basics --- --------------------------- +# :--------------------------- +# :Basics +# :--------------------------- --- Note: the plugin only affects calls to attr.ib if they are within a class definition +# :Note: the attrs plugin for mypy only affects calls to attr.ib if they are +# :within a class definition, so we include a class in each test. -[case test_no_type] +# [case test_no_type] +# :------------------ import attr @attr.s @@ -19,7 +21,9 @@ class C: reveal_type(c.b) # E: Revealed type is 'Any' reveal_type(C.b) # E: Revealed type is 'Any' -[case test_type_arg] + +# [case test_type_arg] +# :------------------- # cmd: mypy --strict-optional import attr from typing import List @@ -44,7 +48,8 @@ class D: reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' -[case test_type_annotations] +# [case test_type_annotations] +# :--------------------------- # cmd: mypy --strict-optional import attr from typing import List @@ -69,11 +74,12 @@ class D: reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' --- --------------------------- --- Defaults --- --------------------------- +# :--------------------------- +# :Defaults +# :--------------------------- -[case test_defaults_no_type] +# [case test_defaults_no_type] +# :---------------------------- import attr @attr.s @@ -85,7 +91,8 @@ class C: reveal_type(b) # E: Revealed type is 'builtins.int*' -[case test_defaults_type_arg] +# [case test_defaults_type_arg] +# :---------------------------- import attr @attr.s @@ -99,7 +106,8 @@ class C: c = attr.ib(default='bad', type=int) # E: Incompatible types in assignment (expression has type "object", variable has type "int") -[case test_defaults_type_annotations] +# [case test_defaults_type_annotations] +# :------------------------------------ import attr @attr.s @@ -115,11 +123,12 @@ class C: d: int = attr.ib(default=0, type=str) # E: Incompatible types in assignment (expression has type "object", variable has type "int") --- --------------------------- --- Factory Defaults --- --------------------------- +# :--------------------------- +# :Factory Defaults +# :--------------------------- -[case test_factory_defaults_type_arg] +# [case test_factory_defaults_type_arg] +# :------------------------------------ import attr from typing import List @@ -140,7 +149,8 @@ class C: reveal_type(d) # E: Revealed type is 'builtins.int' -[case test_factory_defaults_type_annotations] +# [case test_factory_defaults_type_annotations] +# :-------------------------------------------- import attr from typing import List @@ -162,11 +172,12 @@ class C: reveal_type(d) # E: Revealed type is 'builtins.int' --- --------------------------- --- Validators --- --------------------------- +# :--------------------------- +# :Validators +# :--------------------------- -[case test_validators] +# [case test_validators] +# :--------------------- import attr from attr.validators import in_, and_, instance_of import enum @@ -192,7 +203,7 @@ class C: ff = attr.ib(validator=in_(State)) # E: Need type annotation for variable -[case test_init_with_validators] +# [case test_init_with_validators] import attr from attr.validators import instance_of @@ -209,7 +220,8 @@ class C: C("42") -[case test_custom_validators_type_arg] +# [case test_custom_validators_type_arg] +# :------------------------------------- import attr def validate_int(inst, at, val: int): @@ -223,10 +235,11 @@ class C: a = attr.ib(type=int, validator=validate_int) # int b = attr.ib(type=int, validator=validate_str) # E: Argument 2 to "ib" has incompatible type "Callable[[Any, Any, str], Any]"; expected "Union[Callable[[Any, Attribute[Any], int], Any], List[Callable[[Any, Attribute[Any], int], Any]], Tuple[Callable[[Any, Attribute[Any], int], Any], ...]]" - reveal_type(a) # E: Revealed type is 'builtins.int' + reveal_type(a) # E: Revealed type is 'builtins.int' -[case test_custom_validators_type_annotations] +# [case test_custom_validators_type_annotations] +# :--------------------------------------------- import attr def validate_int(inst, at, val: int): @@ -242,14 +255,32 @@ class C: reveal_type(a) # E: Revealed type is 'builtins.int' --- --------------------------- --- Converters --- --------------------------- +# :--------------------------- +# :Converters +# :--------------------------- + +# [case test_converters] +# :--------------------- +import attr +from typing import Union + +def str_to_int(s: Union[str, int]) -> int: + return int(s) + +@attr.s +class C: + a = attr.ib(convert=str_to_int) + reveal_type(a) # E: Revealed type is 'builtins.int*' + + b: str = attr.ib(convert=str_to_int) # E: Incompatible types in assignment (expression has type "int", variable has type "str") + -[case test_converters] +# [case test_converter_init] +# :------------------------- import attr +from typing import Union -def str_to_int(s: str) -> int: +def str_to_int(s: Union[str, int]) -> int: return int(s) @attr.s @@ -258,12 +289,14 @@ class C: C(1) C('1') +C(1.1) # E: Argument 1 to "C" has incompatible type "float"; expected "Union[str, int]" --- --------------------------- --- Make --- --------------------------- +# :--------------------------- +# :Make +# :--------------------------- -[case test_make_from_dict] +# [case test_make_from_dict] +# :------------------------- import attr C = attr.make_class("C", { "x": attr.ib(type=int), @@ -271,12 +304,14 @@ class C: }) -[case test_make_from_str] +# [case test_make_from_str] +# :------------------------ import attr C = attr.make_class("C", ["x", "y"]) -[case test_astuple] +# [case test_astuple] +# :------------------ import attr @attr.s class C: @@ -286,7 +321,8 @@ class C: reveal_type(t1) # E: Revealed type is 'builtins.tuple[Any]' -[case test_asdict] +# [case test_asdict] +# :----------------- import attr @attr.s class C: From cc4bbdcb2af36b89ca04fa5254422ff1772bee38 Mon Sep 17 00:00:00 2001 From: David Euresti Date: Mon, 1 Jan 2018 10:23:03 -0800 Subject: [PATCH 37/64] Use 2 decorators instead of 3 --- src/attr/__init__.pyi | 84 ++++++++++++++++--------------------------- 1 file changed, 30 insertions(+), 54 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 6dc67cae4..a5c0dd137 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -47,30 +47,21 @@ class Attribute(Generic[_T]): # This makes this type of assignments possible: # x: int = attr(8) # -# 1st form catches a default value set. Can't use = ... or you get "overloaded overlap" error. +# Note: If you update these update `ib` and `attr` below. +# 1st form catches _T set. @overload -def attrib( - default: _T, validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., - init: bool = ..., convert: _ConverterType[_T] = ..., - metadata: Mapping = ..., type: Type[_T] = ...) -> _T: ... - -@overload -def attrib( - default: None = ..., validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., - init: bool = ..., convert: Optional[_ConverterType[_T]] = ..., - metadata: Mapping = ..., type: Optional[Type[_T]] = ...) -> _T: ... - -# 3rd form catches nothing set. So returns Any. +def attrib(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., + convert: Optional[_ConverterType[_T]] = ..., metadata: Mapping = ..., + type: Optional[Type[_T]] = ...) -> _T: ... +# 2nd form no _T , so returns Any. @overload -def attrib( - default: None = ..., validator: None = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., - init: bool = ..., convert: None = ..., metadata: Mapping = ..., - type: None = ...) -> Any: ... - +def attrib(default: None = ..., validator: None = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., + convert: None = ..., metadata: Mapping = ..., + type: None = ...) -> Any: ... +# Note: If you update these update `s` and `attributes` below. @overload def attrs( maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., @@ -128,42 +119,27 @@ def get_run_validators() -> bool: ... # https://youtrack.jetbrains.com/issue/PY-27788 @overload -def ib( - default: _T, validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., - init: bool = ..., convert: _ConverterType[_T] = ..., - metadata: Mapping = ..., type: Type[_T] = ...) -> _T: ... -@overload -def ib( - default: None = ..., validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., - init: bool = ..., convert: Optional[_ConverterType[_T]] = ..., - metadata: Mapping = ..., type: Optional[Type[_T]] = ...) -> _T: ... -@overload -def ib( - default: None = ..., validator: None = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., - init: bool = ..., convert: None = ..., metadata: Mapping = ..., - type: None = ...) -> Any: ... - +def ib(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., + convert: Optional[_ConverterType[_T]] = ..., metadata: Mapping = ..., + type: Optional[Type[_T]] = ...) -> _T: ... +# 2nd form catches no type-setters. So returns Any. @overload -def attr( - default: _T, validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., - init: bool = ..., convert: _ConverterType[_T] = ..., - metadata: Mapping = ..., type: Type[_T] = ...) -> _T: ... +def ib(default: None = ..., validator: None = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., + convert: None = ..., metadata: Mapping = ..., + type: None = ...) -> Any: ... @overload -def attr( - default: None = ..., validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., - init: bool = ..., convert: Optional[_ConverterType[_T]] = ..., - metadata: Mapping = ..., type: Optional[Type[_T]] = ...) -> _T: ... +def attr(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., + convert: Optional[_ConverterType[_T]] = ..., metadata: Mapping = ..., + type: Optional[Type[_T]] = ...) -> _T: ... +# 2nd form catches no type-setters. So returns Any. @overload -def attr( - default: None = ..., validator: None = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., - init: bool = ..., convert: None = ..., metadata: Mapping = ..., - type: None = ...) -> Any: ... +def attr(default: None = ..., validator: None = ..., + repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., + convert: None = ..., metadata: Mapping = ..., + type: None = ...) -> Any: ... @overload From 7fcd7c8cdb13a5222eea9c3c4ffedd3bb88e91ca Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Mon, 1 Jan 2018 15:16:29 -0800 Subject: [PATCH 38/64] doctest2: add support for skipping mypy tests --- docs/api.rst | 10 +++++++--- docs/doctest2.py | 38 +++++++++++++++++++++++++++++++------- docs/examples.rst | 9 +++++---- 3 files changed, 43 insertions(+), 14 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 413eae945..be01a4baf 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -36,12 +36,16 @@ Core ... _private = attr.ib() >>> C(private=42) C(_private=42) + + .. doctest:: core + :options: +MYPY_SKIP + >>> class D(object): ... def __init__(self, x): ... self.x = x >>> D(1) - >>> D = attr.s(these={"x": attr.ib()}, init=False)(D) # type: ignore # can't override a type + >>> D = attr.s(these={"x": attr.ib()}, init=False)(D) >>> D(1) D(x=1) @@ -305,7 +309,7 @@ See :ref:`asdict` for examples. ... class C(object): ... x = attr.ib(validator=attr.validators.instance_of(int)) >>> i = C(1) - >>> i.x = "1" # type: ignore # this is a legit error + >>> i.x = "1" # mypy error: Incompatible types in assignment (expression has type "str", variable has type "int") >>> attr.validate(i) Traceback (most recent call last): ... @@ -360,7 +364,7 @@ Validators ... OFF = "off" >>> @attr.s ... class C(object): - ... state = attr.ib(validator=attr.validators.in_(State)) # E: Need type annotation for variable + ... state = attr.ib(validator=attr.validators.in_(State)) # type: ignore ... val = attr.ib(validator=attr.validators.in_([1, 2, 3])) >>> C(State.ON, 1) C(state=, val=1) diff --git a/docs/doctest2.py b/docs/doctest2.py index d03003a9e..9f30223b3 100644 --- a/docs/doctest2.py +++ b/docs/doctest2.py @@ -1,5 +1,6 @@ from __future__ import absolute_import, print_function +import re import sys import sphinx import doctest @@ -20,21 +21,20 @@ MAIN = 'main' +type_comment_re = re.compile(r'#\s*type:\s*ignore\b.*$', re.MULTILINE) + -doctest.register_optionflag('MYPY_ERROR') -doctest.register_optionflag('MYPY_IGNORE') +MYPY_SKIP = doctest.register_optionflag('MYPY_SKIP') def convert_source(input): for i in range(len(input)): # FIXME: convert to regex - input[i] = input[i].replace('#doctest: +MYPY_ERROR', '') + input[i] = input[i].replace('# mypy error:', '# E:') input[i] = input[i].replace('#doctest: +MYPY_IGNORE', '# type: ignore') return input -import re -# FIXME: pull this from mypy_test_plugin def expand_errors(input, output, fnam: str): """Transform comments such as '# E: message' or '# E:3: message' in input. @@ -65,6 +65,7 @@ def expand_errors(input, output, fnam: str): fnam, i + 1, col, severity, m.group('message'))) +# Override SphinxDocTestRunner to gather the source for each group. class SphinxDocTestRunner(sphinx.ext.doctest.SphinxDocTestRunner): group_source = '' @@ -75,14 +76,37 @@ def run(self, test, compileflags=None, out=None, clear_globs=True): # add the source for this block to the group result = doctest.DocTestRunner.run(self, test, compileflags, out, clear_globs) - self.group_source += ''.join(example.source - for example in test.examples) + sources = [example.source for example in test.examples + if not example.options.get(MYPY_SKIP, False)] + self.group_source += ''.join(sources) return result # patch the runner sphinx.ext.doctest.SphinxDocTestRunner = SphinxDocTestRunner +# _orig_run = sphinx.ext.doctest.TestDirective.run +# def _new_run(self): +# nodes = _orig_run(self) +# node = nodes[0] +# code = node.rawsource +# test = None +# if 'test' in node: +# test = node['test'] +# +# if type_comment_re.search(code): +# print("here") +# if not test: +# test = code +# node.rawsource = type_comment_re.sub('', code) +# print(node.rawsource) +# if test is not None: +# # only save if it differs from code +# node['test'] = test +# return nodes + +# sphinx.ext.doctest.TestDirective.run = _new_run + class DocTest2Builder(DocTestBuilder): def test_group(self, group, filename): diff --git a/docs/examples.rst b/docs/examples.rst index 8d5fdcffc..8847dc4bf 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -85,7 +85,7 @@ If you want to initialize your private attributes yourself, you can do that too: ... _x = attr.ib(init=False, default=42) >>> C() C(_x=42) - >>> C(23) # E: Too many arguments for "C" + >>> C(23) # mypy error: Too many arguments for "C" Traceback (most recent call last): ... TypeError: __init__() takes exactly 1 argument (2 given) @@ -94,11 +94,12 @@ An additional way of defining attributes is supported too. This is useful in times when you want to enhance classes that are not yours (nice ``__repr__`` for Django models anyone?): .. doctest:: enhance + :options: +MYPY_SKIP >>> class SomethingFromSomeoneElse(object): ... def __init__(self, x): ... self.x = x - >>> SomethingFromSomeoneElse = attr.s( # type: ignore # can't override a type + >>> SomethingFromSomeoneElse = attr.s( ... these={ ... "x": attr.ib() ... }, init=False)(SomethingFromSomeoneElse) @@ -563,7 +564,7 @@ Slot classes are a little different than ordinary, dictionary-backed classes: ... y = attr.ib() ... >>> c = Coordinates(x=1, y=2) - >>> c.z = 3 # E: "Coordinates" has no attribute "z" + >>> c.z = 3 # mypy error: "Coordinates" has no attribute "z" Traceback (most recent call last): ... AttributeError: 'Coordinates' object has no attribute 'z' @@ -610,7 +611,7 @@ If you'd like to enforce it, ``attrs`` will try to help: ... class C(object): ... x = attr.ib() >>> i = C(1) - >>> i.x = 2 # E: Property "x" defined in "C" is read-only + >>> i.x = 2 # mypy error: Property "x" defined in "C" is read-only Traceback (most recent call last): ... attr.exceptions.FrozenInstanceError: can't set attribute From a430345cd5890e8f899d1d8b42eaa41acdeb8755 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Wed, 3 Jan 2018 09:50:51 -0800 Subject: [PATCH 39/64] Add tests for inheritance, eq, and cmp --- tests/test_stubs.tests.py | 78 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 73 insertions(+), 5 deletions(-) diff --git a/tests/test_stubs.tests.py b/tests/test_stubs.tests.py index 5ba43e961..25a4d7040 100644 --- a/tests/test_stubs.tests.py +++ b/tests/test_stubs.tests.py @@ -56,7 +56,7 @@ class D: @attr.s class C: - a : int = attr.ib() + a: int = attr.ib() c = C(1) reveal_type(c.a) # E: Revealed type is 'builtins.int' @@ -74,6 +74,76 @@ class D: reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' +# [case test_inheritance] +# :---------------------- +import attr + +@attr.s +class A: + x: int = attr.ib() + +@attr.s +class B(A): + y: str = attr.ib() + +B(x=1, y='foo') +B(x=1, y=2) # E: Argument 2 to "B" has incompatible type "int"; expected "str" + + +# [case test_multiple_inheritance] +# :---------------------- +import attr + +@attr.s +class A: + x: int = attr.ib() + +@attr.s +class B: + y: str = attr.ib() + +@attr.s +class C(B, A): + z: float = attr.ib() + +C(x=1, y='foo', z=1.1) +C(x=1, y=2, z=1.1) # E: Argument 2 to "C" has incompatible type "int"; expected "str" + + +# [case test_dunders] +# :------------------ +import attr + +@attr.s +class A: + x: int = attr.ib() + +@attr.s +class B(A): + y: str = attr.ib() + +class C: + pass + +# same class +B(x=1, y='foo') == B(x=1, y='foo') +# child class +B(x=1, y='foo') == A(x=1) +# parent class +A(x=1) == B(x=1, y='foo') +# not attrs class +A(x=1) == C() + +# same class +B(x=1, y='foo') > B(x=1, y='foo') +# child class +B(x=1, y='foo') > A(x=1) +# parent class +A(x=1) > B(x=1, y='foo') +# not attrs class +A(x=1) > C() # E: Unsupported operand types for > ("A" and "C") + + # :--------------------------- # :Defaults # :--------------------------- @@ -278,18 +348,16 @@ class C: # [case test_converter_init] # :------------------------- import attr -from typing import Union -def str_to_int(s: Union[str, int]) -> int: +def str_to_int(s: str) -> int: return int(s) @attr.s class C: x: int = attr.ib(convert=str_to_int) -C(1) C('1') -C(1.1) # E: Argument 1 to "C" has incompatible type "float"; expected "Union[str, int]" +C(1) # E: Argument 1 to "C" has incompatible type "int"; expected "str" # :--------------------------- # :Make From f0900aace175fd69dc0b04aa4e086d34de65fe86 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Wed, 3 Jan 2018 09:51:06 -0800 Subject: [PATCH 40/64] Add fixmes and todos --- src/attr/__init__.pyi | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index a5c0dd137..7c8c04cf6 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -47,7 +47,25 @@ class Attribute(Generic[_T]): # This makes this type of assignments possible: # x: int = attr(8) # -# Note: If you update these update `ib` and `attr` below. + +# FIXME: We had several choices for the annotation to use for type arg: +# 1) Type[_T] +# - Pros: works in PyCharm without plugin support +# - Cons: produces less informative error in the case of conflicting TypeVars +# e.g. `attr.ib(default='bad', type=int)` +# 2) Callable[..., _T] +# - Pros: more informative errors than #1 +# - Cons: validator tests results in confusing error. +# e.g. `attr.ib(type=int, validator=validate_str)` +# 3) type +# - Pros: in mypy, the behavior of type argument is exactly the same as with +# annotations. +# - Cons: completely disables type inspections in PyCharm when using the +# type arg. +# We chose option #1 until either PyCharm adds support for attrs, or python 2 +# reaches EOL. + +# NOTE: If you update these update `ib` and `attr` below. # 1st form catches _T set. @overload def attrib(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., @@ -83,6 +101,7 @@ class _Fields(Tuple[Attribute, ...]): def fields(cls: type) -> _Fields: ... def validate(inst: Any) -> None: ... +# TODO: add support for returning a proper attrs class from the mypy plugin # we use Any instead of _CountingAttr so that e.g. `make_class('Foo', [attr.ib()])` is valid def make_class(name, attrs: Union[List[str], Dict[str, Any]], bases: Tuple[type, ...] = ..., @@ -90,12 +109,14 @@ def make_class(name, attrs: Union[List[str], Dict[str, Any]], # _funcs -- +# TODO: add support for returning TypedDict from the mypy plugin # FIXME: asdict/astuple do not honor their factory args. waiting on one of these: # https://github.com/python/mypy/issues/4236 # https://github.com/python/typing/issues/253 def asdict(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[Mapping] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... +# TODO: add support for returning NamedTuple from the mypy plugin def astuple(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[Sequence] = ..., retain_collection_types: bool = ...) -> Tuple[Any, ...]: ... From 7344ac47a66f1c21075b366a5cc3abd7581f6b3c Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Wed, 3 Jan 2018 14:16:53 -0800 Subject: [PATCH 41/64] Rename convert to converter --- src/attr/__init__.pyi | 26 +++++++++++++------------- tests/test_stubs.tests.py | 6 +++--- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 7c8c04cf6..1fbd57957 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -30,7 +30,7 @@ class Attribute(Generic[_T]): cmp: bool hash: Optional[bool] init: bool - convert: Optional[_ConverterType[_T]] + converter: Optional[_ConverterType[_T]] metadata: Dict[Any, Any] type: Optional[Type[_T]] @@ -70,14 +70,14 @@ class Attribute(Generic[_T]): @overload def attrib(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., metadata: Mapping = ..., - type: Optional[Type[_T]] = ...) -> _T: ... + metadata: Mapping = ..., type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType[_T]] = ...) -> _T: ... # 2nd form no _T , so returns Any. @overload def attrib(default: None = ..., validator: None = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: None = ..., metadata: Mapping = ..., - type: None = ...) -> Any: ... + metadata: Mapping = ..., type: None = ..., + converter: None = ...) -> Any: ... # Note: If you update these update `s` and `attributes` below. @overload @@ -142,25 +142,25 @@ def get_run_validators() -> bool: ... @overload def ib(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., metadata: Mapping = ..., - type: Optional[Type[_T]] = ...) -> _T: ... + metadata: Mapping = ..., type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType[_T]] = ...) -> _T: ... # 2nd form catches no type-setters. So returns Any. @overload def ib(default: None = ..., validator: None = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: None = ..., metadata: Mapping = ..., - type: None = ...) -> Any: ... + metadata: Mapping = ..., type: None = ..., + converter: None = ..., ) -> Any: ... @overload def attr(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., metadata: Mapping = ..., - type: Optional[Type[_T]] = ...) -> _T: ... + metadata: Mapping = ..., type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType[_T]] = ...) -> _T: ... # 2nd form catches no type-setters. So returns Any. @overload def attr(default: None = ..., validator: None = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: None = ..., metadata: Mapping = ..., - type: None = ...) -> Any: ... + metadata: Mapping = ..., type: None = ..., + converter: None = ...) -> Any: ... @overload diff --git a/tests/test_stubs.tests.py b/tests/test_stubs.tests.py index 25a4d7040..ee8d0b51e 100644 --- a/tests/test_stubs.tests.py +++ b/tests/test_stubs.tests.py @@ -339,10 +339,10 @@ def str_to_int(s: Union[str, int]) -> int: @attr.s class C: - a = attr.ib(convert=str_to_int) + a = attr.ib(converter=str_to_int) reveal_type(a) # E: Revealed type is 'builtins.int*' - b: str = attr.ib(convert=str_to_int) # E: Incompatible types in assignment (expression has type "int", variable has type "str") + b: str = attr.ib(converter=str_to_int) # E: Incompatible types in assignment (expression has type "int", variable has type "str") # [case test_converter_init] @@ -354,7 +354,7 @@ def str_to_int(s: str) -> int: @attr.s class C: - x: int = attr.ib(convert=str_to_int) + x: int = attr.ib(converter=str_to_int) C('1') C(1) # E: Argument 1 to "C" has incompatible type "int"; expected "str" From 11b47a0bd01d58fe78656fbf529dccc2960c783e Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Wed, 3 Jan 2018 14:17:19 -0800 Subject: [PATCH 42/64] Attribute.validator is always a single validator --- src/attr/__init__.pyi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 1fbd57957..6f239621f 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -25,7 +25,7 @@ def Factory(factory: Union[Callable[[], _T], Callable[[Any], _T]], takes_self: b class Attribute(Generic[_T]): name: str default: Optional[_T] - validator: Optional[_ValidatorArgType[_T]] + validator: Optional[_ValidatorType[_T]] repr: bool cmp: bool hash: Optional[bool] From d7e5783d885fc8cbae24fb46bdf0611325018a10 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Wed, 3 Jan 2018 15:01:51 -0800 Subject: [PATCH 43/64] Conform stubs to typeshed coding style And add auto_attrib kw --- src/attr/__init__.pyi | 95 +++++++++---------------------------------- 1 file changed, 20 insertions(+), 75 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 6f239621f..5c014f885 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -68,30 +68,16 @@ class Attribute(Generic[_T]): # NOTE: If you update these update `ib` and `attr` below. # 1st form catches _T set. @overload -def attrib(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - metadata: Mapping = ..., type: Optional[Type[_T]] = ..., - converter: Optional[_ConverterType[_T]] = ...) -> _T: ... +def attrib(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., metadata: Mapping = ..., type: Optional[Type[_T]] = ..., converter: Optional[_ConverterType[_T]] = ...) -> _T: ... # 2nd form no _T , so returns Any. @overload -def attrib(default: None = ..., validator: None = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - metadata: Mapping = ..., type: None = ..., - converter: None = ...) -> Any: ... +def attrib(default: None = ..., validator: None = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., metadata: Mapping = ..., type: None = ..., converter: None = ...) -> Any: ... # Note: If you update these update `s` and `attributes` below. @overload -def attrs( - maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., - hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., - frozen: bool = ..., str: bool = ...) -> _C: ... +def attrs(maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ..., auto_attribs: bool = ...) -> _C: ... @overload -def attrs( - maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., - hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., - frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... +def attrs(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ..., auto_attribs: bool = ...) -> Callable[[_C], _C]: ... # TODO: add support for returning NamedTuple from the mypy plugin @@ -103,9 +89,7 @@ def validate(inst: Any) -> None: ... # TODO: add support for returning a proper attrs class from the mypy plugin # we use Any instead of _CountingAttr so that e.g. `make_class('Foo', [attr.ib()])` is valid -def make_class(name, attrs: Union[List[str], Dict[str, Any]], - bases: Tuple[type, ...] = ..., - **attributes_arguments) -> type: ... +def make_class(name, attrs: Union[List[str], Dict[str, Any]], bases: Tuple[type, ...] = ..., **attributes_arguments) -> type: ... # _funcs -- @@ -113,13 +97,9 @@ def make_class(name, attrs: Union[List[str], Dict[str, Any]], # FIXME: asdict/astuple do not honor their factory args. waiting on one of these: # https://github.com/python/mypy/issues/4236 # https://github.com/python/typing/issues/253 -def asdict(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., - dict_factory: Type[Mapping] = ..., - retain_collection_types: bool = ...) -> Dict[str, Any]: ... +def asdict(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[Mapping] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... # TODO: add support for returning NamedTuple from the mypy plugin -def astuple(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., - tuple_factory: Type[Sequence] = ..., - retain_collection_types: bool = ...) -> Tuple[Any, ...]: ... +def astuple(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[Sequence] = ..., retain_collection_types: bool = ...) -> Tuple[Any, ...]: ... def has(cls: type) -> bool: ... def assoc(inst: _T, **changes: Any) -> _T: ... @@ -140,65 +120,30 @@ def get_run_validators() -> bool: ... # https://youtrack.jetbrains.com/issue/PY-27788 @overload -def ib(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - metadata: Mapping = ..., type: Optional[Type[_T]] = ..., - converter: Optional[_ConverterType[_T]] = ...) -> _T: ... -# 2nd form catches no type-setters. So returns Any. +def ib(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., metadata: Mapping = ..., type: Optional[Type[_T]] = ..., converter: Optional[_ConverterType[_T]] = ...) -> _T: ... @overload -def ib(default: None = ..., validator: None = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - metadata: Mapping = ..., type: None = ..., - converter: None = ..., ) -> Any: ... +def ib(default: None = ..., validator: None = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., metadata: Mapping = ..., type: None = ..., converter: None = ...) -> Any: ... + @overload -def attr(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - metadata: Mapping = ..., type: Optional[Type[_T]] = ..., - converter: Optional[_ConverterType[_T]] = ...) -> _T: ... -# 2nd form catches no type-setters. So returns Any. +def attr(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., metadata: Mapping = ..., type: Optional[Type[_T]] = ..., converter: Optional[_ConverterType[_T]] = ...) -> _T: ... @overload -def attr(default: None = ..., validator: None = ..., - repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - metadata: Mapping = ..., type: None = ..., - converter: None = ...) -> Any: ... +def attr(default: None = ..., validator: None = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., metadata: Mapping = ..., type: None = ..., converter: None = ...) -> Any: ... @overload -def attributes( - maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., - hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., - frozen: bool = ..., str: bool = ...) -> _C: ... +def attributes(maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ..., auto_attribs: bool = ...) -> _C: ... @overload -def attributes( - maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., - hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., - frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... +def attributes(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ..., auto_attribs: bool = ...) -> Callable[[_C], _C]: ... @overload -def s( - maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., - hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., - frozen: bool = ..., str: bool = ...) -> _C: ... +def s(maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ..., auto_attribs: bool = ...) -> _C: ... @overload -def s( - maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., - hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., - frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... +def s(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ..., auto_attribs: bool = ...) -> Callable[[_C], _C]: ... +# auto_attrib=True @overload -def dataclass( - maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., - hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., - frozen: bool = ..., str: bool = ...) -> _C: ... +def dataclass(maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> _C: ... @overload -def dataclass( - maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., - hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., - frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... +def dataclass(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... + # -- \ No newline at end of file From b745de6a4183083bb8bf968335cb986f2e74cd4c Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Fri, 2 Feb 2018 11:16:31 -0800 Subject: [PATCH 44/64] backport style fixes from typeshed --- src/attr/__init__.pyi | 203 +++++++++++++++++++++++++++++++++--------- 1 file changed, 161 insertions(+), 42 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 5c014f885..90f3a8f22 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -1,25 +1,23 @@ from typing import Any, Callable, Dict, Generic, List, Optional, Sequence, Mapping, Tuple, Type, TypeVar, Union, overload -# `import X as X` is required to expose these to mypy. otherwise they are invisible +# `import X as X` is required to make these public from . import exceptions as exceptions from . import filters as filters from . import converters as converters from . import validators as validators -# typing -- - _T = TypeVar('_T') _C = TypeVar('_C', bound=type) -_ValidatorType = Callable[[Any, 'Attribute', _T], Any] +_ValidatorType = Callable[[Any, Attribute, _T], Any] _ConverterType = Callable[[Any], _T] -_FilterType = Callable[['Attribute', Any], bool] +_FilterType = Callable[[Attribute, Any], bool] _ValidatorArgType = Union[_ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T], ...]] # _make -- -NOTHING : object +NOTHING: object -# Factory lies about its return type to make this possible: `x: List[int] = Factory(list)` +# NOTE: Factory lies about its return type to make this possible: `x: List[int] = Factory(list)` def Factory(factory: Union[Callable[[], _T], Callable[[Any], _T]], takes_self: bool = ...) -> _T: ... class Attribute(Generic[_T]): @@ -33,22 +31,13 @@ class Attribute(Generic[_T]): converter: Optional[_ConverterType[_T]] metadata: Dict[Any, Any] type: Optional[Type[_T]] - def __lt__(self, x: Attribute) -> bool: ... def __le__(self, x: Attribute) -> bool: ... def __gt__(self, x: Attribute) -> bool: ... def __ge__(self, x: Attribute) -> bool: ... -# `attr` also lies about its return type to make the following possible: -# attr() -> Any -# attr(8) -> int -# attr(validator=) -> Whatever the callable expects. -# This makes this type of assignments possible: -# x: int = attr(8) -# - -# FIXME: We had several choices for the annotation to use for type arg: +# NOTE: We had several choices for the annotation to use for type arg: # 1) Type[_T] # - Pros: works in PyCharm without plugin support # - Cons: produces less informative error in the case of conflicting TypeVars @@ -57,7 +46,7 @@ class Attribute(Generic[_T]): # - Pros: more informative errors than #1 # - Cons: validator tests results in confusing error. # e.g. `attr.ib(type=int, validator=validate_str)` -# 3) type +# 3) type (and do all of the work in the mypy plugin) # - Pros: in mypy, the behavior of type argument is exactly the same as with # annotations. # - Cons: completely disables type inspections in PyCharm when using the @@ -65,19 +54,63 @@ class Attribute(Generic[_T]): # We chose option #1 until either PyCharm adds support for attrs, or python 2 # reaches EOL. -# NOTE: If you update these update `ib` and `attr` below. +# NOTE: If you update these, update `ib` and `attr` below. + +# `attr` lies about its return type to make the following possible: +# attr() -> Any +# attr(8) -> int +# attr(validator=) -> Whatever the callable expects. +# This makes this type of assignments possible: +# x: int = attr(8) + # 1st form catches _T set. @overload -def attrib(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., metadata: Mapping = ..., type: Optional[Type[_T]] = ..., converter: Optional[_ConverterType[_T]] = ...) -> _T: ... +def attrib(default: Optional[_T] = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Mapping = ..., + type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType[_T]] = ...) -> _T: ... # 2nd form no _T , so returns Any. @overload -def attrib(default: None = ..., validator: None = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., metadata: Mapping = ..., type: None = ..., converter: None = ...) -> Any: ... +def attrib(default: None = ..., + validator: None = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Mapping = ..., + type: None = ..., + converter: None = ...) -> Any: ... -# Note: If you update these update `s` and `attributes` below. +# NOTE: If you update these, update `s` and `attributes` below. @overload -def attrs(maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ..., auto_attribs: bool = ...) -> _C: ... +def attrs(maybe_cls: _C, + these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + str: bool = ..., + auto_attribs: bool = ...) -> _C: ... @overload -def attrs(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ..., auto_attribs: bool = ...) -> Callable[[_C], _C]: ... +def attrs(maybe_cls: None = ..., + these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + str: bool = ..., + auto_attribs: bool = ...) -> Callable[[_C], _C]: ... # TODO: add support for returning NamedTuple from the mypy plugin @@ -100,7 +133,6 @@ def make_class(name, attrs: Union[List[str], Dict[str, Any]], bases: Tuple[type, def asdict(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[Mapping] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... # TODO: add support for returning NamedTuple from the mypy plugin def astuple(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[Sequence] = ..., retain_collection_types: bool = ...) -> Tuple[Any, ...]: ... - def has(cls: type) -> bool: ... def assoc(inst: _T, **changes: Any) -> _T: ... def evolve(inst: _T, **changes: Any) -> _T: ... @@ -111,39 +143,126 @@ def set_run_validators(run: bool) -> None: ... def get_run_validators() -> bool: ... # aliases -- -#s = attributes = attrs -#ib = attr = attrib -#dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) +# s = attributes = attrs +# ib = attr = attrib +# dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) # FIXME: there is a bug in PyCharm with creating aliases to overloads. # Remove these when the bug is fixed: # https://youtrack.jetbrains.com/issue/PY-27788 @overload -def ib(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., metadata: Mapping = ..., type: Optional[Type[_T]] = ..., converter: Optional[_ConverterType[_T]] = ...) -> _T: ... +def ib(default: Optional[_T] = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Mapping = ..., + type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType[_T]] = ...) -> _T: ... @overload -def ib(default: None = ..., validator: None = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., metadata: Mapping = ..., type: None = ..., converter: None = ...) -> Any: ... +def ib(default: None = ..., + validator: None = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Mapping = ..., + type: None = ..., + converter: None = ...) -> Any: ... @overload -def attr(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., metadata: Mapping = ..., type: Optional[Type[_T]] = ..., converter: Optional[_ConverterType[_T]] = ...) -> _T: ... +def attr(default: Optional[_T] = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Mapping = ..., + type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType[_T]] = ...) -> _T: ... @overload -def attr(default: None = ..., validator: None = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., metadata: Mapping = ..., type: None = ..., converter: None = ...) -> Any: ... - +def attr(default: None = ..., + validator: None = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Mapping = ..., + type: None = ..., + converter: None = ...) -> Any: ... @overload -def attributes(maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ..., auto_attribs: bool = ...) -> _C: ... +def attributes(maybe_cls: _C, + these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + str: bool = ..., + auto_attribs: bool = ...) -> _C: ... @overload -def attributes(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ..., auto_attribs: bool = ...) -> Callable[[_C], _C]: ... +def attributes(maybe_cls: None = ..., + these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + str: bool = ..., + auto_attribs: bool = ...) -> Callable[[_C], _C]: ... @overload -def s(maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ..., auto_attribs: bool = ...) -> _C: ... +def s(maybe_cls: _C, + these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + str: bool = ..., + auto_attribs: bool = ...) -> _C: ... @overload -def s(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ..., auto_attribs: bool = ...) -> Callable[[_C], _C]: ... +def s(maybe_cls: None = ..., + these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + str: bool = ..., + auto_attribs: bool = ...) -> Callable[[_C], _C]: ... -# auto_attrib=True +# same as above, but with auto_attrib=True @overload -def dataclass(maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> _C: ... +def dataclass(maybe_cls: _C, + these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + str: bool = ...) -> _C: ... @overload -def dataclass(maybe_cls: None = ..., these: Optional[Dict[str, Any]] = ..., repr_ns: Optional[str] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., str: bool = ...) -> Callable[[_C], _C]: ... - -# -- \ No newline at end of file +def dataclass(maybe_cls: None = ..., + these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + str: bool = ...) -> Callable[[_C], _C]: ... From 4863502ceef72d0449b5cdf9c8ba100344717c72 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Fri, 2 Feb 2018 11:16:55 -0800 Subject: [PATCH 45/64] Add test cases to cover forward references and Any --- tests/test_stubs.tests.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/tests/test_stubs.tests.py b/tests/test_stubs.tests.py index ee8d0b51e..8ebb7bc2c 100644 --- a/tests/test_stubs.tests.py +++ b/tests/test_stubs.tests.py @@ -26,7 +26,7 @@ class C: # :------------------- # cmd: mypy --strict-optional import attr -from typing import List +from typing import List, Any @attr.s class C: @@ -47,12 +47,22 @@ class D: a = attr.ib(type=List[int]) reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' +@attr.s +class E: + a = attr.ib(type='List[int]') + reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' + +@attr.s +class F: + a = attr.ib(type=Any) + reveal_type(a) # E: Revealed type is 'Any' + # [case test_type_annotations] # :--------------------------- # cmd: mypy --strict-optional import attr -from typing import List +from typing import List, Any @attr.s class C: @@ -73,6 +83,18 @@ class D: a: List[int] = attr.ib() reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' +@attr.s +class E: + a: 'List[int]' = attr.ib() + reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' + +@attr.s +class F: + a: Any = attr.ib() + reveal_type(a) # E: Revealed type is 'Any' + + + # [case test_inheritance] # :---------------------- @@ -270,7 +292,7 @@ class C: # mypy does not know how to get the contained type from an enum: f = attr.ib(type=State, validator=in_(State)) - ff = attr.ib(validator=in_(State)) # E: Need type annotation for variable + ff = attr.ib(validator=in_(State)) # E: Need type annotation for 'ff' # [case test_init_with_validators] From 84d3eb4c31ab86e8d8ef36060151b496c30ef3e1 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Fri, 2 Feb 2018 11:32:37 -0800 Subject: [PATCH 46/64] Add fixes for forward references and Any --- src/attr/__init__.pyi | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 90f3a8f22..72738df19 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -85,6 +85,18 @@ def attrib(default: None = ..., metadata: Mapping = ..., type: None = ..., converter: None = ...) -> Any: ... +# 3rd form non-Type: e.g. forward references, Any +@overload +def attrib(default: Optional[_T] = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Mapping = ..., + type: object = ..., + converter: Optional[_ConverterType[_T]] = ...) -> Any: ... + # NOTE: If you update these, update `s` and `attributes` below. @overload @@ -171,6 +183,16 @@ def ib(default: None = ..., metadata: Mapping = ..., type: None = ..., converter: None = ...) -> Any: ... +@overload +def ib(default: Optional[_T] = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Mapping = ..., + type: object = ..., + converter: Optional[_ConverterType[_T]] = ...) -> Any: ... @overload def attr(default: Optional[_T] = ..., @@ -192,6 +214,16 @@ def attr(default: None = ..., metadata: Mapping = ..., type: None = ..., converter: None = ...) -> Any: ... +@overload +def attr(default: Optional[_T] = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + metadata: Mapping = ..., + type: object = ..., + converter: Optional[_ConverterType[_T]] = ...) -> Any: ... @overload def attributes(maybe_cls: _C, From 0db6009e7f264df630f8c459d9782dcf25dd41b3 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Fri, 2 Feb 2018 11:46:54 -0800 Subject: [PATCH 47/64] Address typeshed review notes --- src/attr/__init__.pyi | 63 ++++++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 72738df19..74f0be3ef 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -71,7 +71,8 @@ def attrib(default: Optional[_T] = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - metadata: Mapping = ..., + convert: Optional[_ConverterType[_T]] = ..., + metadata: Optional[Mapping[Any, Any]] = ..., type: Optional[Type[_T]] = ..., converter: Optional[_ConverterType[_T]] = ...) -> _T: ... # 2nd form no _T , so returns Any. @@ -82,10 +83,11 @@ def attrib(default: None = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - metadata: Mapping = ..., + convert: Optional[_ConverterType[_T]] = ..., + metadata: Optional[Mapping[Any, Any]] = ..., type: None = ..., converter: None = ...) -> Any: ... -# 3rd form non-Type: e.g. forward references, Any +# 3rd form covers non-Type: e.g. forward references (str), Any @overload def attrib(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., @@ -93,7 +95,8 @@ def attrib(default: Optional[_T] = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - metadata: Mapping = ..., + convert: Optional[_ConverterType[_T]] = ..., + metadata: Optional[Mapping[Any, Any]] = ..., type: object = ..., converter: Optional[_ConverterType[_T]] = ...) -> Any: ... @@ -134,7 +137,18 @@ def validate(inst: Any) -> None: ... # TODO: add support for returning a proper attrs class from the mypy plugin # we use Any instead of _CountingAttr so that e.g. `make_class('Foo', [attr.ib()])` is valid -def make_class(name, attrs: Union[List[str], Dict[str, Any]], bases: Tuple[type, ...] = ..., **attributes_arguments) -> type: ... +def make_class(name: str, + attrs: Union[List[str], Tuple[str, ...], Dict[str, Any]], + bases: Tuple[type, ...] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + str: bool = ..., + auto_attribs: bool = ...) -> type: ... # _funcs -- @@ -142,9 +156,17 @@ def make_class(name, attrs: Union[List[str], Dict[str, Any]], bases: Tuple[type, # FIXME: asdict/astuple do not honor their factory args. waiting on one of these: # https://github.com/python/mypy/issues/4236 # https://github.com/python/typing/issues/253 -def asdict(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., dict_factory: Type[Mapping] = ..., retain_collection_types: bool = ...) -> Dict[str, Any]: ... +def asdict(inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType] = ..., + dict_factory: Type[Mapping[Any, Any]] = ..., + retain_collection_types: bool = ...) -> Dict[str, Any]: ... # TODO: add support for returning NamedTuple from the mypy plugin -def astuple(inst: Any, recurse: bool = ..., filter: Optional[_FilterType] = ..., tuple_factory: Type[Sequence] = ..., retain_collection_types: bool = ...) -> Tuple[Any, ...]: ... +def astuple(inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType] = ..., + tuple_factory: Type[Sequence] = ..., + retain_collection_types: bool = ...) -> Tuple[Any, ...]: ... def has(cls: type) -> bool: ... def assoc(inst: _T, **changes: Any) -> _T: ... def evolve(inst: _T, **changes: Any) -> _T: ... @@ -154,14 +176,17 @@ def evolve(inst: _T, **changes: Any) -> _T: ... def set_run_validators(run: bool) -> None: ... def get_run_validators() -> bool: ... + # aliases -- + +# FIXME: there is a bug in PyCharm with creating aliases to overloads. +# Use the aliases instead of the duplicated overloads when the bug is fixed: +# https://youtrack.jetbrains.com/issue/PY-27788 + # s = attributes = attrs # ib = attr = attrib # dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) -# FIXME: there is a bug in PyCharm with creating aliases to overloads. -# Remove these when the bug is fixed: -# https://youtrack.jetbrains.com/issue/PY-27788 @overload def ib(default: Optional[_T] = ..., @@ -170,7 +195,8 @@ def ib(default: Optional[_T] = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - metadata: Mapping = ..., + convert: Optional[_ConverterType[_T]] = ..., + metadata: Optional[Mapping[Any, Any]] = ..., type: Optional[Type[_T]] = ..., converter: Optional[_ConverterType[_T]] = ...) -> _T: ... @overload @@ -180,7 +206,8 @@ def ib(default: None = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - metadata: Mapping = ..., + convert: Optional[_ConverterType[_T]] = ..., + metadata: Optional[Mapping[Any, Any]] = ..., type: None = ..., converter: None = ...) -> Any: ... @overload @@ -190,7 +217,8 @@ def ib(default: Optional[_T] = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - metadata: Mapping = ..., + convert: Optional[_ConverterType[_T]] = ..., + metadata: Optional[Mapping[Any, Any]] = ..., type: object = ..., converter: Optional[_ConverterType[_T]] = ...) -> Any: ... @@ -201,7 +229,8 @@ def attr(default: Optional[_T] = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - metadata: Mapping = ..., + convert: Optional[_ConverterType[_T]] = ..., + metadata: Optional[Mapping[Any, Any]] = ..., type: Optional[Type[_T]] = ..., converter: Optional[_ConverterType[_T]] = ...) -> _T: ... @overload @@ -211,7 +240,8 @@ def attr(default: None = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - metadata: Mapping = ..., + convert: Optional[_ConverterType[_T]] = ..., + metadata: Optional[Mapping[Any, Any]] = ..., type: None = ..., converter: None = ...) -> Any: ... @overload @@ -221,7 +251,8 @@ def attr(default: Optional[_T] = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - metadata: Mapping = ..., + convert: Optional[_ConverterType[_T]] = ..., + metadata: Optional[Mapping[Any, Any]] = ..., type: object = ..., converter: Optional[_ConverterType[_T]] = ...) -> Any: ... From 7087828a423e9f7f074e1a0712c2bfcead6a1aeb Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Fri, 2 Feb 2018 11:59:19 -0800 Subject: [PATCH 48/64] Use Sequence instead of List/Tuple for validator arg list and tuple are invariant and so prevent passing subtypes of _ValidatorType --- src/attr/__init__.pyi | 5 ++++- tests/test_stubs.tests.py | 17 ++++++++++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 74f0be3ef..8e328a162 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -11,7 +11,10 @@ _C = TypeVar('_C', bound=type) _ValidatorType = Callable[[Any, Attribute, _T], Any] _ConverterType = Callable[[Any], _T] _FilterType = Callable[[Attribute, Any], bool] -_ValidatorArgType = Union[_ValidatorType[_T], List[_ValidatorType[_T]], Tuple[_ValidatorType[_T], ...]] +# FIXME: in reality, if multiple validators are passed they must be in a list or tuple, +# but those are invariant and so would prevent subtypes of _ValidatorType from working +# when passed in a list or tuple. +_ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] # _make -- diff --git a/tests/test_stubs.tests.py b/tests/test_stubs.tests.py index 8ebb7bc2c..01860b747 100644 --- a/tests/test_stubs.tests.py +++ b/tests/test_stubs.tests.py @@ -324,11 +324,14 @@ def validate_str(inst, at, val: str): @attr.s class C: - a = attr.ib(type=int, validator=validate_int) # int - b = attr.ib(type=int, validator=validate_str) # E: Argument 2 to "ib" has incompatible type "Callable[[Any, Any, str], Any]"; expected "Union[Callable[[Any, Attribute[Any], int], Any], List[Callable[[Any, Attribute[Any], int], Any]], Tuple[Callable[[Any, Attribute[Any], int], Any], ...]]" - + a = attr.ib(type=int, validator=validate_int) reveal_type(a) # E: Revealed type is 'builtins.int' + b = attr.ib(type=int, validator=[validate_int]) + reveal_type(b) # E: Revealed type is 'builtins.int' + + c = attr.ib(type=int, validator=validate_str) # E: Argument 2 to "ib" has incompatible type "Callable[[Any, Any, str], Any]"; expected "Union[Callable[[Any, Attribute[Any], int], Any], Sequence[Callable[[Any, Attribute[Any], int], Any]]]" + # [case test_custom_validators_type_annotations] # :--------------------------------------------- @@ -343,9 +346,13 @@ def validate_str(inst, at, val: str): @attr.s class C: a: int = attr.ib(validator=validate_int) - b: int = attr.ib(validator=validate_str) # E: Incompatible types in assignment (expression has type "str", variable has type "int") + reveal_type(a) # E: Revealed type is 'builtins.int' + + b: int = attr.ib(validator=[validate_int]) + reveal_type(b) # E: Revealed type is 'builtins.int' + + c: int = attr.ib(validator=validate_str) # E: Incompatible types in assignment (expression has type "str", variable has type "int") - reveal_type(a) # E: Revealed type is 'builtins.int' # :--------------------------- # :Converters From 201ad636f65ea54983995f2ef63815bf7785e105 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sun, 25 Feb 2018 10:18:50 -0800 Subject: [PATCH 49/64] backports changes from typeshed #1914 --- src/attr/__init__.pyi | 50 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 8e328a162..9746ded49 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -66,7 +66,19 @@ class Attribute(Generic[_T]): # This makes this type of assignments possible: # x: int = attr(8) -# 1st form catches _T set. +# 1st form catches _T set and works around mypy issue #4554 +@overload +def attrib(default: _T, + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + convert: Optional[_ConverterType[_T]] = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType[_T]] = ...) -> _T: ... +# 2nd one with an optional default. @overload def attrib(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., @@ -78,7 +90,7 @@ def attrib(default: Optional[_T] = ..., metadata: Optional[Mapping[Any, Any]] = ..., type: Optional[Type[_T]] = ..., converter: Optional[_ConverterType[_T]] = ...) -> _T: ... -# 2nd form no _T , so returns Any. +# 3rd form no _T , so returns Any. @overload def attrib(default: None = ..., validator: None = ..., @@ -86,11 +98,11 @@ def attrib(default: None = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., + convert: None = ..., metadata: Optional[Mapping[Any, Any]] = ..., type: None = ..., converter: None = ...) -> Any: ... -# 3rd form covers non-Type: e.g. forward references (str), Any +# 4th form covers type=non-Type: e.g. forward references (str), Any @overload def attrib(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., @@ -150,7 +162,7 @@ def make_class(name: str, init: bool = ..., slots: bool = ..., frozen: bool = ..., - str: bool = ..., + str: bool = ..., auto_attribs: bool = ...) -> type: ... # _funcs -- @@ -191,6 +203,17 @@ def get_run_validators() -> bool: ... # dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) +@overload +def ib(default: _T, + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + convert: Optional[_ConverterType[_T]] = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType[_T]] = ...) -> _T: ... @overload def ib(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., @@ -209,7 +232,7 @@ def ib(default: None = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., + convert: None = ..., metadata: Optional[Mapping[Any, Any]] = ..., type: None = ..., converter: None = ...) -> Any: ... @@ -226,6 +249,17 @@ def ib(default: Optional[_T] = ..., converter: Optional[_ConverterType[_T]] = ...) -> Any: ... @overload +def attr(default: _T, + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + convert: Optional[_ConverterType[_T]] = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType[_T]] = ...) -> _T: ... +@overload def attr(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., repr: bool = ..., @@ -243,7 +277,7 @@ def attr(default: None = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., + convert: None = ..., metadata: Optional[Mapping[Any, Any]] = ..., type: None = ..., converter: None = ...) -> Any: ... @@ -331,4 +365,4 @@ def dataclass(maybe_cls: None = ..., init: bool = ..., slots: bool = ..., frozen: bool = ..., - str: bool = ...) -> Callable[[_C], _C]: ... + str: bool = ...) -> Callable[[_C], _C]: ... \ No newline at end of file From e4461f463373aae8ce530998986508d6a8d81d99 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Thu, 1 Mar 2018 09:24:23 -0800 Subject: [PATCH 50/64] backport changes from typeshed #1933 --- src/attr/__init__.pyi | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 9746ded49..099d2e601 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -21,7 +21,11 @@ _ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] NOTHING: object # NOTE: Factory lies about its return type to make this possible: `x: List[int] = Factory(list)` -def Factory(factory: Union[Callable[[], _T], Callable[[Any], _T]], takes_self: bool = ...) -> _T: ... +# Work around mypy issue #4554 by using an overload vs a Union. +@overload +def Factory(factory: Callable[[], _T], takes_self: bool = ...) -> _T: ... +@overload +def Factory(factory: Callable[[Any], _T], takes_self: bool = ...) -> _T: ... class Attribute(Generic[_T]): name: str From 3fc633c2ab95e9c9d0f5a6c2befa1dd82acf9f4f Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sun, 11 Mar 2018 11:40:06 -0700 Subject: [PATCH 51/64] Prevent mypy tests from getting picked up Evidently the discovery rules changed recently for pytest. --- tests/{test_stubs.tests.py => mypy.tests.py} | 0 tests/test_stubs.py | 5 +++-- 2 files changed, 3 insertions(+), 2 deletions(-) rename tests/{test_stubs.tests.py => mypy.tests.py} (100%) diff --git a/tests/test_stubs.tests.py b/tests/mypy.tests.py similarity index 100% rename from tests/test_stubs.tests.py rename to tests/mypy.tests.py diff --git a/tests/test_stubs.py b/tests/test_stubs.py index 72d89962e..0f8add667 100644 --- a/tests/test_stubs.py +++ b/tests/test_stubs.py @@ -15,8 +15,9 @@ # Path to Python 3 interpreter python3_path = sys.executable test_temp_dir = 'tmp' -test_file = os.path.splitext(os.path.realpath(__file__))[0] + '.tests.py' -prefix_dir = os.path.join(os.path.dirname(os.path.dirname(test_file)), 'src') +this_dir = os.path.dirname(os.path.realpath(__file__)) +test_file = os.path.join(this_dir, 'mypy.tests.py') +prefix_dir = os.path.join(os.path.dirname(this_dir), 'src') class PythonEvaluationSuite(DataSuite): From 0ab6d5cd8a7e87c407dc928d22ec8214053f3a7d Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sat, 14 Apr 2018 11:23:14 -0700 Subject: [PATCH 52/64] make our doctest extension compatible with latest sphinx --- docs/doctest2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/doctest2.py b/docs/doctest2.py index 9f30223b3..b07a3af13 100644 --- a/docs/doctest2.py +++ b/docs/doctest2.py @@ -109,12 +109,12 @@ def run(self, test, compileflags=None, out=None, clear_globs=True): class DocTest2Builder(DocTestBuilder): - def test_group(self, group, filename): + def test_group(self, group, *args, **kwargs): self.setup_runner.reset_source() self.test_runner.reset_source() self.cleanup_runner.reset_source() - result = DocTestBuilder.test_group(self, group, filename) + result = DocTestBuilder.test_group(self, group, *args, **kwargs) source = (self.setup_runner.group_source + self.test_runner.group_source + From c95d5ae115a220112b0fcdb7300c31c434393c9e Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sat, 14 Apr 2018 11:23:44 -0700 Subject: [PATCH 53/64] Adjustments to the tests --- docs/conf.py | 3 ++- setup.cfg | 3 +++ setup.py | 12 ++++++++---- tests/mypy_pytest_plugin.py | 3 ++- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 36ef93b90..3af54617c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -42,7 +42,8 @@ def find_version(*file_paths): 'sphinx.ext.todo', ] - +import sys +sys.path.append(HERE) doctest_path = [os.path.join(HERE, '..', 'src')] # Add any paths that contain templates here, relative to this directory. diff --git a/setup.cfg b/setup.cfg index 8ddbbabc3..22272de47 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,3 +24,6 @@ multi_line_output=5 not_skip=__init__.py known_first_party=attr + +[flake8] +exclude = tests/mypy.tests.py diff --git a/setup.py b/setup.py index 9674698af..46050247e 100644 --- a/setup.py +++ b/setup.py @@ -31,11 +31,13 @@ "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules", ] +MYPY_VERSION = "mypy==0.580" INSTALL_REQUIRES = [] EXTRAS_REQUIRE = { "docs": [ "sphinx", "zope.interface", + MYPY_VERSION, ], "tests": [ "coverage", @@ -46,13 +48,15 @@ "zope.interface", ], } -if PY3: - # output formatting can vary by version, so lock version until we have a - # better way to deal with that. - EXTRAS_REQUIRE["tests"].append("mypy==0.570") EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["docs"] +if PY3: + # mypy plugins are not yet externally distributable, so we must lock tests + # against a particular version of mypy. additionally, output formatting + # of mypy can vary by version. + EXTRAS_REQUIRE["tests"].append(MYPY_VERSION) + ############################################################################### HERE = os.path.abspath(os.path.dirname(__file__)) diff --git a/tests/mypy_pytest_plugin.py b/tests/mypy_pytest_plugin.py index 4ae1d646d..31bfb67fc 100644 --- a/tests/mypy_pytest_plugin.py +++ b/tests/mypy_pytest_plugin.py @@ -698,7 +698,8 @@ def parse_test_data(l: List[str], fnam: str) -> List[TestItem]: while i < len(l): s = l[i].strip() - if l[i].startswith('# [') and s.endswith(']') and not s.startswith('# [['): + if (l[i].startswith('# [') and s.endswith(']') + and not s.startswith('# [[')): if id: data = collapse_line_continuation(data) data = strip_list(data) From f33712d2d2f7e9bc4b60c0351390ce71f59f8b29 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sat, 14 Apr 2018 11:42:00 -0700 Subject: [PATCH 54/64] Fix flake and manifest tests (hopefully) --- docs/conf.py | 5 +++-- setup.py | 1 + stubs-requirements.txt | 2 -- tests/mypy_pytest_plugin.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) delete mode 100644 stubs-requirements.txt diff --git a/docs/conf.py b/docs/conf.py index 3af54617c..45c8a09b6 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -3,9 +3,12 @@ import codecs import os import re +import sys HERE = os.path.abspath(os.path.dirname(__file__)) +# to find the doctest2 extension: +sys.path.append(HERE) def read(*parts): @@ -42,8 +45,6 @@ def find_version(*file_paths): 'sphinx.ext.todo', ] -import sys -sys.path.append(HERE) doctest_path = [os.path.join(HERE, '..', 'src')] # Add any paths that contain templates here, relative to this directory. diff --git a/setup.py b/setup.py index 46050247e..24af9c38f 100644 --- a/setup.py +++ b/setup.py @@ -5,6 +5,7 @@ from setuptools import find_packages, setup + PY3 = sys.version_info >= (3,) ############################################################################### diff --git a/stubs-requirements.txt b/stubs-requirements.txt deleted file mode 100644 index 547eaf77d..000000000 --- a/stubs-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest -git+git://github.com/euresti/mypy.git@attrs_plugin#egg=mypy \ No newline at end of file diff --git a/tests/mypy_pytest_plugin.py b/tests/mypy_pytest_plugin.py index 31bfb67fc..ad8988734 100644 --- a/tests/mypy_pytest_plugin.py +++ b/tests/mypy_pytest_plugin.py @@ -11,7 +11,7 @@ from abc import abstractmethod from os import remove, rmdir from typing import ( - List, Tuple, Set, Optional, Iterator, Any, Dict, NamedTuple, Union + Any, Dict, Iterator, List, NamedTuple, Optional, Set, Tuple, Union ) import pytest # type: ignore # no pytest in typeshed From 4ac55903ea44649d8b91d5b2b08ed5b02d86782c Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sun, 15 Apr 2018 18:12:49 -0700 Subject: [PATCH 55/64] Fix tests on pypy3 (hopefully) --- setup.py | 12 ++++++------ tox.ini | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/setup.py b/setup.py index 24af9c38f..96c919971 100644 --- a/setup.py +++ b/setup.py @@ -32,6 +32,9 @@ "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules", ] +# mypy plugins are not yet externally distributable, so we must lock tests +# against a particular version of mypy. additionally, output formatting +# of mypy can vary by version. MYPY_VERSION = "mypy==0.580" INSTALL_REQUIRES = [] EXTRAS_REQUIRE = { @@ -48,16 +51,13 @@ "six", "zope.interface", ], + "test-stubs": [ + MYPY_VERSION + ], } EXTRAS_REQUIRE["dev"] = EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["docs"] -if PY3: - # mypy plugins are not yet externally distributable, so we must lock tests - # against a particular version of mypy. additionally, output formatting - # of mypy can vary by version. - EXTRAS_REQUIRE["tests"].append(MYPY_VERSION) - ############################################################################### HERE = os.path.abspath(os.path.dirname(__file__)) diff --git a/tox.ini b/tox.ini index 77ab11a04..6c4f457e6 100644 --- a/tox.ini +++ b/tox.ini @@ -84,5 +84,5 @@ commands = towncrier --draft [testenv:stubs] basepython = python3.6 -extras = tests +extras = tests,test-stubs commands = pytest tests/test_stubs.py From e13612c21e37dd4a2ae35030b51a8e64d80e95b9 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sat, 7 Jul 2018 14:20:36 -0700 Subject: [PATCH 56/64] Update stubs from typeshed Also update tests. --- setup.py | 2 +- src/attr/__init__.pyi | 226 ++++++---------------------------------- src/attr/validators.pyi | 4 +- tests/mypy.tests.py | 4 +- 4 files changed, 38 insertions(+), 198 deletions(-) diff --git a/setup.py b/setup.py index 96c919971..6569c698a 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ # mypy plugins are not yet externally distributable, so we must lock tests # against a particular version of mypy. additionally, output formatting # of mypy can vary by version. -MYPY_VERSION = "mypy==0.580" +MYPY_VERSION = "mypy==0.610" INSTALL_REQUIRES = [] EXTRAS_REQUIRE = { "docs": [ diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 099d2e601..eaf0900ce 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -21,11 +21,11 @@ _ValidatorArgType = Union[_ValidatorType[_T], Sequence[_ValidatorType[_T]]] NOTHING: object # NOTE: Factory lies about its return type to make this possible: `x: List[int] = Factory(list)` -# Work around mypy issue #4554 by using an overload vs a Union. +# Work around mypy issue #4554 in the common case by using an overload. @overload -def Factory(factory: Callable[[], _T], takes_self: bool = ...) -> _T: ... +def Factory(factory: Callable[[], _T]) -> _T: ... @overload -def Factory(factory: Callable[[Any], _T], takes_self: bool = ...) -> _T: ... +def Factory(factory: Union[Callable[[Any], _T], Callable[[], _T]], takes_self: bool = ...) -> _T: ... class Attribute(Generic[_T]): name: str @@ -69,22 +69,24 @@ class Attribute(Generic[_T]): # attr(validator=) -> Whatever the callable expects. # This makes this type of assignments possible: # x: int = attr(8) - -# 1st form catches _T set and works around mypy issue #4554 +# +# This form catches explicit None or no default but with no other arguments returns Any. @overload -def attrib(default: _T, - validator: Optional[_ValidatorArgType[_T]] = ..., +def attrib(default: None = ..., + validator: None = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., + convert: None = ..., metadata: Optional[Mapping[Any, Any]] = ..., - type: Optional[Type[_T]] = ..., - converter: Optional[_ConverterType[_T]] = ...) -> _T: ... -# 2nd one with an optional default. + type: None = ..., + converter: None = ..., + factory: None = ..., + ) -> Any: ... +# This form catches an explicit None or no default and infers the type from the other arguments. @overload -def attrib(default: Optional[_T] = ..., +def attrib(default: None = ..., validator: Optional[_ValidatorArgType[_T]] = ..., repr: bool = ..., cmp: bool = ..., @@ -93,20 +95,24 @@ def attrib(default: Optional[_T] = ..., convert: Optional[_ConverterType[_T]] = ..., metadata: Optional[Mapping[Any, Any]] = ..., type: Optional[Type[_T]] = ..., - converter: Optional[_ConverterType[_T]] = ...) -> _T: ... -# 3rd form no _T , so returns Any. + converter: Optional[_ConverterType[_T]] = ..., + factory: Optional[Callable[[], _T]] = ..., + ) -> _T: ... +# This form catches an explicit default argument. @overload -def attrib(default: None = ..., - validator: None = ..., +def attrib(default: _T, + validator: Optional[_ValidatorArgType[_T]] = ..., repr: bool = ..., cmp: bool = ..., hash: Optional[bool] = ..., init: bool = ..., - convert: None = ..., + convert: Optional[_ConverterType[_T]] = ..., metadata: Optional[Mapping[Any, Any]] = ..., - type: None = ..., - converter: None = ...) -> Any: ... -# 4th form covers type=non-Type: e.g. forward references (str), Any + type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType[_T]] = ..., + factory: Optional[Callable[[], _T]] = ..., + ) -> _T: ... +# This form covers type=non-Type: e.g. forward references (str), Any @overload def attrib(default: Optional[_T] = ..., validator: Optional[_ValidatorArgType[_T]] = ..., @@ -117,7 +123,9 @@ def attrib(default: Optional[_T] = ..., convert: Optional[_ConverterType[_T]] = ..., metadata: Optional[Mapping[Any, Any]] = ..., type: object = ..., - converter: Optional[_ConverterType[_T]] = ...) -> Any: ... + converter: Optional[_ConverterType[_T]] = ..., + factory: Optional[Callable[[], _T]] = ..., + ) -> Any: ... # NOTE: If you update these, update `s` and `attributes` below. @@ -152,6 +160,7 @@ class _Fields(Tuple[Attribute, ...]): def __getattr__(self, name: str) -> Attribute: ... def fields(cls: type) -> _Fields: ... +def fields_dict(cls: type) -> Dict[str, Attribute]: ... def validate(inst: Any) -> None: ... # TODO: add support for returning a proper attrs class from the mypy plugin @@ -198,175 +207,6 @@ def get_run_validators() -> bool: ... # aliases -- -# FIXME: there is a bug in PyCharm with creating aliases to overloads. -# Use the aliases instead of the duplicated overloads when the bug is fixed: -# https://youtrack.jetbrains.com/issue/PY-27788 - -# s = attributes = attrs -# ib = attr = attrib -# dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) - - -@overload -def ib(default: _T, - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: Optional[Type[_T]] = ..., - converter: Optional[_ConverterType[_T]] = ...) -> _T: ... -@overload -def ib(default: Optional[_T] = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: Optional[Type[_T]] = ..., - converter: Optional[_ConverterType[_T]] = ...) -> _T: ... -@overload -def ib(default: None = ..., - validator: None = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - convert: None = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: None = ..., - converter: None = ...) -> Any: ... -@overload -def ib(default: Optional[_T] = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: object = ..., - converter: Optional[_ConverterType[_T]] = ...) -> Any: ... - -@overload -def attr(default: _T, - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: Optional[Type[_T]] = ..., - converter: Optional[_ConverterType[_T]] = ...) -> _T: ... -@overload -def attr(default: Optional[_T] = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: Optional[Type[_T]] = ..., - converter: Optional[_ConverterType[_T]] = ...) -> _T: ... -@overload -def attr(default: None = ..., - validator: None = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - convert: None = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: None = ..., - converter: None = ...) -> Any: ... -@overload -def attr(default: Optional[_T] = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: object = ..., - converter: Optional[_ConverterType[_T]] = ...) -> Any: ... - -@overload -def attributes(maybe_cls: _C, - these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - str: bool = ..., - auto_attribs: bool = ...) -> _C: ... -@overload -def attributes(maybe_cls: None = ..., - these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - str: bool = ..., - auto_attribs: bool = ...) -> Callable[[_C], _C]: ... - -@overload -def s(maybe_cls: _C, - these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - str: bool = ..., - auto_attribs: bool = ...) -> _C: ... -@overload -def s(maybe_cls: None = ..., - these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - str: bool = ..., - auto_attribs: bool = ...) -> Callable[[_C], _C]: ... - -# same as above, but with auto_attrib=True -@overload -def dataclass(maybe_cls: _C, - these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - str: bool = ...) -> _C: ... -@overload -def dataclass(maybe_cls: None = ..., - these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - str: bool = ...) -> Callable[[_C], _C]: ... \ No newline at end of file +s = attributes = attrs +ib = attr = attrib +dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index 477bac91c..3fc34377c 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -1,9 +1,9 @@ -from typing import Container, List, Union, TypeVar, Type, Any, Optional +from typing import Container, List, Union, TypeVar, Type, Any, Optional, Tuple from . import _ValidatorType _T = TypeVar('_T') -def instance_of(type: Type[_T]) -> _ValidatorType[_T]: ... +def instance_of(type: Union[Tuple[Type[_T], ...], Type[_T]]) -> _ValidatorType[_T]: ... def provides(interface: Any) -> _ValidatorType[Any]: ... def optional(validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]]) -> _ValidatorType[Optional[_T]]: ... def in_(options: Container[_T]) -> _ValidatorType[_T]: ... diff --git a/tests/mypy.tests.py b/tests/mypy.tests.py index b13d3197a..c894ba8b0 100644 --- a/tests/mypy.tests.py +++ b/tests/mypy.tests.py @@ -286,7 +286,7 @@ class C: bb = attr.ib(type=int, validator=(in_([1, 2, 3]), instance_of(int))) bbb = attr.ib(type=int, validator=and_(in_([1, 2, 3]), instance_of(int))) - e = attr.ib(type=int, validator=1) # E: No overload variant of "ib" matches argument types [Overload(def (x: Union[builtins.str, builtins.bytes, typing.SupportsInt] =) -> builtins.int, def (x: Union[builtins.str, builtins.bytes], base: builtins.int) -> builtins.int), builtins.int] + e = attr.ib(type=int, validator=1) # E: No overload variant matches argument types "Type[int]", "int" # mypy does not know how to get the contained type from an enum: f = attr.ib(type=State, validator=in_(State)) @@ -328,7 +328,7 @@ class C: b = attr.ib(type=int, validator=[validate_int]) reveal_type(b) # E: Revealed type is 'builtins.int' - c = attr.ib(type=int, validator=validate_str) # E: Argument "validator" to "ib" has incompatible type "Callable[[Any, Any, str], Any]"; expected "Union[Callable[[Any, Attribute[Any], int], Any], Sequence[Callable[[Any, Attribute[Any], int], Any]]]" + c = attr.ib(type=int, validator=validate_str) # E: Argument "validator" has incompatible type "Callable[[Any, Any, str], Any]"; expected "Union[Callable[[Any, Attribute[Any], int], Any], Sequence[Callable[[Any, Attribute[Any], int], Any]], None]" # [case test_custom_validators_type_annotations] From af4dbcc17dfde384aa0280ce1ca4e78fd571d27d Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sun, 8 Jul 2018 18:50:42 -0700 Subject: [PATCH 57/64] Make PEP 561-compliant --- MANIFEST.in | 1 + setup.py | 2 +- src/attr/py.typed | 0 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 src/attr/py.typed diff --git a/MANIFEST.in b/MANIFEST.in index f294251da..c46e2befb 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,6 +4,7 @@ include LICENSE *.rst *.toml .readthedocs.yml .pre-commit-config.yaml exclude .github/*.md .travis.yml codecov.yml # Stubs +include src/py.typed recursive-include src *.pyi recursive-include tests *.test diff --git a/setup.py b/setup.py index 1124be3ac..acbe3e10c 100644 --- a/setup.py +++ b/setup.py @@ -125,7 +125,7 @@ def find_meta(meta): long_description=LONG, packages=PACKAGES, package_dir={"": "src"}, - package_data={'attr': ['*.pyi']}, + package_data={'attr': ['*.pyi', "py.typed"]}, zip_safe=False, classifiers=CLASSIFIERS, install_requires=INSTALL_REQUIRES, diff --git a/src/attr/py.typed b/src/attr/py.typed new file mode 100644 index 000000000..e69de29bb From b922698c88d8bda919cd1f87182e0f38e6fab98b Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Sun, 8 Jul 2018 18:50:49 -0700 Subject: [PATCH 58/64] minor cleanup --- setup.py | 3 --- src/attr/__init__.pyi | 3 --- 2 files changed, 6 deletions(-) diff --git a/setup.py b/setup.py index acbe3e10c..5dcf67a2d 100644 --- a/setup.py +++ b/setup.py @@ -1,13 +1,10 @@ import codecs import os import re -import sys from setuptools import find_packages, setup -PY3 = sys.version_info >= (3,) - ############################################################################### NAME = "attrs" diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index eaf0900ce..7dd004f0f 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -61,8 +61,6 @@ class Attribute(Generic[_T]): # We chose option #1 until either PyCharm adds support for attrs, or python 2 # reaches EOL. -# NOTE: If you update these, update `ib` and `attr` below. - # `attr` lies about its return type to make the following possible: # attr() -> Any # attr(8) -> int @@ -128,7 +126,6 @@ def attrib(default: Optional[_T] = ..., ) -> Any: ... -# NOTE: If you update these, update `s` and `attributes` below. @overload def attrs(maybe_cls: _C, these: Optional[Dict[str, Any]] = ..., From 989cd8816295fed3a850c0c2af2ccd62e0ccff16 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Tue, 10 Jul 2018 10:28:46 -0700 Subject: [PATCH 59/64] Consolidate stub support files into stub directory In preparation for removing them from the pyi_stubs branch. --- conftest.py | 2 -- {tests => stubs}/mypy.tests.py | 0 {tests => stubs}/mypy_pytest_plugin.py | 0 {docs => stubs}/setup.py | 0 {tests => stubs}/test_stubs.py | 0 tox.ini | 2 +- 6 files changed, 1 insertion(+), 3 deletions(-) rename {tests => stubs}/mypy.tests.py (100%) rename {tests => stubs}/mypy_pytest_plugin.py (100%) rename {docs => stubs}/setup.py (100%) rename {tests => stubs}/test_stubs.py (100%) diff --git a/conftest.py b/conftest.py index b281f3af9..c271ab1cd 100644 --- a/conftest.py +++ b/conftest.py @@ -36,5 +36,3 @@ class C(object): collect_ignore.extend( ["tests/test_annotations.py", "tests/test_init_subclass.py"] ) - -collect_ignore.append("tests/test_stubs.py") diff --git a/tests/mypy.tests.py b/stubs/mypy.tests.py similarity index 100% rename from tests/mypy.tests.py rename to stubs/mypy.tests.py diff --git a/tests/mypy_pytest_plugin.py b/stubs/mypy_pytest_plugin.py similarity index 100% rename from tests/mypy_pytest_plugin.py rename to stubs/mypy_pytest_plugin.py diff --git a/docs/setup.py b/stubs/setup.py similarity index 100% rename from docs/setup.py rename to stubs/setup.py diff --git a/tests/test_stubs.py b/stubs/test_stubs.py similarity index 100% rename from tests/test_stubs.py rename to stubs/test_stubs.py diff --git a/tox.ini b/tox.ini index d3870d0dd..a46d3d33e 100644 --- a/tox.ini +++ b/tox.ini @@ -90,4 +90,4 @@ commands = towncrier --draft [testenv:stubs] basepython = python3.6 extras = tests,test-stubs -commands = pytest tests/test_stubs.py +commands = pytest stubs/test_stubs.py From 875d2acebf3e75572b2d4202406d1da247a308fd Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Tue, 10 Jul 2018 10:29:53 -0700 Subject: [PATCH 60/64] Get tests passing This is a final test of the current stubs before moving the stub tests to a new branch. --- .pre-commit-config.yaml | 2 ++ docs/conf.py | 10 +++++----- docs/examples.rst | 4 ++-- docs/glossary.rst | 8 ++++++-- docs/init.rst | 33 +++++++++++++++++++-------------- setup.cfg | 2 +- setup.py | 12 +++--------- stubs/test_stubs.py | 4 ++-- 8 files changed, 40 insertions(+), 35 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d75c0c6e6..190851fba 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,3 +16,5 @@ repos: hooks: - id: isort language_version: python3.6 + +exclude: stubs/.* \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index ee33fc1ec..2c9882860 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -40,13 +40,13 @@ def find_version(*file_paths): # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'doctest2', - 'sphinx.ext.intersphinx', - 'sphinx.ext.todo', + "sphinx.ext.autodoc", + "doctest2", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", ] -doctest_path = [os.path.join(HERE, '..', 'src')] +doctest_path = [os.path.join(HERE, "..", "src")] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/examples.rst b/docs/examples.rst index 428c14795..54562d2d5 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -343,7 +343,7 @@ You can use a decorator: ...or both at once: -.. doctest:: validators1 +.. doctest:: validators2 >>> @attr.s ... class C(object): @@ -428,7 +428,7 @@ Types ``attrs`` also allows you to associate a type with an attribute using either the *type* argument to :func:`attr.ib` or -- as of Python 3.6 -- using `PEP 526 `_-annotations: -.. doctest:: +.. doctest:: types1 >>> @attr.s ... class C: diff --git a/docs/glossary.rst b/docs/glossary.rst index 2fdf80627..e9a64f3ec 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -1,6 +1,10 @@ Glossary ======== +.. testsetup:: * + + import attr + .. glossary:: dict classes @@ -19,7 +23,7 @@ Glossary - Slotted classes don't allow for any other attribute to be set except for those defined in one of the class' hierarchies ``__slots__``: - .. doctest:: + .. doctest:: glossary1 >>> import attr >>> @attr.s(slots=True) @@ -46,7 +50,7 @@ Glossary - As always with slotted classes, you must specify a ``__weakref__`` slot if you wish for the class to be weak-referenceable. Here's how it looks using ``attrs``: - .. doctest:: + .. doctest:: glossary2 >>> import weakref >>> @attr.s(slots=True) diff --git a/docs/init.rst b/docs/init.rst index e2cdd0392..6e80d7be0 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -1,6 +1,11 @@ Initialization ============== +.. testsetup:: * + + import attr + + In Python, instance intialization happens in the ``__init__`` method. Generally speaking, you should keep as little logic as possible in it, and you should think about what the class needs and not how it is going to be instantiated. @@ -49,7 +54,7 @@ Private Attributes One thing people tend to find confusing is the treatment of private attributes that start with an underscore. ``attrs`` follows the doctrine that `there is no such thing as a private argument`_ and strips the underscores from the name when writing the ``__init__`` method signature: -.. doctest:: +.. doctest:: private1 >>> import inspect, attr >>> @attr.s @@ -61,7 +66,7 @@ One thing people tend to find confusing is the treatment of private attributes t There really isn't a right or wrong, it's a matter of taste. But it's important to be aware of it because it can lead to surprising syntax errors: -.. doctest:: +.. doctest:: private2 >>> @attr.s ... class C(object): @@ -81,7 +86,7 @@ And sometimes, certain attributes aren't even intended to be passed but you want This is when default values come into play: -.. doctest:: +.. doctest:: defaults1 >>> import attr >>> @attr.s @@ -101,7 +106,7 @@ It's important that the decorated method -- or any other method or property! -- Please note that as with function and method signatures, ``default=[]`` will *not* do what you may think it might do: -.. doctest:: +.. doctest:: defaults2 >>> @attr.s ... class C(object): @@ -173,7 +178,7 @@ It takes either a callable or a list of callables (usually functions) and treats Since the validators runs *after* the instance is initialized, you can refer to other attributes while validating: -.. doctest:: +.. doctest:: callables1 >>> def x_smaller_than_y(instance, attribute, value): ... if value >= instance.y: @@ -194,7 +199,7 @@ This example also shows of some syntactic sugar for using the :func:`attr.valida ``attrs`` won't intercept your changes to those attributes but you can always call :func:`attr.validate` on any instance to verify that it's still valid: -.. doctest:: +.. doctest:: callables1 >>> i = C(4, 5) >>> i.x = 5 # works, no magic here @@ -205,7 +210,7 @@ This example also shows of some syntactic sugar for using the :func:`attr.valida ``attrs`` ships with a bunch of validators, make sure to :ref:`check them out ` before writing your own: -.. doctest:: +.. doctest:: callables2 >>> @attr.s ... class C(object): @@ -220,7 +225,7 @@ This example also shows of some syntactic sugar for using the :func:`attr.valida Of course you can mix and match the two approaches at your convenience. If you define validators both ways for an attribute, they are both ran: -.. doctest:: +.. doctest:: callables3 >>> @attr.s ... class C(object): @@ -263,7 +268,7 @@ For that ``attrs`` comes with converters. Attributes can have a ``converter`` function specified, which will be called with the attribute's passed-in value to get a new value to use. This can be useful for doing type-conversions on values that you don't want to force your callers to do. -.. doctest:: +.. doctest:: converters1 >>> @attr.s ... class C(object): @@ -274,7 +279,7 @@ This can be useful for doing type-conversions on values that you don't want to f Converters are run *before* validators, so you can use validators to check the final form of the value. -.. doctest:: +.. doctest:: converters2 >>> def validate_x(instance, attribute, value): ... if value < 0: @@ -293,7 +298,7 @@ Converters are run *before* validators, so you can use validators to check the f Arguably, you can abuse converters as one-argument validators: -.. doctest:: +.. doctest:: converters2 >>> C("x") Traceback (most recent call last): @@ -309,7 +314,7 @@ Generally speaking, the moment you think that you need finer control over how yo However, sometimes you need to do that one quick thing after your class is initialized. And for that ``attrs`` offers the ``__attrs_post_init__`` hook that is automatically detected and run after ``attrs`` is done initializing your instance: -.. doctest:: +.. doctest:: post_init1 >>> @attr.s ... class C(object): @@ -322,7 +327,7 @@ And for that ``attrs`` offers the ``__attrs_post_init__`` hook that is automatic Please note that you can't directly set attributes on frozen classes: -.. doctest:: +.. doctest:: post_init2 >>> @attr.s(frozen=True) ... class FrozenBroken(object): @@ -337,7 +342,7 @@ Please note that you can't directly set attributes on frozen classes: If you need to set attributes on a frozen class, you'll have to resort to the :ref:`same trick ` as ``attrs`` and use :meth:`object.__setattr__`: -.. doctest:: +.. doctest:: post_init3 >>> @attr.s(frozen=True) ... class Frozen(object): diff --git a/setup.cfg b/setup.cfg index 9793fdc1b..2adc3b6a4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,7 @@ multi_line_output=3 not_skip=__init__.py known_first_party=attr -known_third_party=UserDict,attr,hypothesis,pympler,pytest,setuptools,six,zope +known_third_party=UserDict,attr,hypothesis,pympler,pytest,setuptools,six,sphinx,zope [flake8] exclude = tests/mypy.tests.py diff --git a/setup.py b/setup.py index 5dcf67a2d..dcf628f3c 100644 --- a/setup.py +++ b/setup.py @@ -40,11 +40,7 @@ MYPY_VERSION = "mypy==0.610" INSTALL_REQUIRES = [] EXTRAS_REQUIRE = { - "docs": [ - "sphinx", - "zope.interface", - MYPY_VERSION, - ], + "docs": ["sphinx", "zope.interface", MYPY_VERSION], "tests": [ "coverage", "hypothesis", @@ -53,9 +49,7 @@ "six", "zope.interface", ], - "test-stubs": [ - MYPY_VERSION - ], + "test-stubs": [MYPY_VERSION], } EXTRAS_REQUIRE["dev"] = ( EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["docs"] + ["pre-commit"] @@ -122,7 +116,7 @@ def find_meta(meta): long_description=LONG, packages=PACKAGES, package_dir={"": "src"}, - package_data={'attr': ['*.pyi', "py.typed"]}, + package_data={"attr": ["*.pyi", "py.typed"]}, zip_safe=False, classifiers=CLASSIFIERS, install_requires=INSTALL_REQUIRES, diff --git a/stubs/test_stubs.py b/stubs/test_stubs.py index 0f8add667..13ef72802 100644 --- a/stubs/test_stubs.py +++ b/stubs/test_stubs.py @@ -5,12 +5,12 @@ import subprocess import sys -from tests.mypy_pytest_plugin import ( +from mypy_pytest_plugin import ( DataSuite, assert_string_arrays_equal, normalize_error_messages ) -pytest_plugins = ['tests.mypy_pytest_plugin'] +pytest_plugins = ['mypy_pytest_plugin'] # Path to Python 3 interpreter python3_path = sys.executable From 60dd4895645bd479b36b951026a441252ab9d9e9 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Tue, 10 Jul 2018 13:04:36 -0700 Subject: [PATCH 61/64] Revert stub test additions Replace with a simple mypy pass/fail test --- .gitignore | 2 + .pre-commit-config.yaml | 2 - .travis.yml | 2 +- MANIFEST.in | 1 - conftest.py | 1 - docs/api.rst | 40 +- docs/conf.py | 12 +- docs/doctest2.py | 197 -------- docs/examples.rst | 61 +-- docs/extending.rst | 8 +- docs/glossary.rst | 8 +- docs/init.rst | 33 +- docs/why.rst | 6 +- setup.cfg | 5 +- setup.py | 8 +- stubs/mypy.tests.py | 427 ---------------- stubs/mypy_pytest_plugin.py | 968 ------------------------------------ stubs/setup.py | 8 - stubs/test_stubs.py | 79 --- tests/typing_example.py | 90 ++++ tox.ini | 10 +- 21 files changed, 166 insertions(+), 1802 deletions(-) delete mode 100644 docs/doctest2.py delete mode 100644 stubs/mypy.tests.py delete mode 100644 stubs/mypy_pytest_plugin.py delete mode 100644 stubs/setup.py delete mode 100644 stubs/test_stubs.py create mode 100644 tests/typing_example.py diff --git a/.gitignore b/.gitignore index 6c895ee1d..806688363 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ .cache .coverage* .hypothesis +.idea +.mypy_cache .pytest_cache .tox build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 190851fba..d75c0c6e6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,5 +16,3 @@ repos: hooks: - id: isort language_version: python3.6 - -exclude: stubs/.* \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 81760125d..302a90021 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,7 +47,7 @@ matrix: - python: "3.6" env: TOXENV=changelog - python: "3.6" - env: TOXENV=stubs + env: TOXENV=typing allow_failures: - python: "3.6-dev" diff --git a/MANIFEST.in b/MANIFEST.in index c46e2befb..699033de3 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,7 +6,6 @@ exclude .github/*.md .travis.yml codecov.yml # Stubs include src/py.typed recursive-include src *.pyi -recursive-include tests *.test # Tests include tox.ini .coveragerc conftest.py diff --git a/conftest.py b/conftest.py index c271ab1cd..9f84c3451 100644 --- a/conftest.py +++ b/conftest.py @@ -31,7 +31,6 @@ class C(object): collect_ignore = [] - if sys.version_info[:2] < (3, 6): collect_ignore.extend( ["tests/test_annotations.py", "tests/test_init_subclass.py"] diff --git a/docs/api.rst b/docs/api.rst index 0dcc2e1f5..3ba23603e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -13,9 +13,7 @@ API Reference What follows is the API explanation, if you'd like a more hands-on introduction, have a look at :doc:`examples`. -.. testsetup:: * - import attr Core ---- @@ -28,7 +26,7 @@ Core For example: - .. doctest:: core + .. doctest:: >>> import attr >>> @attr.s @@ -36,10 +34,6 @@ Core ... _private = attr.ib() >>> C(private=42) C(_private=42) - - .. doctest:: core - :options: +MYPY_SKIP - >>> class D(object): ... def __init__(self, x): ... self.x = x @@ -58,7 +52,7 @@ Core The object returned by :func:`attr.ib` also allows for setting the default and the validator using decorators: - .. doctest:: attrib + .. doctest:: >>> @attr.s ... class C(object): @@ -89,7 +83,7 @@ Core You should never instantiate this class yourself! - .. doctest:: Attribute + .. doctest:: >>> import attr >>> @attr.s @@ -105,7 +99,7 @@ Core For example: - .. doctest:: make_class + .. doctest:: >>> C1 = attr.make_class("C1", ["x", "y"]) >>> C1(1, 2) @@ -120,7 +114,7 @@ Core For example: - .. doctest:: Factory + .. doctest:: >>> @attr.s ... class C(object): @@ -160,7 +154,7 @@ Helpers For example: - .. doctest:: fields + .. doctest:: >>> @attr.s ... class C(object): @@ -195,7 +189,7 @@ Helpers For example: - .. doctest:: has + .. doctest:: >>> @attr.s ... class C(object): @@ -210,7 +204,7 @@ Helpers For example: - .. doctest:: asdict + .. doctest:: >>> @attr.s ... class C(object): @@ -224,7 +218,7 @@ Helpers For example: - .. doctest:: astuple + .. doctest:: >>> @attr.s ... class C(object): @@ -245,7 +239,7 @@ See :ref:`asdict` for examples. For example: - .. doctest:: evolve + .. doctest:: >>> @attr.s ... class C(object): @@ -271,13 +265,13 @@ See :ref:`asdict` for examples. For example: - .. doctest:: validate + .. doctest:: >>> @attr.s ... class C(object): ... x = attr.ib(validator=attr.validators.instance_of(int)) >>> i = C(1) - >>> i.x = "1" # mypy error: Incompatible types in assignment (expression has type "str", variable has type "int") + >>> i.x = "1" >>> attr.validate(i) Traceback (most recent call last): ... @@ -304,7 +298,7 @@ Validators For example: - .. doctest:: validators.instance_of + .. doctest:: >>> @attr.s ... class C(object): @@ -324,7 +318,7 @@ Validators For example: - .. doctest:: validators.in_ + .. doctest:: >>> import enum >>> class State(enum.Enum): @@ -332,7 +326,7 @@ Validators ... OFF = "off" >>> @attr.s ... class C(object): - ... state = attr.ib(validator=attr.validators.in_(State)) # type: ignore + ... state = attr.ib(validator=attr.validators.in_(State)) ... val = attr.ib(validator=attr.validators.in_([1, 2, 3])) >>> C(State.ON, 1) C(state=, val=1) @@ -360,7 +354,7 @@ Validators For example: - .. doctest:: validators.optional + .. doctest:: >>> @attr.s ... class C(object): @@ -382,7 +376,7 @@ Converters For example: - .. doctest:: converters.optional + .. doctest:: >>> @attr.s ... class C(object): diff --git a/docs/conf.py b/docs/conf.py index 2c9882860..97b2329eb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -3,12 +3,6 @@ import codecs import os import re -import sys - - -HERE = os.path.abspath(os.path.dirname(__file__)) -# to find the doctest2 extension: -sys.path.append(HERE) def read(*parts): @@ -16,7 +10,8 @@ def read(*parts): Build an absolute path from *parts* and and return the contents of the resulting file. Assume UTF-8 encoding. """ - with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f: + here = os.path.abspath(os.path.dirname(__file__)) + with codecs.open(os.path.join(here, *parts), "rb", "utf-8") as f: return f.read() @@ -41,12 +36,11 @@ def find_version(*file_paths): # ones. extensions = [ "sphinx.ext.autodoc", - "doctest2", + "sphinx.ext.doctest", "sphinx.ext.intersphinx", "sphinx.ext.todo", ] -doctest_path = [os.path.join(HERE, "..", "src")] # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/doctest2.py b/docs/doctest2.py deleted file mode 100644 index b07a3af13..000000000 --- a/docs/doctest2.py +++ /dev/null @@ -1,197 +0,0 @@ -from __future__ import absolute_import, print_function - -import re -import sys -import sphinx -import doctest -import subprocess -import tempfile -import os -import shutil - -import sphinx.ext.doctest -from sphinx.ext.doctest import (TestsetupDirective, TestcleanupDirective, - DoctestDirective, TestcodeDirective, - TestoutputDirective, DocTestBuilder) - -# Path to Python 3 interpreter -python3_path = sys.executable - -HERE = os.path.abspath(os.path.dirname(__file__)) - -MAIN = 'main' - -type_comment_re = re.compile(r'#\s*type:\s*ignore\b.*$', re.MULTILINE) - - -MYPY_SKIP = doctest.register_optionflag('MYPY_SKIP') - - -def convert_source(input): - for i in range(len(input)): - # FIXME: convert to regex - input[i] = input[i].replace('# mypy error:', '# E:') - input[i] = input[i].replace('#doctest: +MYPY_IGNORE', '# type: ignore') - return input - - -def expand_errors(input, output, fnam: str): - """Transform comments such as '# E: message' or - '# E:3: message' in input. - - The result is lines like 'fnam:line: error: message'. - """ - - for i in range(len(input)): - # The first in the split things isn't a comment - for possible_err_comment in input[i].split(' # ')[1:]: - m = re.search( - '^([ENW]):((?P\d+):)? (?P.*)$', - possible_err_comment.strip()) - if m: - if m.group(1) == 'E': - severity = 'error' - elif m.group(1) == 'N': - severity = 'note' - elif m.group(1) == 'W': - severity = 'warning' - col = m.group('col') - if col is None: - output.append( - '{}:{}: {}: {}'.format(fnam, i + 1, severity, - m.group('message'))) - else: - output.append('{}:{}:{}: {}: {}'.format( - fnam, i + 1, col, severity, m.group('message'))) - - -# Override SphinxDocTestRunner to gather the source for each group. -class SphinxDocTestRunner(sphinx.ext.doctest.SphinxDocTestRunner): - group_source = '' - - def reset_source(self): - self.group_source = '' - - def run(self, test, compileflags=None, out=None, clear_globs=True): - # add the source for this block to the group - result = doctest.DocTestRunner.run(self, test, compileflags, out, - clear_globs) - sources = [example.source for example in test.examples - if not example.options.get(MYPY_SKIP, False)] - self.group_source += ''.join(sources) - return result - - -# patch the runner -sphinx.ext.doctest.SphinxDocTestRunner = SphinxDocTestRunner - -# _orig_run = sphinx.ext.doctest.TestDirective.run -# def _new_run(self): -# nodes = _orig_run(self) -# node = nodes[0] -# code = node.rawsource -# test = None -# if 'test' in node: -# test = node['test'] -# -# if type_comment_re.search(code): -# print("here") -# if not test: -# test = code -# node.rawsource = type_comment_re.sub('', code) -# print(node.rawsource) -# if test is not None: -# # only save if it differs from code -# node['test'] = test -# return nodes - -# sphinx.ext.doctest.TestDirective.run = _new_run - - -class DocTest2Builder(DocTestBuilder): - def test_group(self, group, *args, **kwargs): - self.setup_runner.reset_source() - self.test_runner.reset_source() - self.cleanup_runner.reset_source() - - result = DocTestBuilder.test_group(self, group, *args, **kwargs) - - source = (self.setup_runner.group_source + - self.test_runner.group_source + - self.cleanup_runner.group_source) - - want_lines = [] - lines = convert_source(source.splitlines(keepends=True)) - expand_errors(lines, want_lines, MAIN) - source = ''.join(lines) - want = '\n'.join(want_lines) + '\n' if want_lines else '' - got = run_mypy(source, self.config.doctest_path) - if want != got: - test = doctest.DocTest([], {}, group.name, '', 0, None) - example = doctest.Example(source, want) - # if not quiet: - self.test_runner.report_failure(self._warn_out, test, example, - got) - # we hardwire no. of failures and no. of tries to 1 - self.test_runner._DocTestRunner__record_outcome(test, 1, 1) - - return result - - -def run_mypy(code, mypy_path): - """ - Returns error output - """ - test_temp_dir = tempfile.mkdtemp() - program_path = os.path.join(test_temp_dir, MAIN) - with open(program_path, 'w') as file: - file.write(code) - args = [ - MAIN, - '--hide-error-context', # don't precede errors w/ notes about context - '--show-traceback', - ] - # Type check the program. - env = {'MYPYPATH': os.path.pathsep.join(mypy_path)} - fixed = [python3_path, '-m', 'mypy'] - process = subprocess.Popen(fixed + args, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - env=env, - cwd=test_temp_dir) - outb = process.stdout.read() - # Remove temp file. - os.remove(program_path) - shutil.rmtree(test_temp_dir) - return str(outb, 'utf8') - # return str(outb, 'utf8').splitlines() - # Split output into lines and strip the file name. - # out = [] - # for line in str(outb, 'utf8').splitlines(): - # parts = line.split(':', 1) - # if len(parts) == 2: - # out.append(parts[1]) - # else: - # out.append(line) - # return out - - -def setup(app): - app.add_directive('testsetup', TestsetupDirective) - app.add_directive('testcleanup', TestcleanupDirective) - app.add_directive('doctest', DoctestDirective) - app.add_directive('testcode', TestcodeDirective) - app.add_directive('testoutput', TestoutputDirective) - app.add_builder(DocTest2Builder) - # this config value adds to sys.path - app.add_config_value('doctest_path', [], False) - app.add_config_value('doctest_test_doctest_blocks', 'default', False) - app.add_config_value('doctest_global_setup', '', False) - app.add_config_value('doctest_global_cleanup', '', False) - app.add_config_value( - 'doctest_default_flags', - (doctest.DONT_ACCEPT_TRUE_FOR_1 | - doctest.ELLIPSIS | - doctest.IGNORE_EXCEPTION_DETAIL), - False) - return {'version': sphinx.__display_version__, 'parallel_read_safe': True} diff --git a/docs/examples.rst b/docs/examples.rst index 54562d2d5..e8014188b 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -4,10 +4,6 @@ ==================== -.. testsetup:: * - - import attr - Basics ------ @@ -68,7 +64,7 @@ If playful naming turns you off, ``attrs`` comes with serious business aliases: For private attributes, ``attrs`` will strip the leading underscores for keyword arguments: -.. doctest:: private +.. doctest:: >>> @attr.s ... class C(object): @@ -78,14 +74,14 @@ For private attributes, ``attrs`` will strip the leading underscores for keyword If you want to initialize your private attributes yourself, you can do that too: -.. doctest:: private_init +.. doctest:: >>> @attr.s ... class C(object): ... _x = attr.ib(init=False, default=42) >>> C() C(_x=42) - >>> C(23) # mypy error: Too many arguments for "C" + >>> C(23) Traceback (most recent call last): ... TypeError: __init__() takes exactly 1 argument (2 given) @@ -93,8 +89,7 @@ If you want to initialize your private attributes yourself, you can do that too: An additional way of defining attributes is supported too. This is useful in times when you want to enhance classes that are not yours (nice ``__repr__`` for Django models anyone?): -.. doctest:: enhance - :options: +MYPY_SKIP +.. doctest:: >>> class SomethingFromSomeoneElse(object): ... def __init__(self, x): @@ -109,7 +104,7 @@ This is useful in times when you want to enhance classes that are not yours (nic `Subclassing is bad for you `_, but ``attrs`` will still do what you'd hope for: -.. doctest:: subclassing +.. doctest:: >>> @attr.s ... class A(object): @@ -136,7 +131,7 @@ In Python 3, classes defined within other classes are `detected >> @attr.s ... class C(object): @@ -165,7 +160,7 @@ When you have a class with data, it often is very convenient to transform that c Some fields cannot or should not be transformed. For that, :func:`attr.asdict` offers a callback that decides whether an attribute should be included: -.. doctest:: asdict_filtered +.. doctest:: >>> @attr.s ... class UserList(object): @@ -181,7 +176,7 @@ For that, :func:`attr.asdict` offers a callback that decides whether an attribut For the common case where you want to :func:`include ` or :func:`exclude ` certain types or attributes, ``attrs`` ships with a few helpers: -.. doctest:: asdict_include_exclude +.. doctest:: >>> @attr.s ... class User(object): @@ -203,7 +198,7 @@ For the common case where you want to :func:`include ` or Other times, all you want is a tuple and ``attrs`` won't let you down: -.. doctest:: astuple +.. doctest:: >>> import sqlite3 >>> import attr @@ -230,7 +225,7 @@ Sometimes you want to have default values for your initializer. And sometimes you even want mutable objects as default values (ever used accidentally ``def f(arg=[])``?). ``attrs`` has you covered in both cases: -.. doctest:: defaults +.. doctest:: >>> import collections >>> @attr.s @@ -272,7 +267,7 @@ More information on why class methods for constructing objects are awesome can b Default factories can also be set using a decorator. The method receives the partially initialized instance which enables you to base a default value on other attributes: -.. doctest:: default_decorator +.. doctest:: >>> @attr.s ... class C(object): @@ -306,7 +301,7 @@ Although your initializers should do as little as possible (ideally: just initia You can use a decorator: -.. doctest:: validator_decorator +.. doctest:: >>> @attr.s ... class C(object): @@ -324,7 +319,7 @@ You can use a decorator: ...or a callable... -.. doctest:: validators1 +.. doctest:: >>> def x_smaller_than_y(instance, attribute, value): ... if value >= instance.y: @@ -343,7 +338,7 @@ You can use a decorator: ...or both at once: -.. doctest:: validators2 +.. doctest:: >>> @attr.s ... class C(object): @@ -366,7 +361,7 @@ You can use a decorator: ``attrs`` ships with a bunch of validators, make sure to :ref:`check them out ` before writing your own: -.. doctest:: validators3 +.. doctest:: >>> @attr.s ... class C(object): @@ -387,7 +382,7 @@ Conversion Attributes can have a ``converter`` function specified, which will be called with the attribute's passed-in value to get a new value to use. This can be useful for doing type-conversions on values that you don't want to force your callers to do. -.. doctest:: converters1 +.. doctest:: >>> @attr.s ... class C(object): @@ -406,7 +401,7 @@ Metadata All ``attrs`` attributes may include arbitrary metadata in the form of a read-only dictionary. -.. doctest:: metadata +.. doctest:: >>> @attr.s ... class C(object): @@ -428,7 +423,7 @@ Types ``attrs`` also allows you to associate a type with an attribute using either the *type* argument to :func:`attr.ib` or -- as of Python 3.6 -- using `PEP 526 `_-annotations: -.. doctest:: types1 +.. doctest:: >>> @attr.s ... class C: @@ -438,8 +433,6 @@ Types >>> attr.fields(C).y.type - >>> attr.fields(C)[1].type - If you don't mind annotating *all* attributes, you can even drop the :func:`attr.ib` and assign default values instead: @@ -484,7 +477,7 @@ Slots :term:`Slotted classes` have a bunch of advantages on CPython. Defining ``__slots__`` by hand is tedious, in ``attrs`` it's just a matter of passing ``slots=True``: -.. doctest:: slots1 +.. doctest:: >>> @attr.s(slots=True) ... class Coordinates(object): @@ -499,13 +492,13 @@ Sometimes you have instances that shouldn't be changed after instantiation. Immutability is especially popular in functional programming and is generally a very good thing. If you'd like to enforce it, ``attrs`` will try to help: -.. doctest:: frozen1 +.. doctest:: >>> @attr.s(frozen=True) ... class C(object): ... x = attr.ib() >>> i = C(1) - >>> i.x = 2 # mypy error: Property "x" defined in "C" is read-only + >>> i.x = 2 Traceback (most recent call last): ... attr.exceptions.FrozenInstanceError: can't set attribute @@ -518,7 +511,7 @@ By themselves, immutable classes are useful for long-lived objects that should n In order to use them in regular program flow, you'll need a way to easily create new instances with changed attributes. In Clojure that function is called `assoc `_ and ``attrs`` shamelessly imitates it: :func:`attr.evolve`: -.. doctest:: frozen2 +.. doctest:: >>> @attr.s(frozen=True) ... class C(object): @@ -540,7 +533,7 @@ Other Goodies Sometimes you may want to create a class programmatically. ``attrs`` won't let you down and gives you :func:`attr.make_class` : -.. doctest:: make_class +.. doctest:: >>> @attr.s ... class C1(object): @@ -552,7 +545,7 @@ Sometimes you may want to create a class programmatically. You can still have power over the attributes if you pass a dictionary of name: ``attr.ib`` mappings and can pass arguments to ``@attr.s``: -.. doctest:: make_class +.. doctest:: >>> C = attr.make_class("C", {"x": attr.ib(default=42), ... "y": attr.ib(default=attr.Factory(list))}, @@ -567,7 +560,7 @@ You can still have power over the attributes if you pass a dictionary of name: ` If you need to dynamically make a class with :func:`attr.make_class` and it needs to be a subclass of something else than ``object``, use the ``bases`` argument: -.. doctest:: make_subclass +.. doctest:: >>> class D(object): ... def __eq__(self, other): @@ -582,7 +575,7 @@ using ``@attr.s``. To do this, just define a ``__attrs_post_init__`` method in your class. It will get called at the end of the generated ``__init__`` method. -.. doctest:: post_init +.. doctest:: >>> @attr.s ... class C(object): @@ -598,7 +591,7 @@ It will get called at the end of the generated ``__init__`` method. Finally, you can exclude single attributes from certain methods: -.. doctest:: exclude +.. doctest:: >>> @attr.s ... class C(object): diff --git a/docs/extending.rst b/docs/extending.rst index 24a00dffe..77f3f6447 100644 --- a/docs/extending.rst +++ b/docs/extending.rst @@ -3,10 +3,6 @@ Extending ========= -.. testsetup:: * - - import attr - Each ``attrs``-decorated class has a ``__attrs_attrs__`` class attribute. It is a tuple of :class:`attr.Attribute` carrying meta-data about each attribute. @@ -60,7 +56,7 @@ Types This information is available to you: -.. doctest:: types +.. doctest:: >>> import attr >>> @attr.s @@ -100,7 +96,7 @@ Here are some tips for effective use of metadata: - Expose ``attr.ib`` wrappers for your specific metadata. This is a more graceful approach if your users don't require metadata from other libraries. - .. doctest:: metadata + .. doctest:: >>> MY_TYPE_METADATA = '__my_type_metadata' >>> diff --git a/docs/glossary.rst b/docs/glossary.rst index e9a64f3ec..2fdf80627 100644 --- a/docs/glossary.rst +++ b/docs/glossary.rst @@ -1,10 +1,6 @@ Glossary ======== -.. testsetup:: * - - import attr - .. glossary:: dict classes @@ -23,7 +19,7 @@ Glossary - Slotted classes don't allow for any other attribute to be set except for those defined in one of the class' hierarchies ``__slots__``: - .. doctest:: glossary1 + .. doctest:: >>> import attr >>> @attr.s(slots=True) @@ -50,7 +46,7 @@ Glossary - As always with slotted classes, you must specify a ``__weakref__`` slot if you wish for the class to be weak-referenceable. Here's how it looks using ``attrs``: - .. doctest:: glossary2 + .. doctest:: >>> import weakref >>> @attr.s(slots=True) diff --git a/docs/init.rst b/docs/init.rst index 6e80d7be0..e2cdd0392 100644 --- a/docs/init.rst +++ b/docs/init.rst @@ -1,11 +1,6 @@ Initialization ============== -.. testsetup:: * - - import attr - - In Python, instance intialization happens in the ``__init__`` method. Generally speaking, you should keep as little logic as possible in it, and you should think about what the class needs and not how it is going to be instantiated. @@ -54,7 +49,7 @@ Private Attributes One thing people tend to find confusing is the treatment of private attributes that start with an underscore. ``attrs`` follows the doctrine that `there is no such thing as a private argument`_ and strips the underscores from the name when writing the ``__init__`` method signature: -.. doctest:: private1 +.. doctest:: >>> import inspect, attr >>> @attr.s @@ -66,7 +61,7 @@ One thing people tend to find confusing is the treatment of private attributes t There really isn't a right or wrong, it's a matter of taste. But it's important to be aware of it because it can lead to surprising syntax errors: -.. doctest:: private2 +.. doctest:: >>> @attr.s ... class C(object): @@ -86,7 +81,7 @@ And sometimes, certain attributes aren't even intended to be passed but you want This is when default values come into play: -.. doctest:: defaults1 +.. doctest:: >>> import attr >>> @attr.s @@ -106,7 +101,7 @@ It's important that the decorated method -- or any other method or property! -- Please note that as with function and method signatures, ``default=[]`` will *not* do what you may think it might do: -.. doctest:: defaults2 +.. doctest:: >>> @attr.s ... class C(object): @@ -178,7 +173,7 @@ It takes either a callable or a list of callables (usually functions) and treats Since the validators runs *after* the instance is initialized, you can refer to other attributes while validating: -.. doctest:: callables1 +.. doctest:: >>> def x_smaller_than_y(instance, attribute, value): ... if value >= instance.y: @@ -199,7 +194,7 @@ This example also shows of some syntactic sugar for using the :func:`attr.valida ``attrs`` won't intercept your changes to those attributes but you can always call :func:`attr.validate` on any instance to verify that it's still valid: -.. doctest:: callables1 +.. doctest:: >>> i = C(4, 5) >>> i.x = 5 # works, no magic here @@ -210,7 +205,7 @@ This example also shows of some syntactic sugar for using the :func:`attr.valida ``attrs`` ships with a bunch of validators, make sure to :ref:`check them out ` before writing your own: -.. doctest:: callables2 +.. doctest:: >>> @attr.s ... class C(object): @@ -225,7 +220,7 @@ This example also shows of some syntactic sugar for using the :func:`attr.valida Of course you can mix and match the two approaches at your convenience. If you define validators both ways for an attribute, they are both ran: -.. doctest:: callables3 +.. doctest:: >>> @attr.s ... class C(object): @@ -268,7 +263,7 @@ For that ``attrs`` comes with converters. Attributes can have a ``converter`` function specified, which will be called with the attribute's passed-in value to get a new value to use. This can be useful for doing type-conversions on values that you don't want to force your callers to do. -.. doctest:: converters1 +.. doctest:: >>> @attr.s ... class C(object): @@ -279,7 +274,7 @@ This can be useful for doing type-conversions on values that you don't want to f Converters are run *before* validators, so you can use validators to check the final form of the value. -.. doctest:: converters2 +.. doctest:: >>> def validate_x(instance, attribute, value): ... if value < 0: @@ -298,7 +293,7 @@ Converters are run *before* validators, so you can use validators to check the f Arguably, you can abuse converters as one-argument validators: -.. doctest:: converters2 +.. doctest:: >>> C("x") Traceback (most recent call last): @@ -314,7 +309,7 @@ Generally speaking, the moment you think that you need finer control over how yo However, sometimes you need to do that one quick thing after your class is initialized. And for that ``attrs`` offers the ``__attrs_post_init__`` hook that is automatically detected and run after ``attrs`` is done initializing your instance: -.. doctest:: post_init1 +.. doctest:: >>> @attr.s ... class C(object): @@ -327,7 +322,7 @@ And for that ``attrs`` offers the ``__attrs_post_init__`` hook that is automatic Please note that you can't directly set attributes on frozen classes: -.. doctest:: post_init2 +.. doctest:: >>> @attr.s(frozen=True) ... class FrozenBroken(object): @@ -342,7 +337,7 @@ Please note that you can't directly set attributes on frozen classes: If you need to set attributes on a frozen class, you'll have to resort to the :ref:`same trick ` as ``attrs`` and use :meth:`object.__setattr__`: -.. doctest:: post_init3 +.. doctest:: >>> @attr.s(frozen=True) ... class Frozen(object): diff --git a/docs/why.rst b/docs/why.rst index ed4758c9e..f1bcee3c9 100644 --- a/docs/why.rst +++ b/docs/why.rst @@ -3,10 +3,6 @@ Why not… ======== -.. testsetup:: * - - import attr - If you'd like third party's account why ``attrs`` is great, have a look at Glyph's `The One Python Library Everyone Needs `_! @@ -254,7 +250,7 @@ And who will guarantee you, that you don't accidentally flip the ``<`` in your t It also should be noted that ``attrs`` is not an all-or-nothing solution. You can freely choose which features you want and disable those that you want more control over: -.. doctest:: opt-in +.. doctest:: >>> @attr.s(repr=False) ... class SmartClass(object): diff --git a/setup.cfg b/setup.cfg index 2adc3b6a4..9419a1572 100644 --- a/setup.cfg +++ b/setup.cfg @@ -25,7 +25,4 @@ multi_line_output=3 not_skip=__init__.py known_first_party=attr -known_third_party=UserDict,attr,hypothesis,pympler,pytest,setuptools,six,sphinx,zope - -[flake8] -exclude = tests/mypy.tests.py +known_third_party=UserDict,attr,hypothesis,pympler,pytest,setuptools,six,zope diff --git a/setup.py b/setup.py index dcf628f3c..2c8c23188 100644 --- a/setup.py +++ b/setup.py @@ -34,13 +34,9 @@ "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development :: Libraries :: Python Modules", ] -# mypy plugins are not yet externally distributable, so we must lock tests -# against a particular version of mypy. additionally, output formatting -# of mypy can vary by version. -MYPY_VERSION = "mypy==0.610" INSTALL_REQUIRES = [] EXTRAS_REQUIRE = { - "docs": ["sphinx", "zope.interface", MYPY_VERSION], + "docs": ["sphinx", "zope.interface"], "tests": [ "coverage", "hypothesis", @@ -49,7 +45,6 @@ "six", "zope.interface", ], - "test-stubs": [MYPY_VERSION], } EXTRAS_REQUIRE["dev"] = ( EXTRAS_REQUIRE["tests"] + EXTRAS_REQUIRE["docs"] + ["pre-commit"] @@ -116,7 +111,6 @@ def find_meta(meta): long_description=LONG, packages=PACKAGES, package_dir={"": "src"}, - package_data={"attr": ["*.pyi", "py.typed"]}, zip_safe=False, classifiers=CLASSIFIERS, install_requires=INSTALL_REQUIRES, diff --git a/stubs/mypy.tests.py b/stubs/mypy.tests.py deleted file mode 100644 index c894ba8b0..000000000 --- a/stubs/mypy.tests.py +++ /dev/null @@ -1,427 +0,0 @@ -# :--------------------------- -# :Basics -# :--------------------------- - -# :Note: the attrs plugin for mypy only affects calls to attr.ib if they are -# :within a class definition, so we include a class in each test. - - -# [case test_no_type] -# :------------------ -import attr - -@attr.s -class C: - a = attr.ib() - b = attr.ib(init=False, metadata={'foo': 1}) - -c = C(1) -reveal_type(c.a) # E: Revealed type is 'Any' -reveal_type(C.a) # E: Revealed type is 'Any' -reveal_type(c.b) # E: Revealed type is 'Any' -reveal_type(C.b) # E: Revealed type is 'Any' - - -# [case test_type_arg] -# :------------------- -# cmd: mypy --strict-optional -import attr -from typing import List, Any - -@attr.s -class C: - a = attr.ib(type=int) - -c = C(1) -reveal_type(c.a) # E: Revealed type is 'builtins.int' -reveal_type(C.a) # E: Revealed type is 'builtins.int' - -C("1") # E: Argument 1 to "C" has incompatible type "str"; expected "int" -C(a="1") # E: Argument "a" to "C" has incompatible type "str"; expected "int" -C(None) # E: Argument 1 to "C" has incompatible type "None"; expected "int" -C(a=None) # E: Argument "a" to "C" has incompatible type "None"; expected "int" -C(a=1) - -@attr.s -class D: - a = attr.ib(type=List[int]) - reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' - -@attr.s -class E: - a = attr.ib(type='List[int]') - reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' - -@attr.s -class F: - a = attr.ib(type=Any) - reveal_type(a) # E: Revealed type is 'Any' - - -# [case test_type_annotations] -# :--------------------------- -# cmd: mypy --strict-optional -import attr -from typing import List, Any - -@attr.s -class C: - a: int = attr.ib() - -c = C(1) -reveal_type(c.a) # E: Revealed type is 'builtins.int' -reveal_type(C.a) # E: Revealed type is 'builtins.int' - -C("1") # E: Argument 1 to "C" has incompatible type "str"; expected "int" -C(a="1") # E: Argument "a" to "C" has incompatible type "str"; expected "int" -C(None) # E: Argument 1 to "C" has incompatible type "None"; expected "int" -C(a=None) # E: Argument "a" to "C" has incompatible type "None"; expected "int" -C(a=1) - -@attr.s -class D: - a: List[int] = attr.ib() - reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' - -@attr.s -class E: - a: 'List[int]' = attr.ib() - reveal_type(a) # E: Revealed type is 'builtins.list[builtins.int]' - -@attr.s -class F: - a: Any = attr.ib() - reveal_type(a) # E: Revealed type is 'Any' - - -# [case test_inheritance] -# :---------------------- -import attr - -@attr.s -class A: - x: int = attr.ib() - -@attr.s -class B(A): - y: str = attr.ib() - -B(x=1, y='foo') -B(x=1, y=2) # E: Argument "y" to "B" has incompatible type "int"; expected "str" - - -# [case test_multiple_inheritance] -# :---------------------- -import attr - -@attr.s -class A: - x: int = attr.ib() - -@attr.s -class B: - y: str = attr.ib() - -@attr.s -class C(B, A): - z: float = attr.ib() - -C(x=1, y='foo', z=1.1) -C(x=1, y=2, z=1.1) # E: Argument "y" to "C" has incompatible type "int"; expected "str" - - -# [case test_dunders] -# :------------------ -import attr - -@attr.s -class A: - x: int = attr.ib() - -@attr.s -class B(A): - y: str = attr.ib() - -class C: - pass - -# same class -B(x=1, y='foo') == B(x=1, y='foo') -# child class -B(x=1, y='foo') == A(x=1) -# parent class -A(x=1) == B(x=1, y='foo') -# not attrs class -A(x=1) == C() - -# same class -B(x=1, y='foo') > B(x=1, y='foo') -# child class -B(x=1, y='foo') > A(x=1) -# parent class -A(x=1) > B(x=1, y='foo') -# not attrs class -A(x=1) > C() # E: Unsupported operand types for > ("A" and "C") - - -# :--------------------------- -# :Defaults -# :--------------------------- - -# [case test_defaults_no_type] -# :---------------------------- -import attr - -@attr.s -class C: - a = attr.ib(default=0) - reveal_type(a) # E: Revealed type is 'builtins.int*' - - b = attr.ib(0) - reveal_type(b) # E: Revealed type is 'builtins.int*' - - -# [case test_defaults_type_arg] -# :---------------------------- -import attr - -@attr.s -class C: - a = attr.ib(type=int) - reveal_type(a) # E: Revealed type is 'builtins.int' - - b = attr.ib(default=0, type=int) - reveal_type(b) # E: Revealed type is 'builtins.int' - - c = attr.ib(default='bad', type=int) # E: Incompatible types in assignment (expression has type "object", variable has type "int") - - -# [case test_defaults_type_annotations] -# :------------------------------------ -import attr - -@attr.s -class C: - a: int = attr.ib() - reveal_type(a) # E: Revealed type is 'builtins.int' - - b: int = attr.ib(default=0) - reveal_type(b) # E: Revealed type is 'builtins.int' - - c: int = attr.ib(default='bad') # E: Incompatible types in assignment (expression has type "str", variable has type "int") - - d: int = attr.ib(default=0, type=str) # E: Incompatible types in assignment (expression has type "object", variable has type "int") - - -# :--------------------------- -# :Factory Defaults -# :--------------------------- - -# [case test_factory_defaults_type_arg] -# :------------------------------------ -import attr -from typing import List - -def int_factory() -> int: - return 0 - -@attr.s -class C: - a = attr.ib(default=attr.Factory(list)) - reveal_type(a) # E: Revealed type is 'builtins.list*[_T`1]' - - b = attr.ib(default=attr.Factory(list), type=List[int]) - reveal_type(b) # E: Revealed type is 'builtins.list[builtins.int]' - - c = attr.ib(default=attr.Factory(list), type=int) # E: Incompatible types in assignment (expression has type "object", variable has type "int") - - d = attr.ib(default=attr.Factory(int_factory), type=int) - reveal_type(d) # E: Revealed type is 'builtins.int' - - -# [case test_factory_defaults_type_annotations] -# :-------------------------------------------- -import attr -from typing import List - - -def int_factory() -> int: - return 0 - -@attr.s -class C: - a = attr.ib(default=attr.Factory(list)) - reveal_type(a) # E: Revealed type is 'builtins.list*[_T`1]' - - b: List[int] = attr.ib(default=attr.Factory(list)) - reveal_type(b) # E: Revealed type is 'builtins.list[builtins.int]' - - c: int = attr.ib(default=attr.Factory(list)) # E: Incompatible types in assignment (expression has type "List[_T]", variable has type "int") - - d: int = attr.ib(default=attr.Factory(int_factory)) - reveal_type(d) # E: Revealed type is 'builtins.int' - - -# :--------------------------- -# :Validators -# :--------------------------- - -# [case test_validators] -# :--------------------- -import attr -from attr.validators import in_, and_, instance_of -import enum - -class State(enum.Enum): - ON = "on" - OFF = "off" - -@attr.s -class C: - a = attr.ib(type=int, validator=in_([1, 2, 3])) - aa = attr.ib(validator=in_([1, 2, 3])) - - # multiple: - b = attr.ib(type=int, validator=[in_([1, 2, 3]), instance_of(int)]) - bb = attr.ib(type=int, validator=(in_([1, 2, 3]), instance_of(int))) - bbb = attr.ib(type=int, validator=and_(in_([1, 2, 3]), instance_of(int))) - - e = attr.ib(type=int, validator=1) # E: No overload variant matches argument types "Type[int]", "int" - - # mypy does not know how to get the contained type from an enum: - f = attr.ib(type=State, validator=in_(State)) - ff = attr.ib(validator=in_(State)) # E: Need type annotation for 'ff' - - -# [case test_init_with_validators] -import attr -from attr.validators import instance_of - -@attr.s -class C: - x = attr.ib(validator=instance_of(int)) - -reveal_type(C.x) # E: Revealed type is 'builtins.int*' - -C(42) -C(x=42) -# NOTE: even though the type of C.x is known to be int, the following is not an error. -# The mypy plugin that generates __init__ runs at semantic analysis time, but type inference (which handles TypeVars happens later) -C("42") - - -# [case test_custom_validators_type_arg] -# :------------------------------------- -import attr - -def validate_int(inst, at, val: int): - pass - -def validate_str(inst, at, val: str): - pass - -@attr.s -class C: - a = attr.ib(type=int, validator=validate_int) - reveal_type(a) # E: Revealed type is 'builtins.int' - - b = attr.ib(type=int, validator=[validate_int]) - reveal_type(b) # E: Revealed type is 'builtins.int' - - c = attr.ib(type=int, validator=validate_str) # E: Argument "validator" has incompatible type "Callable[[Any, Any, str], Any]"; expected "Union[Callable[[Any, Attribute[Any], int], Any], Sequence[Callable[[Any, Attribute[Any], int], Any]], None]" - - -# [case test_custom_validators_type_annotations] -# :--------------------------------------------- -import attr - -def validate_int(inst, at, val: int): - pass - -def validate_str(inst, at, val: str): - pass - -@attr.s -class C: - a: int = attr.ib(validator=validate_int) - reveal_type(a) # E: Revealed type is 'builtins.int' - - b: int = attr.ib(validator=[validate_int]) - reveal_type(b) # E: Revealed type is 'builtins.int' - - c: int = attr.ib(validator=validate_str) # E: Incompatible types in assignment (expression has type "str", variable has type "int") - - -# :--------------------------- -# :Converters -# :--------------------------- - -# [case test_converters] -# :--------------------- -import attr -from typing import Union - -def str_to_int(s: Union[str, int]) -> int: - return int(s) - -@attr.s -class C: - a = attr.ib(converter=str_to_int) - reveal_type(a) # E: Revealed type is 'builtins.int*' - - b: str = attr.ib(converter=str_to_int) # E: Incompatible types in assignment (expression has type "int", variable has type "str") - - -# [case test_converter_init] -# :------------------------- -import attr - -def str_to_int(s: str) -> int: - return int(s) - -@attr.s -class C: - x: int = attr.ib(converter=str_to_int) - -C('1') -C(1) # E: Argument 1 to "C" has incompatible type "int"; expected "str" - -# :--------------------------- -# :Make -# :--------------------------- - -# [case test_make_from_dict] -# :------------------------- -import attr -C = attr.make_class("C", { - "x": attr.ib(type=int), - "y": attr.ib() -}) - - -# [case test_make_from_str] -# :------------------------ -import attr -C = attr.make_class("C", ["x", "y"]) - - -# [case test_astuple] -# :------------------ -import attr -@attr.s -class C: - a: int = attr.ib() - -t1 = attr.astuple(C) -reveal_type(t1) # E: Revealed type is 'builtins.tuple[Any]' - - -# [case test_asdict] -# :----------------- -import attr -@attr.s -class C: - a: int = attr.ib() - -t1 = attr.asdict(C) -reveal_type(t1) # E: Revealed type is 'builtins.dict[builtins.str, Any]' diff --git a/stubs/mypy_pytest_plugin.py b/stubs/mypy_pytest_plugin.py deleted file mode 100644 index ad8988734..000000000 --- a/stubs/mypy_pytest_plugin.py +++ /dev/null @@ -1,968 +0,0 @@ -"""Utilities for processing .test files containing test case descriptions.""" - -import os -import os.path -import posixpath -import re -import shutil -import sys -import tempfile - -from abc import abstractmethod -from os import remove, rmdir -from typing import ( - Any, Dict, Iterator, List, NamedTuple, Optional, Set, Tuple, Union -) - -import pytest # type: ignore # no pytest in typeshed - - -# AssertStringArraysEqual displays special line alignment helper messages if -# the first different line has at least this many characters, -MIN_LINE_LENGTH_FOR_ALIGNMENT = 5 - -test_temp_dir = 'tmp' -test_data_prefix = os.path.dirname(__file__) - -root_dir = os.path.normpath(os.path.join( - os.path.dirname(__file__), '..', '..')) - -# File modify/create operation: copy module contents from source_path. -UpdateFile = NamedTuple('UpdateFile', [('module', str), - ('source_path', str), - ('target_path', str)]) - -# File delete operation: delete module file. -DeleteFile = NamedTuple('DeleteFile', [('module', str), - ('path', str)]) - -FileOperation = Union[UpdateFile, DeleteFile] - - -class AssertionFailure(Exception): - """Exception used to signal failed test cases.""" - def __init__(self, s: Optional[str] = None) -> None: - if s: - super().__init__(s) - else: - super().__init__() - - -class BaseTestCase: - """Common base class for _MyUnitTestCase and DataDrivenTestCase. - - Handles temporary folder creation and deletion. - """ - def __init__(self, name: str) -> None: - self.name = name - self.old_cwd = None # type: Optional[str] - self.tmpdir = None # type: Optional[tempfile.TemporaryDirectory[str]] - - def setup(self) -> None: - self.old_cwd = os.getcwd() - self.tmpdir = tempfile.TemporaryDirectory(prefix='mypy-test-') - os.chdir(self.tmpdir.name) - os.mkdir('tmp') - - def teardown(self) -> None: - assert self.old_cwd is not None and self.tmpdir is not None, \ - "test was not properly set up" - os.chdir(self.old_cwd) - try: - self.tmpdir.cleanup() - except OSError: - pass - self.old_cwd = None - self.tmpdir = None - - -def assert_string_arrays_equal(expected: List[str], actual: List[str], - msg: str) -> None: - """Assert that two string arrays are equal. - - Display any differences in a human-readable form. - """ - - actual = clean_up(actual) - - if actual != expected: - num_skip_start = num_skipped_prefix_lines(expected, actual) - num_skip_end = num_skipped_suffix_lines(expected, actual) - - sys.stderr.write('Expected:\n') - - # If omit some lines at the beginning, indicate it by displaying a line - # with '...'. - if num_skip_start > 0: - sys.stderr.write(' ...\n') - - # Keep track of the first different line. - first_diff = -1 - - # Display only this many first characters of identical lines. - width = 75 - - for i in range(num_skip_start, len(expected) - num_skip_end): - if i >= len(actual) or expected[i] != actual[i]: - if first_diff < 0: - first_diff = i - sys.stderr.write(' {:<45} (diff)'.format(expected[i])) - else: - e = expected[i] - sys.stderr.write(' ' + e[:width]) - if len(e) > width: - sys.stderr.write('...') - sys.stderr.write('\n') - if num_skip_end > 0: - sys.stderr.write(' ...\n') - - sys.stderr.write('Actual:\n') - - if num_skip_start > 0: - sys.stderr.write(' ...\n') - - for j in range(num_skip_start, len(actual) - num_skip_end): - if j >= len(expected) or expected[j] != actual[j]: - sys.stderr.write(' {:<45} (diff)'.format(actual[j])) - else: - a = actual[j] - sys.stderr.write(' ' + a[:width]) - if len(a) > width: - sys.stderr.write('...') - sys.stderr.write('\n') - if actual == []: - sys.stderr.write(' (empty)\n') - if num_skip_end > 0: - sys.stderr.write(' ...\n') - - sys.stderr.write('\n') - - if first_diff >= 0 and first_diff < len(actual) and ( - len(expected[first_diff]) >= MIN_LINE_LENGTH_FOR_ALIGNMENT - or len(actual[first_diff]) >= MIN_LINE_LENGTH_FOR_ALIGNMENT): - # Display message that helps visualize the differences between two - # long lines. - show_align_message(expected[first_diff], actual[first_diff]) - - raise AssertionFailure(msg) - - -def show_align_message(s1: str, s2: str) -> None: - """Align s1 and s2 so that the their first difference is highlighted. - - For example, if s1 is 'foobar' and s2 is 'fobar', display the - following lines: - - E: foobar - A: fobar - ^ - - If s1 and s2 are long, only display a fragment of the strings around the - first difference. If s1 is very short, do nothing. - """ - - # Seeing what went wrong is trivial even without alignment if the expected - # string is very short. In this case do nothing to simplify output. - if len(s1) < 4: - return - - maxw = 72 # Maximum number of characters shown - - sys.stderr.write('Alignment of first line difference:\n') - - trunc = False - while s1[:30] == s2[:30]: - s1 = s1[10:] - s2 = s2[10:] - trunc = True - - if trunc: - s1 = '...' + s1 - s2 = '...' + s2 - - max_len = max(len(s1), len(s2)) - extra = '' - if max_len > maxw: - extra = '...' - - # Write a chunk of both lines, aligned. - sys.stderr.write(' E: {}{}\n'.format(s1[:maxw], extra)) - sys.stderr.write(' A: {}{}\n'.format(s2[:maxw], extra)) - # Write an indicator character under the different columns. - sys.stderr.write(' ') - for j in range(min(maxw, max(len(s1), len(s2)))): - if s1[j:j + 1] != s2[j:j + 1]: - sys.stderr.write('^') # Difference - break - else: - sys.stderr.write(' ') # Equal - sys.stderr.write('\n') - - -def assert_string_arrays_equal_wildcards(expected: List[str], - actual: List[str], - msg: str) -> None: - # Like above, but let a line with only '...' in expected match any number - # of lines in actual. - actual = clean_up(actual) - - while actual != [] and actual[-1] == '': - actual = actual[:-1] - - # Expand "..." wildcards away. - expected = match_array(expected, actual) - assert_string_arrays_equal(expected, actual, msg) - - -def clean_up(a: List[str]) -> List[str]: - """Remove common directory prefix from all strings in a. - - This uses a naive string replace; it seems to work well enough. Also - remove trailing carriage returns. - """ - res = [] - for s in a: - prefix = os.sep - ss = s - for p in prefix, prefix.replace(os.sep, '/'): - if p != '/' and p != '//' and p != '\\' and p != '\\\\': - ss = ss.replace(p, '') - # Ignore spaces at end of line. - ss = re.sub(' +$', '', ss) - res.append(re.sub('\\r$', '', ss)) - return res - - -def match_array(pattern: List[str], target: List[str]) -> List[str]: - """Expand '...' wildcards in pattern by matching against target.""" - - res = [] # type: List[str] - i = 0 - j = 0 - - while i < len(pattern): - if pattern[i] == '...': - # Wildcard in pattern. - if i + 1 == len(pattern): - # Wildcard at end of pattern; match the rest of target. - res.extend(target[j:]) - # Finished. - break - else: - # Must find the instance of the next pattern line in target. - jj = j - while jj < len(target): - if target[jj] == pattern[i + 1]: - break - jj += 1 - if jj == len(target): - # No match. Get out. - res.extend(pattern[i:]) - break - res.extend(target[j:jj]) - i += 1 - j = jj - elif (j < len(target) and (pattern[i] == target[j] - or (i + 1 < len(pattern) - and j + 1 < len(target) - and pattern[i + 1] == target[j + 1]))): - # In sync; advance one line. The above condition keeps sync also if - # only a single line is different, but loses it if two consecutive - # lines fail to match. - res.append(pattern[i]) - i += 1 - j += 1 - else: - # Out of sync. Get out. - res.extend(pattern[i:]) - break - return res - - -def num_skipped_prefix_lines(a1: List[str], a2: List[str]) -> int: - num_eq = 0 - while num_eq < min(len(a1), len(a2)) and a1[num_eq] == a2[num_eq]: - num_eq += 1 - return max(0, num_eq - 4) - - -def num_skipped_suffix_lines(a1: List[str], a2: List[str]) -> int: - num_eq = 0 - while (num_eq < min(len(a1), len(a2)) - and a1[-num_eq - 1] == a2[-num_eq - 1]): - num_eq += 1 - return max(0, num_eq - 4) - - -def normalize_error_messages(messages: List[str]) -> List[str]: - """Translate an array of error messages to use / as path separator.""" - - a = [] - for m in messages: - a.append(m.replace(os.sep, '/')) - return a - - -def parse_test_cases( - path: str, - base_path: str = '.', - optional_out: bool = False, - native_sep: bool = False) -> List['DataDrivenTestCase']: - """Parse a file with test case descriptions. - - Return an array of test cases. - - NB: this function and DataDrivenTestCase were shared between the - myunit and pytest codepaths -- if something looks redundant, - that's likely the reason. - """ - if native_sep: - join = os.path.join - else: - join = posixpath.join # type: ignore - include_path = os.path.dirname(path) - with open(path, encoding='utf-8') as f: - lst = f.readlines() - for i in range(len(lst)): - lst[i] = lst[i].rstrip('\n') - p = parse_test_data(lst, path) - out = [] # type: List[DataDrivenTestCase] - - # Process the parsed items. Each item has a header of form [id args], - # optionally followed by lines of text. - i = 0 - while i < len(p): - ok = False - i0 = i - if p[i].id == 'case': - i += 1 - - # path and contents - files = [] # type: List[Tuple[str, str]] - # path and contents for output files - output_files = [] # type: List[Tuple[str, str]] - # Regular output errors - tcout = [] # type: List[str] - # Output errors for incremental, runs 2+ - tcout2 = {} # type: Dict[int, List[str]] - # from run number of paths - deleted_paths = {} # type: Dict[int, Set[str]] - # from run number to module names - stale_modules = {} # type: Dict[int, Set[str]] - # from run number module names - rechecked_modules = {} # type: Dict[ int, Set[str]] - # Active triggers (one line per incremental step) - triggered = [] # type: List[str] - while i < len(p) and p[i].id != 'case': - if p[i].id == 'file' or p[i].id == 'outfile': - # Record an extra file needed for the test case. - arg = p[i].arg - assert arg is not None - contents = '\n'.join(p[i].data) - contents = expand_variables(contents) - file_entry = (join(base_path, arg), contents) - if p[i].id == 'file': - files.append(file_entry) - elif p[i].id == 'outfile': - output_files.append(file_entry) - elif p[i].id in ('builtins', 'builtins_py2'): - # Use an alternative stub file for the builtins module. - arg = p[i].arg - assert arg is not None - mpath = join(os.path.dirname(path), arg) - if p[i].id == 'builtins': - fnam = 'builtins.pyi' - else: - # Python 2 - fnam = '__builtin__.pyi' - with open(mpath) as f: - files.append((join(base_path, fnam), f.read())) - elif p[i].id == 'typing': - # Use an alternative stub file for the typing module. - arg = p[i].arg - assert arg is not None - src_path = join(os.path.dirname(path), arg) - with open(src_path) as f: - files.append((join(base_path, 'typing.pyi'), f.read())) - elif re.match(r'stale[0-9]*$', p[i].id): - if p[i].id == 'stale': - passnum = 1 - else: - passnum = int(p[i].id[len('stale'):]) - assert passnum > 0 - arg = p[i].arg - if arg is None: - stale_modules[passnum] = set() - else: - stale_modules[passnum] = {item.strip() - for item in arg.split(',')} - elif re.match(r'rechecked[0-9]*$', p[i].id): - if p[i].id == 'rechecked': - passnum = 1 - else: - passnum = int(p[i].id[len('rechecked'):]) - arg = p[i].arg - if arg is None: - rechecked_modules[passnum] = set() - else: - rechecked_modules[passnum] = {item.strip() - for item - in arg.split(',')} - elif p[i].id == 'delete': - # File to delete during a multi-step test case - arg = p[i].arg - assert arg is not None - m = re.match(r'(.*)\.([0-9]+)$', arg) - assert m, 'Invalid delete section: {}'.format(arg) - num = int(m.group(2)) - assert num >= 2, "Can't delete during step {}".format(num) - full = join(base_path, m.group(1)) - deleted_paths.setdefault(num, set()).add(full) - elif p[i].id == 'out' or p[i].id == 'out1': - tcout = p[i].data - tcout = [expand_variables(line) for line in tcout] - if os.path.sep == '\\': - tcout = [fix_win_path(line) for line in tcout] - ok = True - elif re.match(r'out[0-9]*$', p[i].id): - passnum = int(p[i].id[3:]) - assert passnum > 1 - output = p[i].data - output = [expand_variables(line) for line in output] - if native_sep and os.path.sep == '\\': - output = [fix_win_path(line) for line in output] - tcout2[passnum] = output - ok = True - elif p[i].id == 'triggered' and p[i].arg is None: - triggered = p[i].data - else: - raise ValueError( - 'Invalid section header {} in {} at line {}'.format( - p[i].id, path, p[i].line)) - i += 1 - - for passnum in stale_modules.keys(): - if passnum not in rechecked_modules: - # If the set of rechecked modules isn't specified, make it - # the same as the set - # of modules with a stale public interface. - rechecked_modules[passnum] = stale_modules[passnum] - if (passnum in stale_modules - and passnum in rechecked_modules - and not stale_modules[passnum].issubset( - rechecked_modules[passnum])): - raise ValueError( - ('Stale modules after pass {} must be a subset of ' - 'rechecked modules ({}:{})').format(passnum, path, - p[i0].line)) - - if optional_out: - ok = True - - if ok: - input = expand_includes(p[i0].data, include_path) - expand_errors(input, tcout, 'main') - for file_path, contents in files: - expand_errors(contents.split('\n'), tcout, file_path) - lastline = p[i].line if i < len(p) else p[i - 1].line + 9999 - arg0 = p[i0].arg - assert arg0 is not None - tc = DataDrivenTestCase(arg0, input, tcout, tcout2, path, - p[i0].line, lastline, - files, output_files, stale_modules, - rechecked_modules, deleted_paths, - native_sep, triggered) - out.append(tc) - if not ok: - raise ValueError( - '{}, line {}: Error in test case description'.format( - path, p[i0].line)) - - return out - - -class DataDrivenTestCase(BaseTestCase): - """Holds parsed data and handles directory setup and teardown for - MypyDataCase.""" - - # TODO: rename to ParsedTestCase or merge with MypyDataCase (yet avoid - # multiple inheritance) - # TODO: only create files on setup, not during parsing - - input = None # type: List[str] - # Output for the first pass - output = None # type: List[str] - # Output for runs 2+, indexed by run number - output2 = None # type: Dict[int, List[str]] - - file = '' - line = 0 - - # (file path, file content) tuples - files = None # type: List[Tuple[str, str]] - expected_stale_modules = None # type: Dict[int, Set[str]] - expected_rechecked_modules = None # type: Dict[int, Set[str]] - - # Files/directories to clean up after test case; (is directory, path) - # tuples - clean_up = None # type: List[Tuple[bool, str]] - - def __init__(self, - name: str, - input: List[str], - output: List[str], - output2: Dict[int, List[str]], - file: str, - line: int, - lastline: int, - files: List[Tuple[str, str]], - output_files: List[Tuple[str, str]], - expected_stale_modules: Dict[int, Set[str]], - expected_rechecked_modules: Dict[int, Set[str]], - deleted_paths: Dict[int, Set[str]], - native_sep: bool = False, - triggered: Optional[List[str]] = None, - ) -> None: - super().__init__(name) - self.input = input - self.output = output - self.output2 = output2 - self.lastline = lastline - self.file = file - self.line = line - self.files = files - self.output_files = output_files - self.expected_stale_modules = expected_stale_modules - self.expected_rechecked_modules = expected_rechecked_modules - self.deleted_paths = deleted_paths - self.native_sep = native_sep - self.triggered = triggered or [] - - def setup(self) -> None: - super().setup() - encountered_files = set() - self.clean_up = [] - for paths in self.deleted_paths.values(): - for path in paths: - self.clean_up.append((False, path)) - encountered_files.add(path) - for path, content in self.files: - dir = os.path.dirname(path) - for d in self.add_dirs(dir): - self.clean_up.append((True, d)) - with open(path, 'w') as f: - f.write(content) - if path not in encountered_files: - self.clean_up.append((False, path)) - encountered_files.add(path) - if re.search(r'\.[2-9]$', path): - # Make sure new files introduced in the second and later runs - # are accounted for - renamed_path = path[:-2] - if renamed_path not in encountered_files: - encountered_files.add(renamed_path) - self.clean_up.append((False, renamed_path)) - for path, _ in self.output_files: - # Create directories for expected output and mark them to be - # cleaned up at the end of the test case. - dir = os.path.dirname(path) - for d in self.add_dirs(dir): - self.clean_up.append((True, d)) - self.clean_up.append((False, path)) - - def add_dirs(self, dir: str) -> List[str]: - """Add all subdirectories required to create dir. - - Return an array of the created directories in the order of creation. - """ - if dir == '' or os.path.isdir(dir): - return [] - else: - dirs = self.add_dirs(os.path.dirname(dir)) + [dir] - os.mkdir(dir) - return dirs - - def teardown(self) -> None: - # First remove files. - for is_dir, path in reversed(self.clean_up): - if not is_dir: - try: - remove(path) - except FileNotFoundError: - # Breaking early using Ctrl+C may happen before file - # creation. Also, some files may be deleted by a test case. - pass - # Then remove directories. - for is_dir, path in reversed(self.clean_up): - if is_dir: - pycache = os.path.join(path, '__pycache__') - if os.path.isdir(pycache): - shutil.rmtree(pycache) - try: - rmdir(path) - except OSError as error: - print(' ** Error removing directory %s -- contents:' % - path) - for item in os.listdir(path): - print(' ', item) - # Most likely, there are some files in the - # directory. Use rmtree to nuke the directory, but - # fail the test case anyway, since this seems like - # a bug in a test case -- we shouldn't leave - # garbage lying around. By nuking the directory, - # the next test run hopefully passes. - path = error.filename - # Be defensive -- only call rmtree if we're sure we aren't - # removing anything valuable. - if (path.startswith(test_temp_dir + '/') and - os.path.isdir(path)): - shutil.rmtree(path) - raise - super().teardown() - - def find_steps(self) -> List[List[FileOperation]]: - """Return a list of descriptions of file operations for each - incremental step. - - The first list item corresponds to the first incremental step, the - second for the second step, etc. Each operation can either be a file - modification/creation (UpdateFile) or deletion (DeleteFile). - """ - steps = {} # type: Dict[int, List[FileOperation]] - for path, _ in self.files: - m = re.match(r'.*\.([0-9]+)$', path) - if m: - num = int(m.group(1)) - assert num >= 2 - target_path = re.sub(r'\.[0-9]+$', '', path) - module = module_from_path(target_path) - operation = UpdateFile(module, path, target_path) - steps.setdefault(num, []).append(operation) - for num, paths in self.deleted_paths.items(): - assert num >= 2 - for path in paths: - module = module_from_path(path) - steps.setdefault(num, []).append(DeleteFile(module, path)) - max_step = max(steps) - return [steps[num] for num in range(2, max_step + 1)] - - -def module_from_path(path: str) -> str: - path = re.sub(r'\.py$', '', path) - # We can have a mix of Unix-style and Windows-style separators. - parts = re.split(r'[/\\]', path) - assert parts[0] == test_temp_dir - del parts[0] - module = '.'.join(parts) - module = re.sub(r'\.__init__$', '', module) - return module - - -class TestItem: - """Parsed test caseitem. - - An item is of the form - [id arg] - .. data .. - """ - - id = '' - arg = '' # type: Optional[str] - - # Text data, array of 8-bit strings - data = None # type: List[str] - - file = '' - line = 0 # Line number in file - - def __init__(self, id: str, arg: Optional[str], data: List[str], file: str, - line: int) -> None: - self.id = id - self.arg = arg - self.data = data - self.file = file - self.line = line - - -def parse_test_data(l: List[str], fnam: str) -> List[TestItem]: - """Parse a list of lines that represent a sequence of test items.""" - - ret = [] # type: List[TestItem] - data = [] # type: List[str] - - id = None # type: Optional[str] - arg = None # type: Optional[str] - - i = 0 - i0 = 0 - while i < len(l): - s = l[i].strip() - - if (l[i].startswith('# [') and s.endswith(']') - and not s.startswith('# [[')): - if id: - data = collapse_line_continuation(data) - data = strip_list(data) - ret.append(TestItem(id, arg, strip_list(data), fnam, i0 + 1)) - i0 = i - id = s[3:-1] - arg = None - if ' ' in id: - arg = id[id.index(' ') + 1:] - id = id[:id.index(' ')] - data = [] - # elif l[i].startswith('# [['): - # data.append(l[i][3:]) - elif not l[i].startswith('# :'): - data.append(l[i]) - # elif l[i].startswith('----'): - # data.append(l[i][2:]) - i += 1 - - # Process the last item. - if id: - data = collapse_line_continuation(data) - data = strip_list(data) - ret.append(TestItem(id, arg, data, fnam, i0 + 1)) - - return ret - - -def strip_list(l: List[str]) -> List[str]: - """Return a stripped copy of l. - - Strip whitespace at the end of all lines, and strip all empty - lines from the end of the array. - """ - - r = [] # type: List[str] - for s in l: - # Strip spaces at end of line - r.append(re.sub(r'\s+$', '', s)) - - while len(r) > 0 and r[-1] == '': - r.pop() - - return r - - -def collapse_line_continuation(l: List[str]) -> List[str]: - r = [] # type: List[str] - cont = False - for s in l: - ss = re.sub(r'\\$', '', s) - if cont: - r[-1] += re.sub('^ +', '', ss) - else: - r.append(ss) - cont = s.endswith('\\') - return r - - -def expand_includes(a: List[str], base_path: str) -> List[str]: - """Expand @includes within a list of lines. - - Replace all lies starting with @include with the contents of the - file name following the prefix. Look for the files in base_path. - """ - - res = [] # type: List[str] - for s in a: - if s.startswith('@include '): - fn = s.split(' ', 1)[1].strip() - with open(os.path.join(base_path, fn)) as f: - res.extend(f.readlines()) - else: - res.append(s) - return res - - -def expand_variables(s: str) -> str: - return s.replace('', root_dir) - - -def expand_errors(input: List[str], output: List[str], fnam: str) -> None: - """Transform comments such as '# E: message' or - '# E:3: message' in input. - - The result is lines like 'fnam:line: error: message'. - """ - - for i in range(len(input)): - # The first in the split things isn't a comment - for possible_err_comment in input[i].split(' # ')[1:]: - m = re.search( - r'^([ENW]):((?P\d+):)? (?P.*)$', - possible_err_comment.strip()) - if m: - if m.group(1) == 'E': - severity = 'error' - elif m.group(1) == 'N': - severity = 'note' - elif m.group(1) == 'W': - severity = 'warning' - col = m.group('col') - if col is None: - output.append( - '{}:{}: {}: {}'.format(fnam, i + 1, severity, - m.group('message'))) - else: - output.append('{}:{}:{}: {}: {}'.format( - fnam, i + 1, col, severity, m.group('message'))) - - -def fix_win_path(line: str) -> str: - r"""Changes Windows paths to Linux paths in error messages. - - E.g. foo\bar.py -> foo/bar.py. - """ - line = line.replace(root_dir, root_dir.replace('\\', '/')) - m = re.match(r'^([\S/]+):(\d+:)?(\s+.*)', line) - if not m: - return line - else: - filename, lineno, message = m.groups() - return '{}:{}{}'.format(filename.replace('\\', '/'), - lineno or '', message) - - -def fix_cobertura_filename(line: str) -> str: - r"""Changes filename paths to Linux paths in Cobertura output files. - - E.g. filename="pkg\subpkg\a.py" -> filename="pkg/subpkg/a.py". - """ - m = re.search(r' None: - group = parser.getgroup('mypy') - group.addoption('--update-data', action='store_true', default=False, - help='Update test data to reflect actual output' - ' (supported only for certain tests)') - - -# This function name is special to pytest. See -# http://doc.pytest.org/en/latest/writing_plugins.html#collection-hooks -def pytest_pycollect_makeitem(collector: Any, name: str, - obj: object) -> 'Optional[Any]': - """Called by pytest on each object in modules configured in conftest.py - files. - - collector is pytest.Collector, returns Optional[pytest.Class] - """ - if isinstance(obj, type): - # Only classes derived from DataSuite contain test cases, not the - # DataSuite class itself - if issubclass(obj, DataSuite) and obj is not DataSuite: - # Non-None result means this obj is a test case. - # The collect method of the returned MypyDataSuite instance - # will be called later, - # with self.obj being obj. - return MypyDataSuite(name, parent=collector) - return None - - -class MypyDataSuite(pytest.Class): # type: ignore # inheriting from Any - def collect(self) -> Iterator[pytest.Item]: # type: ignore - """Called by pytest on each of the object returned from - pytest_pycollect_makeitem""" - - # obj is the object for which pytest_pycollect_makeitem returned self. - suite = self.obj # type: DataSuite - for f in suite.files: - for case in parse_test_cases(os.path.join(test_data_prefix, f), - base_path=suite.base_path, - optional_out=suite.optional_out, - native_sep=suite.native_sep): - if suite.filter(case): - yield MypyDataCase(case.name, self, case) - - -def is_incremental(testcase: DataDrivenTestCase) -> bool: - return ('incremental' in testcase.name.lower() or - 'incremental' in testcase.file) - - -def has_stable_flags(testcase: DataDrivenTestCase) -> bool: - if any(re.match(r'# flags[2-9]:', line) for line in testcase.input): - return False - for filename, contents in testcase.files: - if os.path.basename(filename).startswith('mypy.ini.'): - return False - return True - - -class MypyDataCase(pytest.Item): # type: ignore # inheriting from Any - def __init__(self, name: str, parent: MypyDataSuite, - case: DataDrivenTestCase) -> None: - self.skip = False - if name.endswith('-skip'): - self.skip = True - name = name[:-len('-skip')] - - super().__init__(name, parent) - self.case = case - - def runtest(self) -> None: - if self.skip: - pytest.skip() - update_data = self.config.getoption('--update-data', False) - self.parent.obj(update_data=update_data).run_case(self.case) - - def setup(self) -> None: - self.case.setup() - - def teardown(self) -> None: - self.case.teardown() - - def reportinfo(self) -> Tuple[str, int, str]: - return self.case.file, self.case.line, self.case.name - - def repr_failure(self, excinfo: Any) -> str: - if excinfo.errisinstance(SystemExit): - # We assume that before doing exit() (which raises SystemExit) - # we've printed enough context about what happened so that a stack - # trace is not useful. In particular, uncaught exceptions during - # semantic analysis or type checking call exit() and they already - # print out a stack trace. - excrepr = excinfo.exconly() - else: - self.parent._prunetraceback(excinfo) - excrepr = excinfo.getrepr(style='short') - - return "data: {}:{}:\n{}".format(self.case.file, self.case.line, - excrepr) - - -class DataSuite: - # option fields - class variables - files = None # type: List[str] - base_path = '.' # type: str - optional_out = False # type: bool - native_sep = False # type: bool - - def __init__(self, *, update_data: bool) -> None: - self.update_data = update_data - - @abstractmethod - def run_case(self, testcase: DataDrivenTestCase) -> None: - raise NotImplementedError - - @classmethod - def filter(cls, testcase: DataDrivenTestCase) -> bool: - return True diff --git a/stubs/setup.py b/stubs/setup.py deleted file mode 100644 index c083c802b..000000000 --- a/stubs/setup.py +++ /dev/null @@ -1,8 +0,0 @@ -from setuptools import setup -if __name__ == "__main__": - setup( - name='doctest2', - py_modules=['doctest2'], - version='0.1', - zip_safe=False, - ) diff --git a/stubs/test_stubs.py b/stubs/test_stubs.py deleted file mode 100644 index 13ef72802..000000000 --- a/stubs/test_stubs.py +++ /dev/null @@ -1,79 +0,0 @@ -# this file is adapted from mypy.test.testcmdline - -import os -import re -import subprocess -import sys - -from mypy_pytest_plugin import ( - DataSuite, assert_string_arrays_equal, normalize_error_messages -) - - -pytest_plugins = ['mypy_pytest_plugin'] - -# Path to Python 3 interpreter -python3_path = sys.executable -test_temp_dir = 'tmp' -this_dir = os.path.dirname(os.path.realpath(__file__)) -test_file = os.path.join(this_dir, 'mypy.tests.py') -prefix_dir = os.path.join(os.path.dirname(this_dir), 'src') - - -class PythonEvaluationSuite(DataSuite): - files = [test_file] - base_path = test_temp_dir - optional_out = True - native_sep = True - - def run_case(self, testcase): - _test_python_evaluation(testcase) - - -def _test_python_evaluation(testcase): - assert testcase.old_cwd is not None, "test was not properly set up" - # Write the program to a file. - # we omit .py extension to be compatible with called to - # expand_errors parse_test_cases. - program = 'main' - 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', program] - process = subprocess.Popen(fixed + args, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - env={'MYPYPATH': prefix_dir}, - cwd=test_temp_dir) - outb, errb = process.communicate() - # 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): - """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 - return m.group(1).split() diff --git a/tests/typing_example.py b/tests/typing_example.py new file mode 100644 index 000000000..05491c7e2 --- /dev/null +++ b/tests/typing_example.py @@ -0,0 +1,90 @@ +from typing import * + +import attr + + +# Type argument + + +@attr.s +class C: + a = attr.ib(type=int) + + +c = C(1) +C(a=1) + + +@attr.s +class D: + x = attr.ib(type=List[int]) + + +@attr.s +class E: + y = attr.ib(type="List[int]") + + +@attr.s +class F: + z = attr.ib(type=Any) + + +# Annotations + + +@attr.s +class CC: + a: int = attr.ib() + + +cc = CC(1) +CC(a=1) + + +@attr.s +class DD: + x: List[int] = attr.ib() + + +@attr.s +class EE: + y: "List[int]" = attr.ib() + + +@attr.s +class FF: + z: Any = attr.ib() + + +# Inheritence + + +@attr.s +class GG(DD): + y: str = attr.ib() + + +GG(x=[1], y="foo") + + +@attr.s +class HH(DD, EE): + z: float = attr.ib() + + +HH(x=[1], y=[], z=1.1) + + +@attr.s +class A: + x: int = attr.ib() + + +@attr.s +class B(A): + y: str = attr.ib() + + +# same class +c == cc diff --git a/tox.ini b/tox.ini index a46d3d33e..27960b7ec 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = pre-commit,lint,py27,py34,py35,py36,py37,pypy,pypy3,manifest,docs,readme,changelog,coverage-report,stubs +envlist = pre-commit,lint,py27,py34,py35,py36,py37,pypy,pypy3,manifest,docs,readme,changelog,coverage-report,typing [testenv] @@ -63,7 +63,7 @@ setenv = extras = docs commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html - sphinx-build -b doctest -d {envtmpdir}/doctrees docs docs/_build/html + sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html python -m doctest README.rst @@ -87,7 +87,7 @@ deps = towncrier skip_install = true commands = towncrier --draft -[testenv:stubs] +[testenv:typing] basepython = python3.6 -extras = tests,test-stubs -commands = pytest stubs/test_stubs.py +deps = mypy +commands = mypy tests/typing_example.py From 2b19c364a1929a38457a9060e3d21767c3fb24c5 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Tue, 10 Jul 2018 22:00:41 -0700 Subject: [PATCH 62/64] get pre-commit passing --- .pre-commit-config.yaml | 3 + MANIFEST.in | 2 +- src/attr/__init__.pyi | 233 +++++++++++++++++++++++----------------- src/attr/converters.pyi | 6 +- src/attr/exceptions.pyi | 1 + src/attr/validators.pyi | 10 +- tests/typing_example.py | 2 +- 7 files changed, 149 insertions(+), 108 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d75c0c6e6..4c206776a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,9 @@ repos: hooks: - id: black language_version: python3.6 + # override until resolved: https://github.com/ambv/black/issues/402 + files: \.pyi?$ + types: [] - repo: https://github.com/asottile/seed-isort-config rev: v1.0.1 diff --git a/MANIFEST.in b/MANIFEST.in index 699033de3..47b34c1b5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,7 +4,7 @@ include LICENSE *.rst *.toml .readthedocs.yml .pre-commit-config.yaml exclude .github/*.md .travis.yml codecov.yml # Stubs -include src/py.typed +include src/attr/py.typed recursive-include src *.pyi # Tests diff --git a/src/attr/__init__.pyi b/src/attr/__init__.pyi index 7dd004f0f..c8b4c6585 100644 --- a/src/attr/__init__.pyi +++ b/src/attr/__init__.pyi @@ -1,12 +1,27 @@ -from typing import Any, Callable, Dict, Generic, List, Optional, Sequence, Mapping, Tuple, Type, TypeVar, Union, overload +from typing import ( + Any, + Callable, + Dict, + Generic, + List, + Optional, + Sequence, + Mapping, + Tuple, + Type, + TypeVar, + Union, + overload, +) + # `import X as X` is required to make these public from . import exceptions as exceptions from . import filters as filters from . import converters as converters from . import validators as validators -_T = TypeVar('_T') -_C = TypeVar('_C', bound=type) +_T = TypeVar("_T") +_C = TypeVar("_C", bound=type) _ValidatorType = Callable[[Any, Attribute, _T], Any] _ConverterType = Callable[[Any], _T] @@ -25,7 +40,10 @@ NOTHING: object @overload def Factory(factory: Callable[[], _T]) -> _T: ... @overload -def Factory(factory: Union[Callable[[Any], _T], Callable[[], _T]], takes_self: bool = ...) -> _T: ... +def Factory( + factory: Union[Callable[[Any], _T], Callable[[], _T]], + takes_self: bool = ..., +) -> _T: ... class Attribute(Generic[_T]): name: str @@ -43,7 +61,6 @@ class Attribute(Generic[_T]): def __gt__(self, x: Attribute) -> bool: ... def __ge__(self, x: Attribute) -> bool: ... - # NOTE: We had several choices for the annotation to use for type arg: # 1) Type[_T] # - Pros: works in PyCharm without plugin support @@ -70,87 +87,95 @@ class Attribute(Generic[_T]): # # This form catches explicit None or no default but with no other arguments returns Any. @overload -def attrib(default: None = ..., - validator: None = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - convert: None = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: None = ..., - converter: None = ..., - factory: None = ..., - ) -> Any: ... +def attrib( + default: None = ..., + validator: None = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + convert: None = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: None = ..., + converter: None = ..., + factory: None = ..., +) -> Any: ... + # This form catches an explicit None or no default and infers the type from the other arguments. @overload -def attrib(default: None = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: Optional[Type[_T]] = ..., - converter: Optional[_ConverterType[_T]] = ..., - factory: Optional[Callable[[], _T]] = ..., - ) -> _T: ... +def attrib( + default: None = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + convert: Optional[_ConverterType[_T]] = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType[_T]] = ..., + factory: Optional[Callable[[], _T]] = ..., +) -> _T: ... + # This form catches an explicit default argument. @overload -def attrib(default: _T, - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: Optional[Type[_T]] = ..., - converter: Optional[_ConverterType[_T]] = ..., - factory: Optional[Callable[[], _T]] = ..., - ) -> _T: ... +def attrib( + default: _T, + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + convert: Optional[_ConverterType[_T]] = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: Optional[Type[_T]] = ..., + converter: Optional[_ConverterType[_T]] = ..., + factory: Optional[Callable[[], _T]] = ..., +) -> _T: ... + # This form covers type=non-Type: e.g. forward references (str), Any @overload -def attrib(default: Optional[_T] = ..., - validator: Optional[_ValidatorArgType[_T]] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - convert: Optional[_ConverterType[_T]] = ..., - metadata: Optional[Mapping[Any, Any]] = ..., - type: object = ..., - converter: Optional[_ConverterType[_T]] = ..., - factory: Optional[Callable[[], _T]] = ..., - ) -> Any: ... - - +def attrib( + default: Optional[_T] = ..., + validator: Optional[_ValidatorArgType[_T]] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + convert: Optional[_ConverterType[_T]] = ..., + metadata: Optional[Mapping[Any, Any]] = ..., + type: object = ..., + converter: Optional[_ConverterType[_T]] = ..., + factory: Optional[Callable[[], _T]] = ..., +) -> Any: ... @overload -def attrs(maybe_cls: _C, - these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - str: bool = ..., - auto_attribs: bool = ...) -> _C: ... +def attrs( + maybe_cls: _C, + these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., +) -> _C: ... @overload -def attrs(maybe_cls: None = ..., - these: Optional[Dict[str, Any]] = ..., - repr_ns: Optional[str] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - str: bool = ..., - auto_attribs: bool = ...) -> Callable[[_C], _C]: ... - +def attrs( + maybe_cls: None = ..., + these: Optional[Dict[str, Any]] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., +) -> Callable[[_C], _C]: ... # TODO: add support for returning NamedTuple from the mypy plugin class _Fields(Tuple[Attribute, ...]): @@ -162,18 +187,20 @@ def validate(inst: Any) -> None: ... # TODO: add support for returning a proper attrs class from the mypy plugin # we use Any instead of _CountingAttr so that e.g. `make_class('Foo', [attr.ib()])` is valid -def make_class(name: str, - attrs: Union[List[str], Tuple[str, ...], Dict[str, Any]], - bases: Tuple[type, ...] = ..., - repr_ns: Optional[str] = ..., - repr: bool = ..., - cmp: bool = ..., - hash: Optional[bool] = ..., - init: bool = ..., - slots: bool = ..., - frozen: bool = ..., - str: bool = ..., - auto_attribs: bool = ...) -> type: ... +def make_class( + name: str, + attrs: Union[List[str], Tuple[str, ...], Dict[str, Any]], + bases: Tuple[type, ...] = ..., + repr_ns: Optional[str] = ..., + repr: bool = ..., + cmp: bool = ..., + hash: Optional[bool] = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., +) -> type: ... # _funcs -- @@ -181,17 +208,22 @@ def make_class(name: str, # FIXME: asdict/astuple do not honor their factory args. waiting on one of these: # https://github.com/python/mypy/issues/4236 # https://github.com/python/typing/issues/253 -def asdict(inst: Any, - recurse: bool = ..., - filter: Optional[_FilterType] = ..., - dict_factory: Type[Mapping[Any, Any]] = ..., - retain_collection_types: bool = ...) -> Dict[str, Any]: ... +def asdict( + inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType] = ..., + dict_factory: Type[Mapping[Any, Any]] = ..., + retain_collection_types: bool = ..., +) -> Dict[str, Any]: ... + # TODO: add support for returning NamedTuple from the mypy plugin -def astuple(inst: Any, - recurse: bool = ..., - filter: Optional[_FilterType] = ..., - tuple_factory: Type[Sequence] = ..., - retain_collection_types: bool = ...) -> Tuple[Any, ...]: ... +def astuple( + inst: Any, + recurse: bool = ..., + filter: Optional[_FilterType] = ..., + tuple_factory: Type[Sequence] = ..., + retain_collection_types: bool = ..., +) -> Tuple[Any, ...]: ... def has(cls: type) -> bool: ... def assoc(inst: _T, **changes: Any) -> _T: ... def evolve(inst: _T, **changes: Any) -> _T: ... @@ -201,7 +233,6 @@ def evolve(inst: _T, **changes: Any) -> _T: ... def set_run_validators(run: bool) -> None: ... def get_run_validators() -> bool: ... - # aliases -- s = attributes = attrs diff --git a/src/attr/converters.pyi b/src/attr/converters.pyi index 9e31677e7..bdf7a0c2e 100644 --- a/src/attr/converters.pyi +++ b/src/attr/converters.pyi @@ -1,6 +1,8 @@ from typing import TypeVar, Optional from . import _ConverterType -_T = TypeVar('_T') +_T = TypeVar("_T") -def optional(converter: _ConverterType[_T]) -> _ConverterType[Optional[_T]]: ... +def optional( + converter: _ConverterType[_T] +) -> _ConverterType[Optional[_T]]: ... diff --git a/src/attr/exceptions.pyi b/src/attr/exceptions.pyi index 4a2904fc2..48fffcc1e 100644 --- a/src/attr/exceptions.pyi +++ b/src/attr/exceptions.pyi @@ -1,5 +1,6 @@ class FrozenInstanceError(AttributeError): msg: str = ... + class AttrsAttributeNotFoundError(ValueError): ... class NotAnAttrsClassError(ValueError): ... class DefaultAlreadySetError(RuntimeError): ... diff --git a/src/attr/validators.pyi b/src/attr/validators.pyi index 3fc34377c..abbaedf10 100644 --- a/src/attr/validators.pyi +++ b/src/attr/validators.pyi @@ -1,10 +1,14 @@ from typing import Container, List, Union, TypeVar, Type, Any, Optional, Tuple from . import _ValidatorType -_T = TypeVar('_T') +_T = TypeVar("_T") -def instance_of(type: Union[Tuple[Type[_T], ...], Type[_T]]) -> _ValidatorType[_T]: ... +def instance_of( + type: Union[Tuple[Type[_T], ...], Type[_T]] +) -> _ValidatorType[_T]: ... def provides(interface: Any) -> _ValidatorType[Any]: ... -def optional(validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]]) -> _ValidatorType[Optional[_T]]: ... +def optional( + validator: Union[_ValidatorType[_T], List[_ValidatorType[_T]]] +) -> _ValidatorType[Optional[_T]]: ... def in_(options: Container[_T]) -> _ValidatorType[_T]: ... def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... diff --git a/tests/typing_example.py b/tests/typing_example.py index 05491c7e2..3c1b7deb7 100644 --- a/tests/typing_example.py +++ b/tests/typing_example.py @@ -1,4 +1,4 @@ -from typing import * +from typing import Any, List import attr From 88fcd12b5ee36482cdd683b9c5615b5bf34f3538 Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Wed, 11 Jul 2018 10:03:44 -0700 Subject: [PATCH 63/64] Address review feedback --- .gitignore | 1 - setup.py | 1 + tests/typing_example.py | 16 +++------------- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 806688363..5d5f8c6aa 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ .cache .coverage* .hypothesis -.idea .mypy_cache .pytest_cache .tox diff --git a/setup.py b/setup.py index 2c8c23188..b784cf55f 100644 --- a/setup.py +++ b/setup.py @@ -115,4 +115,5 @@ def find_meta(meta): classifiers=CLASSIFIERS, install_requires=INSTALL_REQUIRES, extras_require=EXTRAS_REQUIRE, + include_package_data=True, ) diff --git a/tests/typing_example.py b/tests/typing_example.py index 3c1b7deb7..300ac8728 100644 --- a/tests/typing_example.py +++ b/tests/typing_example.py @@ -3,7 +3,7 @@ import attr -# Type argument +# Typing via "type" Argument --- @attr.s @@ -30,7 +30,7 @@ class F: z = attr.ib(type=Any) -# Annotations +# Typing via Annotations --- @attr.s @@ -57,7 +57,7 @@ class FF: z: Any = attr.ib() -# Inheritence +# Inheritance -- @attr.s @@ -76,15 +76,5 @@ class HH(DD, EE): HH(x=[1], y=[], z=1.1) -@attr.s -class A: - x: int = attr.ib() - - -@attr.s -class B(A): - y: str = attr.ib() - - # same class c == cc From 85862cb6b2cbd08e5f5da2ebcf2cd766889b3d7b Mon Sep 17 00:00:00 2001 From: Chad Dombrova Date: Wed, 11 Jul 2018 10:08:08 -0700 Subject: [PATCH 64/64] Move typing test up in tox envlist --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 27960b7ec..addf75194 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = pre-commit,lint,py27,py34,py35,py36,py37,pypy,pypy3,manifest,docs,readme,changelog,coverage-report,typing +envlist = pre-commit,typing,lint,py27,py34,py35,py36,py37,pypy,pypy3,manifest,docs,readme,changelog,coverage-report [testenv]