Skip to content

Commit

Permalink
Add annotations and py.typed to conform with PEP561. Add type checkin…
Browse files Browse the repository at this point in the history
…g to tox (#40)

* Add type stubs and py.typed. Add type checking to tox
* use black on pyi
* Fix type packagings

Signed-off-by: Wilfred Wong <keiclone@users.noreply.github.com>
Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net>
Co-authored-by: Bernat Gabor <bgabor8@bloomberg.net>
  • Loading branch information
keiclone and gaborbernat committed May 31, 2020
1 parent ae8e8d0 commit 79bfc60
Show file tree
Hide file tree
Showing 13 changed files with 218 additions and 53 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ build-backend = 'setuptools.build_meta'
line-length = 80

[tool.setuptools_scm]
write_to = "attrs_strict/_version.py"
write_to = "src/attrs_strict/_version.py"
write_to_template = """
\"\"\" Version information \"\"\"
Expand Down
13 changes: 11 additions & 2 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,21 @@ project_urls =
[options]
packages = attrs_strict
install_requires =
attrs
typing;python_version<'3.5'
attrs>=19.1.0
typing>=3.7.4.1;python_version<'3.5'
python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4
package_dir =
=src
tests_require =
mock;python_version<'3.3'
pytest >= 4

[options.package_data]
* = *.pyi
attrs_strict = py.typed

[options.packages.find]
where = src

[bdist_wheel]
universal = true
File renamed without changes.
8 changes: 8 additions & 0 deletions src/attrs_strict/__init__.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from ._error import (
AttributeTypeError as AttributeTypeError,
BadTypeError as BadTypeError,
TupleError as TupleError,
TypeValidationError as TypeValidationError,
UnionError as UnionError,
)
from ._type_validation import type_validator as type_validator
File renamed without changes.
4 changes: 4 additions & 0 deletions src/attrs_strict/_commons.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import typing

def is_newtype(type_: typing.Type[typing.Any]) -> bool: ...
def format_type(type_: typing.Type[typing.Any]) -> str: ...
42 changes: 29 additions & 13 deletions attrs_strict/_error.py → src/attrs_strict/_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,26 @@ def _render(self, error):


class AttributeTypeError(BadTypeError):
def __init__(self, container, attribute):
def __init__(self, value, attribute):
super(AttributeTypeError, self).__init__()
self.container = container
self.value = value
self.attribute = attribute

def __str__(self):
error = "{} must be {} (got {} that is a {})".format(
self.attribute.name,
format_type(self.attribute.type),
self.container,
type(self.container),
)
if self.attribute.type is None:
error = (
"attrs-strict error: AttributeTypeError was "
"raised on an attribute ({}) with no defined type".format(
self.attribute.name
)
)
else:
error = "{} must be {} (got {} that is a {})".format(
self.attribute.name,
format_type(self.attribute.type),
self.value,
type(self.value),
)

return self._render(error)

Expand Down Expand Up @@ -77,11 +85,19 @@ def __init__(self, container, attribute):
self.attribute = attribute

def __str__(self):
error = "{} can not be empty and must be {} (got {})".format(
self.attribute.name,
format_type(self.attribute.type),
self.container,
)
if self.attribute.type is None:
error = (
"attrs-strict error: AttributeTypeError was "
"raised on an attribute ({}) with no defined type".format(
self.attribute.name
)
)
else:
error = "{} can not be empty and must be {} (got {})".format(
self.attribute.name,
format_type(self.attribute.type),
self.container,
)

return self._render(error)

Expand Down
54 changes: 54 additions & 0 deletions src/attrs_strict/_error.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import typing
import attr
import inspect

class TypeValidationError(Exception):
def __repr__(self) -> str: ...

class BadTypeError(TypeValidationError, ValueError):
def __init__(self) -> None:
self.containers: typing.List[typing.Iterable[typing.Any]]
def add_container(self, container: typing.Any) -> None: ...
def _render(self, error: str) -> str: ...

class AttributeTypeError(BadTypeError):
def __init__(
self, value: typing.Any, attribute: attr.Attribute[typing.Any]
) -> None: ...
def __str__(self) -> str: ...

class CallableError(BadTypeError):
def __init__(
self,
attribute: attr.Attribute[typing.Any],
callable_signature: inspect.Signature,
expected_signature: typing.Type[typing.Callable[..., typing.Any]],
mismatch_callable_arg: inspect.Parameter,
expected_callable_arg: inspect.Parameter,
) -> None: ...
def __str__(self) -> str: ...

class EmptyError(BadTypeError):
def __init__(
self, container: typing.Any, attribute: attr.Attribute[typing.Any]
) -> None: ...
def __str__(self) -> str: ...

class TupleError(BadTypeError):
def __init__(
self,
container: typing.Any,
attribute: typing.Optional[typing.Type[typing.Any]],
tuple_types: typing.Tuple[typing.Type[typing.Any]],
) -> None: ...
def __str__(self) -> str: ...
def _more_or_less(self) -> str: ...

class UnionError(BadTypeError):
def __init__(
self,
container: typing.Any,
attribute: str,
expected_type: typing.Type[typing.Any],
) -> None: ...
def __str__(self) -> str: ...
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
try:
from inspect import signature
except ImportError:
from funcsigs import signature
# silencing type error so mypy doesn't complain about duplicate import
from funcsigs import signature # type: ignore

try:
from itertools import zip_longest
except ImportError:
from itertools import izip_longest as zip_longest
# silencing type error so mypy doesn't complain about duplicate import
from itertools import izip_longest as zip_longest # type: ignore


class SimilarTypes:
Expand Down Expand Up @@ -64,29 +66,34 @@ def _validator(instance, attribute, field):


def _validate_elements(attribute, value, expected_type):
if expected_type is None:
return

base_type = _get_base_type(expected_type)

if base_type is None or base_type == typing.Any:
if base_type == typing.Any:
return

if base_type != typing.Union and not isinstance(value, base_type):
if base_type != typing.Union and not isinstance( # type: ignore
value, base_type
):
raise AttributeTypeError(value, attribute)

if base_type in SimilarTypes.List:
if base_type == typing.Union: # type: ignore
_handle_union(attribute, value, expected_type)
elif base_type in SimilarTypes.List:
_handle_set_or_list(attribute, value, expected_type)
elif base_type in SimilarTypes.Dict:
_handle_dict(attribute, value, expected_type)
elif base_type in SimilarTypes.Tuple:
_handle_tuple(attribute, value, expected_type)
elif base_type == typing.Union:
_handle_union(attribute, value, expected_type)
elif base_type in SimilarTypes.Callable:
elif base_type in SimilarTypes.Callable: # type: ignore
_handle_callable(attribute, value, expected_type)


def _get_base_type(type_):
if hasattr(type_, "__origin__") and type_.__origin__ is not None:
base_type = type_.__origin__
base_type = type_.__origin__ # type: typing.Type[typing.Any]
elif is_newtype(type_):
base_type = type_.__supertype__
else:
Expand All @@ -104,7 +111,7 @@ def _type_matching(actual, expected):

base_type = _get_base_type(expected)

if base_type == typing.Union:
if base_type == typing.Union: # type: ignore
return any(
_type_matching(actual, expected_candidate)
for expected_candidate in expected.__args__
Expand Down Expand Up @@ -132,11 +139,11 @@ def _handle_callable(attribute, callable_, expected_type):
param.annotation for param in _signature.parameters.values()
]
callable_args.append(_signature.return_annotation)
if not expected_type.__args__:
if not expected_type.__args__: # type: ignore
return # No annotations specified on type, matches all Callables

for callable_arg, expected_arg in zip_longest(
callable_args, expected_type.__args__
callable_args, expected_type.__args__ # type: ignore
):
if not _type_matching(callable_arg, expected_arg):
raise CallableError(
Expand All @@ -145,7 +152,7 @@ def _handle_callable(attribute, callable_, expected_type):


def _handle_set_or_list(attribute, container, expected_type):
(element_type,) = expected_type.__args__
(element_type,) = expected_type.__args__ # type: ignore

for element in container:
try:
Expand All @@ -156,7 +163,7 @@ def _handle_set_or_list(attribute, container, expected_type):


def _handle_dict(attribute, container, expected_type):
key_type, value_type = expected_type.__args__
key_type, value_type = expected_type.__args__ # type: ignore

for key in container:
try:
Expand All @@ -168,7 +175,7 @@ def _handle_dict(attribute, container, expected_type):


def _handle_tuple(attribute, container, expected_type):
tuple_types = expected_type.__args__
tuple_types = expected_type.__args__ # type: ignore
if len(tuple_types) == 2 and tuple_types[1] == Ellipsis:
element_type = tuple_types[0]
tuple_types = (element_type,) * len(container)
Expand Down
60 changes: 60 additions & 0 deletions src/attrs_strict/_type_validation.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import typing
import attr

def type_validator(
empty_ok: bool = True,
) -> typing.Callable[
[typing.Any, attr.Attribute[typing.Any], typing.Any], None
]:
def _validator(
instance: typing.Any,
attribute: attr.Attribute[typing.Any],
field: typing.Any,
) -> None: ...
return _validator

def _validate_elements(
attribute: attr.Attribute[typing.Any],
value: typing.Any,
expected_type: typing.Optional[typing.Type[typing.Any]],
) -> None: ...
def _get_base_type(
type_: typing.Type[typing.Any],
) -> typing.Type[typing.Any]: ...
def _type_matching(
actual: typing.Type[typing.Any], expected: typing.Type[typing.Any]
) -> bool: ...
def _handle_callable(
attribute: attr.Attribute[typing.Any],
callable_: typing.Callable[..., typing.Any],
expected_type: typing.Type[typing.Callable[..., typing.Any]],
) -> None: ...
def _handle_set_or_list(
attribute: attr.Attribute[typing.Any],
container: typing.Union[typing.Set[typing.Any], typing.List[typing.Any]],
expected_type: typing.Union[
typing.Type[typing.Set[typing.Any]],
typing.Type[typing.List[typing.Any]],
],
) -> None: ...
def _handle_dict(
attribute: attr.Attribute[typing.Any],
container: typing.Union[
typing.Mapping[typing.Any, typing.Any],
typing.MutableMapping[typing.Any, typing.Any],
],
expected_type: typing.Union[
typing.Type[typing.Mapping[typing.Any, typing.Any]],
typing.Type[typing.MutableMapping[typing.Any, typing.Any]],
],
) -> None: ...
def _handle_tuple(
attribute: attr.Attribute[typing.Any],
container: typing.Tuple[typing.Any],
expected_type: typing.Type[typing.Tuple[typing.Any]],
) -> None: ...
def _handle_union(
attribute: attr.Attribute[typing.Any],
value: typing.Any,
expected_type: typing.Type[typing.Any],
) -> None: ...
1 change: 1 addition & 0 deletions src/attrs_strict/_version.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ : str= ...
Empty file added src/attrs_strict/py.typed
Empty file.

0 comments on commit 79bfc60

Please sign in to comment.