Skip to content
This repository has been archived by the owner on May 4, 2021. It is now read-only.

Commit

Permalink
Merge pull request #32 from herczy/f/mobprogramming
Browse files Browse the repository at this point in the history
Partially implement some typing types
  • Loading branch information
herczy authored Mar 2, 2018
2 parents 9e9a458 + 801e504 commit fcc05a8
Show file tree
Hide file tree
Showing 6 changed files with 242 additions and 13 deletions.
118 changes: 118 additions & 0 deletions features/pep484-supported-types.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
Feature: typing types are supported in annotations

As a developer
In order to write PEP-484 compliant code
I want to use the typing module

Scenario: calling a function expecting a typing.Callable argument passes with callable argument
Given that "mylib.py" contains the following code:
"""
import typing
def apply_func(func: typing.Callable, *args):
return func(*args)
"""
And that "myapp.py" contains the following type checked code:
"""
import mylib
mylib.apply_func(lambda a, b: (b, a), 1, 2)
"""
When "python3 myapp.py" is run
Then it must pass

Scenario: calling a function expecting a typing.Callable argument fails with non-callable argument
Given that "mylib.py" contains the following code:
"""
import typing
def apply_func(func: typing.Callable, *args):
return func(*args)
"""
And that "myapp.py" contains the following type checked code:
"""
import mylib
mylib.apply_func(None, 1, 2, 3)
"""
When "python3 myapp.py" is run
Then it must fail
"""
TypesafetyError: Argument 'func' of function 'apply_func' is invalid (expected: Callable; got: NoneType)
"""

Scenario: calling a function expecting a typing.Union argument passes
Given that "mylib.py" contains the following type checked code:
"""
import typing
def func(arg: typing.Union[int, str]):
return arg
"""
And that "myapp.py" contains the following code:
"""
import mylib
mylib.func(42)
mylib.func("42")
"""
When "python3 myapp.py" is run
Then it must pass

Scenario: calling a function expecting a typing.Union argument fails with wrong argument
Given that "mylib.py" contains the following code:
"""
import typing
def func(arg: typing.Union[int, str]):
return arg
"""
And that "myapp.py" contains the following type checked code:
"""
import mylib
mylib.func(None)
"""
When "python3 myapp.py" is run
Then it must fail
"""
TypesafetyError: Argument 'arg' of function 'func' is invalid (expected: typing.Union[int, str]; got: NoneType)
"""

Scenario: calling a function expecting a typing.Optional argument passes
Given that "mylib.py" contains the following code:
"""
import typing
def func(arg: typing.Optional[bool]):
return arg
"""
And that "myapp.py" contains the following type checked code:
"""
import mylib
mylib.func(False)
mylib.func(None)
"""
When "python3 myapp.py" is run
Then it must pass

Scenario: calling a function expecting a typing.Optional argument fails with wrong argument
Given that "mylib.py" contains the following code:
"""
import typing
def func(arg: typing.Optional[float]):
return arg
"""
And that "myapp.py" contains the following type checked code:
"""
import mylib
mylib.func('spam')
"""
When "python3 myapp.py" is run
Then it must fail
"""
TypesafetyError: Argument 'arg' of function 'func' is invalid (expected: typing.Union[float, NoneType]; got: str)
"""
8 changes: 8 additions & 0 deletions features/steps/steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import os.path
import shlex
import subprocess
import re

from nose.tools import *
from behave import given, when, then
Expand All @@ -32,9 +33,16 @@ def write_file(context, file_name):
with open(os.path.join(context.test_dir, file_name), "w") as f:
print(context.text, file=f)

@given('that "{file_name}" contains the following type checked code')
def write_file(context, file_name):
with open(os.path.join(context.test_dir, file_name), "w") as f:
print('import typesafety; typesafety.activate()', file=f)
print(context.text, file=f)


