Skip to content

Commit

Permalink
Release 0.1.1 (#4)
Browse files Browse the repository at this point in the history
* Add 'with_module_name' flag

* Add test for 'with_module_name' flag

* Bump version: 0.1.0 → 0.1.1-alpha

* Complete test for evaluation

* Bump version: 0.1.1-alpha → 0.1.1

* Complete test for evaluation

* Complete 'group_by' utility: remove unused default argument
  • Loading branch information
lycantropos committed Nov 3, 2019
1 parent fc77261 commit d465bb5
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 47 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 0.1.0
current_version = 0.1.1
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(-(?P<release>.*))?
serialize =
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.cpython.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
args:
- PYTHON_IMAGE=${CPYTHON_IMAGE_NAME}
- PYTHON_IMAGE_VERSION=${CPYTHON_IMAGE_VERSION}
image: lycantropos/reprit-cpython:0.1.0
image: lycantropos/reprit-cpython:0.1.1
volumes:
- ./reprit/:/opt/reprit/reprit/
- ./tests/:/opt/reprit/tests/
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.pypy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
args:
- PYTHON_IMAGE=${PYPY_IMAGE_NAME}
- PYTHON_IMAGE_VERSION=${PYPY_IMAGE_VERSION}
image: lycantropos/reprit-pypy:0.1.0
image: lycantropos/reprit-pypy:0.1.1
volumes:
- ./reprit/:/opt/reprit/reprit/
- ./tests/:/opt/reprit/tests/
Expand Down
2 changes: 1 addition & 1 deletion reprit/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Auto __repr__ method generation."""

__version__ = '0.1.0'
__version__ = '0.1.1'
60 changes: 42 additions & 18 deletions reprit/base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import inspect
from collections import (OrderedDict,
abc)
from inspect import (_ParameterKind,
signature)
from typing import (Iterable,
Union)

Expand All @@ -15,7 +16,8 @@
def generate_repr(constructor_or_initializer: Union[Constructor, Initializer],
*,
field_seeker: FieldSeeker = seekers.simple,
prefer_keyword: bool = False) -> Map[Domain, str]:
prefer_keyword: bool = False,
with_module_name: bool = False) -> Map[Domain, str]:
"""
Generates `__repr__` method based on constructor/initializer parameters.
Expand All @@ -27,8 +29,11 @@ def generate_repr(constructor_or_initializer: Union[Constructor, Initializer],
which parameters will be used in resulting representation.
:param field_seeker: function that re-creates parameter value
based on class instance and name.
:param prefer_keyword: flag that tells if positional-or-keyword parameters
should be outputted as keyword ones when possible.
:param prefer_keyword: flag that specifies
if positional-or-keyword parameters should be outputted
as keyword ones when possible.
:param with_module_name: flag that specifies
if module name should be added.
>>> from reprit.base import generate_repr
>>> class Person:
Expand All @@ -50,6 +55,18 @@ def generate_repr(constructor_or_initializer: Union[Constructor, Initializer],
ScoreBoard(first=1)
>>> ScoreBoard(1, 40)
ScoreBoard(1, 40)
>>> class Student:
... def __init__(self, name, group):
... self.name = name
... self.group = group
... __repr__ = generate_repr(__init__,
... with_module_name=True)
>>> Student('Kira', 132)
reprit.base.Student('Kira', 132)
>>> Student('Kira', 132)
reprit.base.Student('Kira', 132)
>>> Student('Naomi', 248)
reprit.base.Student('Naomi', 248)
>>> from reprit import seekers
>>> class Account:
... def __init__(self, id_, *, balance=0):
Expand All @@ -62,41 +79,48 @@ def generate_repr(constructor_or_initializer: Union[Constructor, Initializer],
>>> Account(100, balance=-10)
Account(100, balance=-10)
"""
signature = inspect.signature(constructor_or_initializer)
parameters = OrderedDict(signature.parameters)
if with_module_name:
def to_class_name(cls: type) -> str:
return cls.__module__ + '.' + cls.__qualname__
else:
def to_class_name(cls: type) -> str:
return cls.__qualname__

def __repr__(self: Domain) -> str:
return (to_class_name(type(self))
+ '(' + ', '.join(to_arguments_strings(self)) + ')')

parameters = OrderedDict(signature(constructor_or_initializer).parameters)
# remove `cls`/`self`
parameters.popitem(0)
variadic_positional = next((parameter
for parameter in parameters.values()
if parameter.kind is inspect._VAR_POSITIONAL),
None)
variadic_positional = next(
(parameter
for parameter in parameters.values()
if parameter.kind is _ParameterKind.VAR_POSITIONAL),
None)
to_positional_argument_string = repr
to_keyword_argument_string = '{}={!r}'.format

def __repr__(self: Domain) -> str:
return (type(self).__qualname__
+ '(' + ', '.join(to_arguments_strings(self)) + ')')

def to_arguments_strings(object_: Domain) -> Iterable[str]:
variadic_positional_unset = (
variadic_positional is None
or not field_seeker(object_, variadic_positional.name))
for parameter_name, parameter in parameters.items():
field = field_seeker(object_, parameter_name)
if parameter.kind is inspect._POSITIONAL_ONLY:
if parameter.kind is _ParameterKind.POSITIONAL_ONLY:
yield to_positional_argument_string(field)
elif parameter.kind is inspect._POSITIONAL_OR_KEYWORD:
elif parameter.kind is _ParameterKind.POSITIONAL_OR_KEYWORD:
if prefer_keyword and variadic_positional_unset:
yield to_keyword_argument_string(parameter_name, field)
else:
yield to_positional_argument_string(field)
elif parameter.kind is inspect._VAR_POSITIONAL:
elif parameter.kind is _ParameterKind.VAR_POSITIONAL:
if isinstance(field, abc.Iterator):
# we don't want to exhaust iterator
yield '...'
else:
yield from map(to_positional_argument_string, field)
elif parameter.kind is inspect._KEYWORD_ONLY:
elif parameter.kind is _ParameterKind.KEYWORD_ONLY:
yield to_keyword_argument_string(parameter_name, field)
else:
yield from map(to_keyword_argument_string,
Expand Down
3 changes: 1 addition & 2 deletions reprit/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@

def group_by(iterable: Iterable[Domain],
*,
key: Map[Domain, Range] = lambda x: x
) -> Iterable[Tuple[Range, List[Domain]]]:
key: Map[Domain, Range]) -> Iterable[Tuple[Range, List[Domain]]]:
result = defaultdict(list)
for element in iterable:
result[key(element)].append(element)
Expand Down
50 changes: 39 additions & 11 deletions tests/base_tests/test_complex.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,32 @@
from reprit import seekers
from reprit.base import generate_repr
from tests import strategies
from tests.utils import Domain
from tests.utils import Domain, to_namespace


@given(strategies.complex_classes, strategies.booleans)
@given(strategies.complex_classes, strategies.booleans, strategies.booleans)
def test_basic(class_: Type[Domain],
prefer_keyword: bool) -> None:
prefer_keyword: bool,
with_module_name: bool) -> None:
result = generate_repr(class_.__init__,
field_seeker=seekers.complex_,
prefer_keyword=prefer_keyword)
prefer_keyword=prefer_keyword,
with_module_name=with_module_name)

assert callable(result)


@given(strategies.complex_classes_with_instances, strategies.booleans)
@given(strategies.complex_classes_with_instances, strategies.booleans,
strategies.booleans)
def test_call(class_with_instance: Tuple[Type[Domain], Domain],
prefer_keyword: bool) -> None:
prefer_keyword: bool,
with_module_name: bool) -> None:
cls, instance = class_with_instance

repr_ = generate_repr(cls.__init__,
field_seeker=seekers.complex_,
prefer_keyword=prefer_keyword)
prefer_keyword=prefer_keyword,
with_module_name=with_module_name)

result = repr_(instance)

Expand All @@ -39,17 +44,24 @@ def test_call(class_with_instance: Tuple[Type[Domain], Domain],
@pytest.mark.skipif(platform.python_implementation() == 'PyPy'
and sys.version_info > (3, 5, 3),
reason='Unreproducible failures on PyPy3.5.3')
@given(strategies.complex_classes_with_instances, strategies.booleans)
@given(strategies.complex_classes_with_instances, strategies.booleans,
strategies.booleans)
def test_evaluation(class_with_instance: Tuple[Type[Domain], Domain],
prefer_keyword: bool) -> None:
prefer_keyword: bool,
with_module_name: bool) -> None:
cls, instance = class_with_instance

repr_ = generate_repr(cls.__init__,
field_seeker=seekers.complex_,
prefer_keyword=prefer_keyword)
prefer_keyword=prefer_keyword,
with_module_name=with_module_name)
instance_repr = repr_(instance)

result = eval(instance_repr, {cls.__name__: cls})
result = eval(instance_repr,
to_namespace(cls.__module__ + '.' + cls.__qualname__
if with_module_name
else cls.__qualname__,
cls))

assert vars(instance) == vars(result)

Expand All @@ -66,3 +78,19 @@ def test_unsupported(class_with_instance: Tuple[Type[Domain], Domain],

with pytest.raises(AttributeError):
repr_(instance)


@given(strategies.complex_classes_with_instances, strategies.booleans)
def test_with_module_name(class_with_instance: Tuple[Type[Domain],
Domain],
prefer_keyword: bool) -> None:
cls, instance = class_with_instance

repr_ = generate_repr(cls.__init__,
field_seeker=seekers.complex_,
prefer_keyword=prefer_keyword,
with_module_name=True)

result = repr_(instance)

assert result.startswith(cls.__module__)
49 changes: 38 additions & 11 deletions tests/base_tests/test_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,30 @@

from reprit.base import generate_repr
from tests import strategies
from tests.utils import Domain
from tests.utils import Domain, to_namespace


@given(strategies.simple_classes, strategies.booleans)
def test_basic(cls: Type[Domain], prefer_keyword: bool) -> None:
@given(strategies.simple_classes, strategies.booleans, strategies.booleans)
def test_basic(cls: Type[Domain],
prefer_keyword: bool,
with_module_name: bool) -> None:
result = generate_repr(cls.__init__,
prefer_keyword=prefer_keyword)
prefer_keyword=prefer_keyword,
with_module_name=with_module_name)

assert callable(result)


@given(strategies.simple_classes_with_instances, strategies.booleans)
@given(strategies.simple_classes_with_instances, strategies.booleans,
strategies.booleans)
def test_call(class_with_instance: Tuple[Type[Domain], Domain],
prefer_keyword: bool) -> None:
prefer_keyword: bool,
with_module_name: bool) -> None:
cls, instance = class_with_instance

repr_ = generate_repr(cls.__init__,
prefer_keyword=prefer_keyword)
prefer_keyword=prefer_keyword,
with_module_name=with_module_name)

result = repr_(instance)

Expand All @@ -35,15 +41,36 @@ def test_call(class_with_instance: Tuple[Type[Domain], Domain],
@pytest.mark.skipif(platform.python_implementation() == 'PyPy'
and sys.version_info > (3, 5, 3),
reason='Unreproducible failures on PyPy3.5.3')
@given(strategies.simple_classes_with_instances, strategies.booleans)
@given(strategies.simple_classes_with_instances, strategies.booleans,
strategies.booleans)
def test_evaluation(class_with_instance: Tuple[Type[Domain], Domain],
prefer_keyword: bool) -> None:
prefer_keyword: bool,
with_module_name: bool) -> None:
cls, instance = class_with_instance

repr_ = generate_repr(cls.__init__,
prefer_keyword=prefer_keyword)
prefer_keyword=prefer_keyword,
with_module_name=with_module_name)
instance_repr = repr_(instance)

result = eval(instance_repr, {cls.__name__: cls})
result = eval(instance_repr,
to_namespace(cls.__module__ + '.' + cls.__qualname__
if with_module_name
else cls.__qualname__,
cls))

assert vars(result) == vars(instance)


@given(strategies.simple_classes_with_instances, strategies.booleans)
def test_with_module_name(class_with_instance: Tuple[Type[Domain], Domain],
prefer_keyword: bool) -> None:
cls, instance = class_with_instance

repr_ = generate_repr(cls.__init__,
prefer_keyword=prefer_keyword,
with_module_name=True)

result = repr_(instance)

assert result.startswith(cls.__module__)
20 changes: 19 additions & 1 deletion tests/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
from types import ModuleType
from typing import (Any,
TypeVar)
Dict,
TypeVar,
Union)

from hypothesis import (Phase,
core,
Expand Down Expand Up @@ -35,3 +38,18 @@ def condition(object_: Any) -> bool:
raise unpacking_error from search_error
else:
return result


def to_namespace(object_path: str, object_: Domain
) -> Dict[str, Union[Domain, ModuleType]]:
object_path_parts = object_path.split('.')
if len(object_path_parts) == 1:
return {object_path_parts[0]: object_}
step_module = ModuleType(object_path_parts[0])
result = {object_path_parts[0]: step_module}
for part in object_path_parts[1:-1]:
next_step_module = ModuleType(part)
setattr(step_module, part, next_step_module)
step_module = next_step_module
setattr(step_module, object_path_parts[-1], object_)
return result

0 comments on commit d465bb5

Please sign in to comment.