Last active
August 29, 2015 13:56
-
-
Save BrianHicks/9070717 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""\ | |
TinyObj does the bare minimum to make a nice object layer on top of datastores | |
which return dicts. It can set defaults, validate data types, and provides a | |
dot-access style interface for getting and setting attributes (truthfully, | |
because it just sprinkles a little magic Python dust over the dicts so they are | |
full Python objects with all of those qualities.) | |
""" | |
import datetime | |
class Field(object): | |
"""base for other fields""" | |
def __init__(self): | |
self.default = None | |
def initialize(self, value=()): | |
"""\ | |
initialize returns a cleaned value or the default, raising ValueErrors | |
as necessary. | |
""" | |
if value is (): | |
try: | |
return self.default() | |
except TypeError: | |
return self.default | |
else: | |
return self.clean(value) | |
def clean(self, value): | |
"""clean a value, returning the cleaned value""" | |
raise NotImplementedError | |
class UnicodeField(Field): | |
"""accept and validate unicode""" | |
def __init__(self): | |
self.default = unicode() | |
def clean(self, value): | |
return unicode(value) | |
class NumberField(Field): | |
"""accept and validate numbers""" | |
def __init__(self, t=float, allow_negative=True, allow_positive=True): | |
""" | |
take a type to coerce values to, can be (EG) ``float``, ``int``, | |
``long``, or ``complex``. | |
""" | |
self.t = t | |
self.default = t() | |
self.allow_negative = allow_negative | |
self.allow_positive = allow_positive | |
def clean(self, value): | |
if not isinstance(value, self.t): | |
value = self.t(value) | |
if not self.allow_negative and value < 0: | |
raise ValueError('value was negative') | |
if not self.allow_positive and value > 0: | |
raise ValueError('values was positive') | |
return value | |
class BoolField(Field): | |
"""accept and validate boolean numbers | |
note that this field will just call ``bool`` on values, this may not be | |
your desired behavior so you might want to implement a subclass that parses | |
truthy/falsey values in a way specific to your application""" | |
def __init__(self, default=False): | |
self.clean = bool | |
self.default = bool(default) | |
class NoValidationField(Field): | |
"""don't validate at all, but return the value passed defaulting to None""" | |
def initialize(self, value=None): | |
return value | |
class FieldParserMetaclass(type): | |
"""\ | |
FieldParserMetaclass moves instances of ``Field`` to the ``_fields`` | |
attribute on the class, preparing for actual values to be set in their | |
place. | |
""" | |
def __init__(self, name, parents, attrs): | |
fields = {} | |
for name, value in attrs.items(): | |
if isinstance(value, Field): | |
fields[name] = value | |
self._fields = fields | |
class TinyObj(object): | |
"""\ | |
TinyObj is the main superclass for objects which want to tie into the | |
getters/setters/deserialization/serialization mechanism. Subclass TinyObj | |
and provide a number of ``Field``s, like so:: | |
class User(TinyObj): | |
username = UnicodeField() | |
password = UnicodeField() | |
active = BoolField(default=True) | |
def __unicode__(self): | |
return self.username | |
Then just use the fields like Python objects (because they are.):: | |
u = User(username='test', password='plaintext') | |
assert u.username == 'test' | |
assert u.password == 'plaintext' | |
assert u.active == False | |
""" | |
__metaclass__ = FieldParserMetaclass | |
def __init__(self, *doc, **attrs): | |
"""\ | |
The expected usage pattern is to either pass a single dict or a number | |
of attributes. You can do both, the attributes will take precedence. | |
""" | |
source = {} | |
for item in doc: | |
source.update(doc) | |
source.update(attrs) | |
for key in set(source.keys()) | set(self._fields.keys()): | |
if key not in self._fields: | |
value = source[key] | |
elif key not in attrs: | |
value = self._fields[key].initialize() | |
else: | |
value = self._fields[key].initialize(source[key]) | |
self.__dict__[key] = value | |
def __repr__(self): | |
if hasattr(self, '__unicode__'): | |
return u'<{}: {}>'.format(self.__class__.__name__, unicode(self)) | |
else: | |
return u'<{}>'.format(self.__class__.__name__) | |
def to_dict(self): | |
"""return a dict of this object's fields""" | |
return self.__dict__ | |
if __name__ == '__main__': | |
class User(TinyObj): | |
username = UnicodeField() | |
password = UnicodeField() | |
active = BoolField(default=True) | |
def __unicode__(self): | |
return self.username | |
# simple assertion tests | |
user = User(username='test', password='plaintext') | |
assert user.username == 'test' | |
assert user.password == 'plaintext' | |
assert user.active == True |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment