Skip to content

Instantly share code, notes, and snippets.

@cdunklau
Last active June 16, 2017 09:59
Show Gist options
  • Save cdunklau/88d10a3409d0bf985371ddb055b22bd8 to your computer and use it in GitHub Desktop.
Save cdunklau/88d10a3409d0bf985371ddb055b22bd8 to your computer and use it in GitHub Desktop.
Type-checking declarative attributes via metaclass shenanigans
import six
class TypedField(object):
def __init__(self, classinfo):
self._classinfo = classinfo
self.fieldname = None # Set by the metaclass
def __get__(self, instance, owner):
try:
return instance._fieldvalues[self.fieldname]
except KeyError:
raise AttributeError('Attribute {0!r} is not set'.format(
self.fieldname
))
def __set__(self, instance, value):
if not isinstance(value, self._classinfo):
raise TypeError('Invalid type {0!r} for attribute {1!r}'.format(
type(value), self.fieldname,
))
instance._fieldvalues[self.fieldname] = value
def __delete__(self, instance):
try:
del instance._fieldvalues[self.fieldname]
except KeyError:
raise AttributeError('Attribute {0!r} is not set'.format(
self.fieldname
))
class FieldsMeta(type):
def __new__(metacls, classname, bases, classdict):
for attrname, attrvalue in classdict.items():
if isinstance(attrvalue, TypedField):
attrvalue.fieldname = attrname
return super(FieldsMeta, metacls).__new__(
metacls, classname, bases, classdict)
class ModelBase(six.with_metaclass(FieldsMeta, object)):
def __init__(self):
self._fieldvalues = {}
class MyModel(ModelBase):
name = TypedField(str)
index = TypedField(int)
import unittest
class ShenanigansTestCase(unittest.TestCase):
def test_setting_and_getting(self):
m = MyModel()
m.name = 'Alice'
m.index = 1
self.assertEqual(m.name, 'Alice')
self.assertEqual(m.index, 1)
def test_setting_checks_type(self):
m = MyModel()
with self.assertRaises(TypeError) as excinfo:
m.index = 'not an int'
self.assertIn(
'Invalid type {0!r}'.format(str),
str(excinfo.exception)
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment