tri.declarative contains class decorators to define classes with subclass semantics in the style of django Model classes.
Project description
tri.declarative
tri.declarative contains tools that make it easy to write declarative code. This includes:
class decorators to define classes with subclass semantics in the style of django Model classes
recursively evaluating embedded lambda expressions in complex data structures
recursively filtering of complex data structures
get/set attribute given a path string (e.g. ‘foo__bar__baz’)
Class decorators
With just a few lines of code, turn your API from:
quux = Foo(things=[Bar(name='a', param=1), Bar(name='b', param=2), Bar(name='c', param=2)], baz=3)
into:
class Quux(Foo):
a = Bar(param=1)
b = Bar(param=2)
c = Bar(param=2)
class Meta:
baz = 3
And you can still use the first style when it’s more convenient!
More detailed usage examples on @declarative below.
Evaluating
d = dict(
foo=lambda x: x*2,
bar=lambda y: y+5,
baz=[
foo=lambda x: x*6,
],
)
# evaluate only one level
assert evaluate(d, x=2) == dict(
foo=4,
bar=lambda y: y+5, # this function doesn't match the signature so isn't evaluated
baz=[
foo=lambda x: x*6, # one level down so isn't evaluated
],
)
# evaluate recursively
assert evaluate_recursive(d, x=2) == dict(
foo=4,
bar=lambda y: y+5, # this function doesn't match the signature so isn't evaluated
baz=[
foo=12,
],
)
Filtering
d = dict(
foo=dict(
show=False,
x=1,
),
bar=dict(
show=True,
x=2,
),
)
assert filter_show_recursive(d) == dict(
bar=dict(
show=True,
x=2,
),
)
Keyword argument dispatching
@dispatch:
@dispatch(
bar={},
baz__foo=2)
def foo(bar, baz):
do_bar(**bar)
do_baz(**baz)
Get/set attribute given a path string
class Foo:
def __init__(a):
self.a = a
class Bar:
def __init__(b):
self.b = b
class Baz:
def __init__(c):
self.c = c
x = Foo(Bar(Baz(c=3)))
assert getattr_path(x, 'a__b__c') == 3
assert setattr_path(x, 'a__b__c', 10)
assert getattr_path(x, 'a__b__c') == 10
Running tests
You need tox installed then just make test.
License
BSD
Documentation
Usage
@declarative
In the example below, the @declarative(str)
decorator will ensure that all str
members of class Foo will be
collected and sent as members
constructor keyword argument.
from tri_declarative import declarative
@declarative(str)
class Foo:
bar = 'barbar'
baz = 'bazbaz'
boink = 17
def __init__(self, members):
assert members['bar'] == 'barbar'
assert members['baz'] == 'bazbaz'
assert 'boink' not in members
f = Foo()
The value of the members
argument will also be collected from sub-classes:
from tri_declarative import declarative
@declarative(str)
class Foo:
def __init__(self, members):
assert members['bar'] == 'barbar'
assert members['baz'] == 'bazbaz'
class MyFoo(Foo):
bar = 'barbar'
baz = 'bazbaz'
def __init__(self):
super(MyFoo, self).__init__()
f = MyFoo()
The members
argument can be given another name (things
in the example below).
from tri_declarative.declarative import declarative
@declarative(str, 'things')
class Foo:
bar = 'barbar'
def __init__(self, **kwargs):
assert 'things' in kwargs
assert kwargs['things']['bar'] == 'barbar'
f = Foo()
Note that the collected dict is ordered by class inheritance and by using
sorted
of the values within each class. (In the ‘str’ example, sorted
yields in alphabetical order).
Also note that the collection of class members based on their class does not interfere with instance constructor argument of the same type.
from tri_declarative import declarative
@declarative(str)
class Foo:
charlie = '3'
alice = '1'
def __init__(self, members):
assert list(members.items()) == [('alice', '1'), ('charlie', '3'),
('bob', '2'), ('dave', '4'),
('eric', '5')])
assert 'animal' not in members
class MyFoo(Foo):
dave = '4'
bob = '2'
class MyOtherFoo(MyFoo):
eric = '5'
def __init__(self, animal)
assert animal == 'elephant'
f = MyOtherFoo('elephant')
Real world use-case
Below is a more complete example of using @declarative:
from tri_declarative import declarative, creation_ordered
@creation_ordered
class Field:
pass
class IntField(Field):
def render(self, value):
return '%s' % value
class StringField(Field):
def render(self, value):
return "'%s'" % value
@declarative(Field, 'table_fields')
class SimpleSQLModel:
def __init__(self, **kwargs):
self.table_fields = kwargs.pop('table_fields')
for name in kwargs:
assert name in self.table_fields
setattr(self, name, kwargs[name])
def insert_statement(self):
return 'INSERT INTO %s(%s) VALUES (%s)' % (self.__class__.__name__,
', '.join(self.table_fields.keys()),
', '.join([field.render(getattr(self, name))
for name, field in self.table_fields.items()]))
class User(SimpleSQLModel):
username = StringField()
password = StringField()
age = IntField()
my_user = User(username='Bruce_Wayne', password='Batman', age=42)
assert my_user.username == 'Bruce_Wayne'
assert my_user.password == 'Batman'
assert my_user.insert_statement() == "INSERT INTO User(username, password, age) VALUES ('Bruce_Wayne', 'Batman', 42)"
# Fields are ordered by creation time (due to having used the @creation_ordered decorator)
assert list(my_user.get_declared('table_fields').keys()) == ['username', 'password', 'age']
@with_meta
Class decorator to enable a class (and it’s sub-classes) to have a ‘Meta’ class attribute.
The members of the Meta class will be injected as arguments to constructor calls. e.g.:
from tri_declarative import with_meta
@with_meta
class Foo:
class Meta:
foo = 'bar'
def __init__(self, foo, buz):
assert foo == 'bar'
assert buz == 'buz'
foo = Foo(buz='buz')
# Members of the 'Meta' class can be accessed thru the get_meta() class method.
assert foo.get_meta() == {'foo': 'bar'}
assert Foo.get_meta() == {'foo': 'bar'}
Foo() # Crashes, has 'foo' parameter, but no has no 'buz' parameter.
The passing of the merged name space to the constructor is optional.
It can be disabled by passing add_init_kwargs=False
to the decorator.
from tri_declarative import with_meta
@with_meta(add_init_kwargs=False)
class Foo:
class Meta:
foo = 'bar'
Foo() # No longer crashes
assert Foo().get_meta() == {'foo': 'bar'}
Another example:
from tri_declarative import with_meta
class Foo:
class Meta:
foo = 'bar'
bar = 'bar'
@with_meta
class Bar(Foo):
class Meta:
foo = 'foo'
buz = 'buz'
def __init__(self, *args, **kwargs):
assert kwargs['foo'] == 'foo' # from Bar (overrides Foo)
assert kwargs['bar'] == 'bar' # from Foo
assert kwargs['buz'] == 'buz' # from Bar
This can be used e.g to enable sub-classes to modify constructor default arguments.
Changelog
5.7.0 (2020-12-18)
Make getattr_path more in line with the standard library getattr semantics
If a default value is provided, return that on missing attributes
If no default value is given, give a more detailed error message of what was missing
Added the special case of the empty path returning the object
5.6.0 (2020-12-02)
Fix corner case of class Meta failing to merge with None namespace values
5.5.0 (2020-08-21)
Include tri.struct 4.x as possible requirement
5.4.1 (2020-06-34)
Optimizations
5.4.0 (2020-04-16)
Minor bug fix on trailing comma explanation TypeException
Fix bug when nesting @class_shortcut with same name i sub classes
Refactor code to separate modules to get better stack traces
5.3.0 (2020-04-01)
Enable @class_shortcut to override baseclass shortcuts with the same name.
Fix @with_meta failing on method declarations with @staticmethod declaration
5.2.0 (2020-02-28)
The namespace merge is narrowed to only affect the @with_meta case.
Handle calling Namespace with call_target__attribute=None
5.1.1 (2020-02-11)
Improve namespace merge in @with_meta to not trip up @declarative
5.1.0 (2020-02-11)
Fix @with_meta argument injector to merge namespaces
5.0.1 (2019-02-03)
A minor update to the documentation generation to make it play nice with rST
5.0.0 (2019-01-30)
Added private field to shortcuts: __tri_declarative_shortcut_stack. This is useful to be able to figure out a shortcut stack after the fact
get_callable_description thought stuff that contained a lambda in its string representation was a lambda
- Removed all deprecated APIs/behaviors:
creation_ordered
The promotion of string values to keys in Namespace
Much improved error messages
4.0.1 (2019-10-23)
Bugfix to correctly handle Namespace as callable/not callable depending on content
4.0.0 (2019-10-11)
get_meta() now collects extra arguments in a Namespace to get consistent override behaviour.
should_show no longer accepts a callable as a valid return value. It will assert on this, because it’s always a mistake.
Added evaluate_strict and evaluate_recursive_strict that will not accept callables left over after the evaluation. If possible prefer these methods because they will stop the user of your library from making the mistake of not matching the given signature and ending up with an unevaluated callable in the output.
3.1.0 (2019-06-28)
Fixed issues when Namespace contained a key called any of items, values, keys, or get
Removed sorting on Namespace kwargs that isn’t needed in python 3 anymore. The sorting also destroys the given order which can be surprising
Removed old obsolete functions collect_namespaces, extract_subkeys, and setdefaults
3.0.0 (2019-06-10)
Renamed module from tri.declarative to tri_declarative. This is a breaking change
Dropped support for python2
2.0.0 (2019-04-12)
Fixed get_signature cache to not pollute struct-like dicts
New call_target semantics for class method shortcuts, this is a potential breaking change
1.2.1 (2019-13-15)
Improved documentation output
1.2.0 (2019-13-14)
Add get_members function to enable reuse of @declarative attribute collection
Add @class_shortcut decorator to enable @with_meta aware class shortcuts
1.1.0 (2018-11-22)
Added generate_rst_docs function.
1.0.6 (2018-09-28)
Shortcut is now a special case when merging Namespace objects. When already in a Namespace, a Shortcut now get overwritten by setitem_path(), not merged into the written value.
1.0.5 (2018-09-21)
Fix broken handling of empty key
1.0.4 (2018-09-21)
Cleanup Namespace path logic and make sure it is symmetrical and tested.
Added deprecation warning on string to dict promotion on namespace merge.
1.0.3 (2018-06-26)
Fixed release functionality
1.0.2 (2018-06-18)
Don’t support RefinableObject in evaluate_recursive. This was a mistake.
1.0.1 (2018-06-15)
Support RefinableObject in evaluate_recursive.
1.0.0 (2018-05-23)
Cleanup deprecation warnings from inspect.getargspec
0.34.0 (2017-08-21)
Fix bug in 0.33.0 when promoting callable to Namespace.
0.33.0 (2017-08-21)
Fix bug when promoting callable to Namespace.
Fix handling of EMPTY marker.
0.32.0 (2017-07-04)
Added promoting callable namespace members to Namespace with call_target in setdefaults_path.
0.31.0 (2017-06-15)
Improve sort_after to allow more combinations of after=… specifications. e.g. by name of an entry also moved by spec.
Changed name of first parameter of setdefaults_path to __target__ to avoid collitions with namespace parameters.
Added RefinableObject base for reuse by classes wanting to be able to be configured via constructor kwarg parameters in a declarative fashion. (The namespace of possible constructor overrides are declared with Refinable() for values and the decorator @refinable for methods.
Added first incarnation of crawling the definitions to recursively find available parameters on objects and their aggregates.
Added Shortcut abstraction to be able to find pre-defined set of overrides of RefinableObject classes.
0.30.0 (2017-02-10)
evaluate and evaluate_recursive also works for methods as well as for functions.
0.29.0 (2016-09-12)
Fixed loop detection in flatten for Namespaces. This resulted in data corruption.
0.28.0 (2016-07-15)
Added Namespace subclass of tri.struct.Struct to explicit capture the path splitting semantics. (And added method for flattening a Namespace back to path notation.)
0.27.0 (2016-07-13)
Fix bug in evaluate signature detection with optional arguments. (lambda a, b=17: a+b was correctly matched but lambda b, a=17: a+b was not)
0.26.0 (2016-05-06)
Added EMPTY marker to setdefaults_path to avoid mixup when empty dict is provided in function defaults.
0.25.0 (2016-04-28)
Added @dispatch decorator
0.24.0 (2016-04-20)
Fix bug in setdefault_path tripping up on key ordering.
Dropped namespace_factory keyword argument to setdefaults_path not likely ever beeing used.
0.23.0 (2016-04-15)
setdefaults_path now accepts multiple default dicts. (To simplify the pattern of shortcuts in tri.form, tri.query and tri.table where we now will end up with: new_kwargs = setdefaults_path(Struct(), kwargs, dict(….))
0.22.0 (2016-03-24)
sort_after() should produce an error when attempting to sort after non-existant keys
Tweaked namespace merge in setdefaults_path
0.21.0 (2016-03-01)
Fix corner case in collect_namespaces where one parameter imply a value and others imply a namespace.
Added setdefaults_path helper with __ namespace traversal.
0.20.0 (2016-02-29)
Added assert_kwargs_not_empty convenience function.
Improved documentation.
0.19.0 (2016-01-12)
When making instances of a class decorated with @declarative the declared values are copied (shallow) before being passed to __init__.
Instances will get an own copy of the declared attributes written to their __dict__
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file tri.declarative-5.7.0.tar.gz
.
File metadata
- Download URL: tri.declarative-5.7.0.tar.gz
- Upload date:
- Size: 24.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.22.0 setuptools/49.2.0 requests-toolbelt/0.9.1 tqdm/4.42.0 CPython/3.8.5
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 0980e3877a7896f519b108ffee75d1501c430a0bb0120b1f8825a3d118285b5f |
|
MD5 | 1e7da8b0a4d7c4025448e9e047704a5f |
|
BLAKE2b-256 | c0a25789afc9cba1c5ecd956172e83ea18d15db1cbcea24c8ebc2964ec3823b5 |
File details
Details for the file tri.declarative-5.7.0-py2.py3-none-any.whl
.
File metadata
- Download URL: tri.declarative-5.7.0-py2.py3-none-any.whl
- Upload date:
- Size: 20.2 kB
- Tags: Python 2, Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/3.1.1 pkginfo/1.5.0.1 requests/2.22.0 setuptools/49.2.0 requests-toolbelt/0.9.1 tqdm/4.42.0 CPython/3.8.5
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | cb0ee59b183b0d17fe0f1eb7518de883bd5e20b1129db9351e90c4099973a105 |
|
MD5 | 69d37f46279dba90c395067a8befc6fb |
|
BLAKE2b-256 | ac0b4c756966d84f77a18760cf11641da2a796ac63eec953052570ff981884c0 |