Skip to content

Commit

Permalink
Add tests for stubs and address a few issues.
Browse files Browse the repository at this point in the history
  • Loading branch information
chadrik committed Nov 3, 2017
1 parent c60d2f1 commit 642ddb5
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 15 deletions.
6 changes: 6 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
1 change: 1 addition & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ zope.interface
pympler
hypothesis
six
git+git://github.com/python/mypy.git#egg=mypy; python_version >= '3.5'
20 changes: 11 additions & 9 deletions src/attr/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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]

Expand All @@ -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: ...
Expand All @@ -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: ...
Expand Down
3 changes: 1 addition & 2 deletions src/attr/exceptions.pyi
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from typing import List

class FrozenInstanceError(AttributeError):
msg : str = ...
args : List[str] = ...
args : tuple = ...

class AttrsAttributeNotFoundError(ValueError): ...
class NotAnAttrsClassError(ValueError): ...
Expand Down
6 changes: 2 additions & 4 deletions src/attr/validators.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
78 changes: 78 additions & 0 deletions tests/test_stubs.py
Original file line number Diff line number Diff line change
@@ -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 <options>
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()
20 changes: 20 additions & 0 deletions tests/test_stubs.test
Original file line number Diff line number Diff line change
@@ -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'

0 comments on commit 642ddb5

Please sign in to comment.