@when('"{command}" is run')
def execute_command(context, command: str):
command = re.sub(r'^python3', sys.executable, command)
args = shlex.split(command)
process = subprocess.Popen(
args,
Expand Down
12 changes: 4 additions & 8 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,6 @@
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#


import sys
if sys.hexversion < 0x03020000:
raise RuntimeError("Required python version: 3.2 or newer (current: %s)" % sys.version)

try:
from setuptools import setup

Expand All @@ -38,7 +33,7 @@
are valid.
""",
license="LGPLv2+",
version="2.0.0",
version="2.1.0",
author="Viktor Hercinger",
author_email="[email protected]",
maintainer="Viktor Hercinger",
Expand All @@ -54,9 +49,9 @@
'Operating System :: OS Independent',
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.2',
'Programming Language :: Python :: 3.3',
'Programming Language :: Python :: 3.4',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Topic :: Software Development',
'Topic :: Software Development :: Testing',
'Topic :: Documentation :: Sphinx',
Expand All @@ -73,5 +68,6 @@
requires=[
'nose',
'sphinx',
'typing_inspect',
]
)
2 changes: 2 additions & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ deps=
behave

coverage
typing_inspect
py34: typing

commands=
pycodestyle --max-line-length 120 --repeat typesafety
Expand Down
83 changes: 82 additions & 1 deletion typesafety/tests/test_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#

import contextlib
import typing
import unittest
import warnings

from typesafety.validator import Validator, TypesafetyError

Expand Down Expand Up @@ -204,3 +206,82 @@ def func(arg: (str, int, None)):
validator(4.2)

self.assertIn("expected: (str, int, None)", str(error.exception))

def test_validate_typing_callable(self):
def func(args: typing.Callable):
return args

def args_example():
pass

validator = Validator(func)

self.assertEqual(args_example, validator(args_example))
self.assertRaises(TypesafetyError, validator, None)

def test_validate_typing_union(self):
def func(arg: typing.Union[int, str]):
return arg

validator = Validator(func)

self.assertEqual(1, validator(1))
self.assertEqual("spam", validator("spam"))
with self.assertRaises(TypesafetyError) as context:
validator([])
self.assertIn('typing.Union[int, str]', str(context.exception))

def test_validate_typing_optional(self):
def func(arg: typing.Optional[int]):
return arg

validator = Validator(func)

self.assertEqual(1, validator(1))
self.assertEqual(None, validator(None))
with self.assertRaises(TypesafetyError) as context:
validator([])
self.assertIn('typing.Union[int, NoneType]', str(context.exception))

@contextlib.contextmanager
def __capture_warnings(self):
old_filters = list(warnings.filters)
try:
warnings.simplefilter("always")
with warnings.catch_warnings(record=True) as log:
yield log

finally:
warnings.filters = old_filters

def test_tuple_notation_is_deprecated(self):
def deprecated(arg: (int, str)):
return arg

with self.__capture_warnings() as log:
Validator(deprecated)

self.assertEqual(1, len(log), msg="No warnings found after executing the action")
self.assertEqual(log[-1].category, DeprecationWarning)

def test_callable_as_validator_deprecated(self):
def deprecated(arg: callable):
return arg

with self.__capture_warnings() as log:
Validator(deprecated)

self.assertEqual(1, len(log), msg="No warnings found after executing the action")
self.assertEqual(log[-1].category, DeprecationWarning)

def test_callable_validators_are_not_yet_deprecated(self):
def validator(argument) -> bool:
return True

def deprecated(arg: validator):
return arg

with self.__capture_warnings() as log:
Validator(deprecated)

self.assertEqual(0, len(log), msg="Some warnings found after executing the action")
32 changes: 28 additions & 4 deletions typesafety/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@

import functools
import inspect
import warnings

from typing_inspect import is_union_type, get_args


class TypesafetyError(Exception):
Expand Down Expand Up @@ -89,7 +92,7 @@ def decorate(cls, function):
validator = cls(function)

if not validator.need_validate_arguments and \
not validator.need_validate_return_value:
not validator.need_validate_return_value:
return function

@functools.wraps(function)
Expand Down Expand Up @@ -179,7 +182,7 @@ def __format_expectation(self, annotation):
if annotation is None:
return "None"

return annotation.__name__
return getattr(annotation, '__name__', str(annotation))

def validate_return_value(self, retval):
'''
Expand Down Expand Up @@ -219,7 +222,7 @@ def __call__(self, *args, **kwargs):
def __process_type_annotations(self):
for name, value in self.__spec.annotations.items():
if name == 'return' or \
not self.__is_valid_typecheck_annotation(value):
not self.__is_valid_typecheck_annotation(value):
continue

self.__argument_annotation[name] = value
Expand Down Expand Up @@ -272,6 +275,12 @@ def __is_valid(self, value, validator):
for subvalidator in validator
)

if is_union_type(validator):
return any(
self.__is_valid(value, subvalidator)
for subvalidator in get_args(validator)
)

if isinstance(validator, type):
return isinstance(value, validator)

Expand All @@ -286,15 +295,30 @@ def __is_valid(self, value, validator):

def __is_valid_typecheck_annotation(self, validator):
if isinstance(validator, tuple):
return all(
is_valid = all(
self.__is_valid_typecheck_annotation(subvalidator)
for subvalidator in validator
)
if is_valid:
warnings.warn("Tuple notation is deprecated, use typing.Union or typing.Optional", DeprecationWarning)

return is_valid

if is_union_type(validator):
return all(
self.__is_valid_typecheck_annotation(subvalidator)
for subvalidator in get_args(validator)
)

if isinstance(validator, type):
return True

if callable(validator):
if validator == callable:
warnings.warn(
"Using callable() as a notation is deprecated, use typing.Callable instead",
DeprecationWarning
)
return True

if validator is None:
Expand Down

0 comments on commit fcc05a8

Please sign in to comment.