Last active
June 16, 2017 09:59
-
-
Save cdunklau/88d10a3409d0bf985371ddb055b22bd8 to your computer and use it in GitHub Desktop.
Type-checking declarative attributes via metaclass shenanigans
This file contains 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
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