Skip to content
This repository was archived by the owner on Apr 22, 2020. It is now read-only.
Open
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
34 changes: 33 additions & 1 deletion easypy/collections.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from __future__ import absolute_import
import collections
from numbers import Integral
from itertools import chain, islice
from itertools import chain, islice, count
from functools import partial
import random
from contextlib import contextmanager
from .predicates import make_predicate
from .tokens import UNIQUE

Expand Down Expand Up @@ -684,3 +685,34 @@ def append(self, item):
super().append(item)
if len(self) > self.size:
self.pop(0)


class ContextCollection(object):
"""
A collection where you add and remove things using context managers::

cc = ContextCollection()

assert list(cc) == []
with cc.added(10):
assert list(cc) == [10]
with cc.added(20):
assert list(cc) == [10, 20]
assert list(cc) == [10]
assert list(cc) == []
"""
def __init__(self):
self._items = PythonOrderedDict()
self._index_generator = count(1)

def __iter__(self):
return iter(self._items.values())

@contextmanager
def added(self, value):
index = next(self._index_generator)
try:
self._items[index] = value
yield index
finally:
del self._items[index]
25 changes: 25 additions & 0 deletions easypy/decorations.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,28 @@ def foo(self):
def wrapper(func):
return LazyDecoratorDescriptor(decorator_factory, func)
return wrapper


class RegistryDecorator(object):
"""
A factory for simple decorators that register functions by name::

register = RegistryDecorator()

@register
def foo():
return 'I am foo'

@register
def bar():
return 'I am bar'

assert register.registry['foo']() == 'I am foo'
assert register.registry['bar']() == 'I am bar'
"""
def __init__(self):
self.registry = {}

def __call__(self, fn):
self.registry[fn.__name__] = fn
return fn
136 changes: 135 additions & 1 deletion easypy/typed_struct.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from copy import deepcopy
from contextlib import contextmanager
from itertools import count

from .exceptions import TException
from .tokens import AUTO, MANDATORY
from .collections import ListCollection, PythonOrderedDict, iterable
from .collections import ListCollection, PythonOrderedDict, iterable, ContextCollection
from .bunch import Bunch, bunchify
from .decorations import RegistryDecorator


class InvalidFieldType(TException):
Expand Down Expand Up @@ -98,6 +101,12 @@ def __init__(self, type, *, default=MANDATORY, preprocess=None, meta=Bunch()):
>>> a.default = 12
>>> Foo()
Foo(a=12)

Alternatively, use the ``DEFAULT`` context manager::

class Foo(TypedStruct):
with DEFAULT(12):
a = int
"""

# NOTE: _validate_type() will be also be called in _process_new_value()
Expand All @@ -107,6 +116,36 @@ def __init__(self, type, *, default=MANDATORY, preprocess=None, meta=Bunch()):
# specific FieldTypeMismatch.
self.preprocess = preprocess or self._validate_type
self.meta = Bunch(meta)
"""
Metadata for the field, to be used for reflection

>>> class Foo(TypedStruct):
>>> a = int
>>> a.meta.caption = 'Field A'
>>>
>>> b = int
>>> b.meta.caption = 'Field B'
>>>
>>> Foo.a.meta
Bunch(caption='Field A')
>>> Foo.b.meta
Bunch(caption='Field B')
>>> foo = Foo(a=1, b=2)
>>>
>>> for k, v in foo.items():
>>> caption = getattr(type(foo), k).meta.caption
>>> print('%s:\t%s' % (caption, v))
Field A: 1
Field B: 2

Alternatively, use the ``META`` context manager::

class Foo(TypedStruct):
with META(caption='Field A'):
a = int
with META(caption='Field B'):
b = int
"""
self.name = None

if issubclass(self.type, TypedStruct):
Expand Down Expand Up @@ -141,6 +180,12 @@ def add_validation(self, predicate, ex_type, *ex_args, **ex_kwargs):
Foo(a=5)
>>> Foo(a=15)
ValueError: value for `a` is too big

Alternatively, use the ``VALIDATION`` context manager::

class Foo(TypedStruct):
with VALIDATION(lambda value: value < 10, ValueError, 'value for `a` is too big'):
a = int
"""
orig_preprocess = self.preprocess

Expand Down Expand Up @@ -173,6 +218,13 @@ def add_conversion(self, predicate, conversion):
Foo(a=2)
>>> Foo(a=[10, 20, 30]) # the list has 3 items
Foo(a=3)


Alternatively, use the ``CONVERSION`` context manager::

