Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add annotations and py.typed to conform with PEP561. Add type checking to tox #40

Merged
merged 3 commits into from
May 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the none check dropped here ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure to be fair; I assume given the type checker does not complain it's not needed? @keiclone knows probably better

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.
Loading