class Foo(TypedStruct):
with CONVERSION(str, int), CONVERSION(list, len):
a = int
"""
if isinstance(predicate, type):
typ = predicate
Expand Down Expand Up @@ -211,6 +263,12 @@ def convertible_from(self, *predicates, conversion=AUTO):
Foo(a=2)
>>> Foo(a=3.0)
Foo(a=3)

Alternatively, use the ``CONVERTIBLE_FROM`` context manager::

class Foo(TypedStruct):
CONVERTIBLE_FROM(str, float):
a = int
"""
if conversion is AUTO:
conversion = self.type
Expand Down Expand Up @@ -382,16 +440,65 @@ def altered_dct_gen():


class _TypedStructDslDict(PythonOrderedDict):
field_context = RegistryDecorator()

def __init__(self):
super().__init__()
self.active_field_contexts = ContextCollection()

def __getitem__(self, name):
try:
field_context = self.field_context.registry[name]
except KeyError:
return super().__getitem__(name)
else:
return field_context.__get__(self)

def __setitem__(self, name, value):
if isinstance(value, Field):
value = value._named(name)
self.apply_field_contexts(value)
else:
try:
value = Field(value)._named(name)
except InvalidFieldType:
pass
self.apply_field_contexts(value)
return super().__setitem__(name, value)

def apply_field_contexts(self, field):
for field_context in self.active_field_contexts:
field_context(field)
pass

@field_context
@contextmanager
def FIELD_SETTING(self, dlg):
with self.active_field_contexts.added(dlg):
yield

@field_context
def DEFAULT(self, default):
def applier(field):
field.default = default
return self.FIELD_SETTING(applier)

@field_context
def VALIDATION(self, predicate, ex_type, *ex_args, **ex_kwargs):
return self.FIELD_SETTING(lambda field: field.add_validation(predicate, ex_type, *ex_args, **ex_kwargs))

@field_context
def CONVERSION(self, predicate, conversion):
return self.FIELD_SETTING(lambda field: field.add_conversion(predicate, conversion))

@field_context
def CONVERTIBLE_FROM(self, *predicates, conversion=AUTO):
return self.FIELD_SETTING(lambda field: field.convertible_from(*predicates, conversion=conversion))

@field_context
def META(self, **kwargs):
return self.FIELD_SETTING(lambda field: field.meta.update(kwargs))


class TypedStruct(dict, metaclass=TypedStructMeta):
"""
Expand Down Expand Up @@ -422,6 +529,33 @@ class Foo(TypedStruct):
a = int
a.default = 20
a.convertible_from(str, float)

Alternatively, you can use the special context managers::

from easypy.typed_struct import TypedStruct

class Foo(TypedStruct):
with DEFAULT(20), CONVERTIBLE_FROM(str, float):
a = int

If you have a complex setting you need to apply to the fields you can use
the FIELD_SETTING context manager::

from easypy.typed_struct import TypedStruct

def my_field_setting(field):
field.default = 50
field.convertible_from(str, float)
field.add_validation(lambda n: 0 <= n <= 100, ValueError, 'number not in range')

class Foo(TypedStruct):
with FIELD_SETTING(my_field_setting):
a = int
b = int
c = int

You do not need to import these special context managers - they will
automatically be there when you define the typed struct.
"""

@classmethod
Expand Down
28 changes: 28 additions & 0 deletions tests/test_collections.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import pytest
from easypy.collections import separate
from easypy.collections import ListCollection, partial_dict, UNIQUE, ObjectNotFound
from easypy.collections import ContextCollection
from easypy.bunch import Bunch
from collections import Counter
from contextlib import ExitStack


class O(Bunch):
Expand Down Expand Up @@ -98,3 +100,29 @@ def test_collections_slicing():
assert L[-2:] == list('ef')
assert L[::2] == list('ace')
assert L[::-2] == list('fdb')


def test_context_collection():
cc = ContextCollection()

assert list(cc) == []
with cc.added(10):
assert list(cc) == [10]
with cc.added(20):
assert list(cc) == [10, 20]
assert list(cc) == [10]
assert list(cc) == []

with ExitStack() as stack:
with cc.added(30):
assert list(cc) == [30]

stack.enter_context(cc.added(40))
assert list(cc) == [30, 40]

assert list(cc) == [40]

with cc.added(50):
assert list(cc) == [40, 50]
assert list(cc) == [40]
assert list(cc) == []
16 changes: 16 additions & 0 deletions tests/test_decorations.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from functools import wraps

from easypy.decorations import deprecated_arguments, kwargs_resilient, lazy_decorator
from easypy.decorations import RegistryDecorator


def test_deprecated_arguments():
Expand Down Expand Up @@ -111,3 +112,18 @@ def foo(self):
foo.num = 20
assert foo.foo() == 21
assert foo.foo.__name__ == 'foo + 20'


def test_registry_decorator():
register = RegistryDecorator()

@register
def foo():
return 'I am foo'

@register
def bar():
return 'I am bar'

assert register.registry['foo']() == 'I am foo'
assert register.registry['bar']() == 'I am bar'
Loading