Skip to content

Commit

Permalink
Improve subclassability/extendability of attribute types.
Browse files Browse the repository at this point in the history
Attribute now use class-scope defaults for most, if not all, Attribute
properties. That allows subclasses to have a common setup, e.g. an Email
type that is a String subclass that always provides a LooksLikeAnEmail
validator.

Purely as a nod to syntax, the default validator is an Always validator.
That allows:

SomeType(validator=All(SomeType.validator, MyValidator()))

Instead of:

SomeType(validator=(MyValidator() if not SomeType.validator else
All(SomeType.validator, MyValidator())))

The 2nd version is very ugly and uses duplicate code.
  • Loading branch information
Matt Goodall committed Nov 28, 2008
1 parent fe82899 commit b08936a
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 17 deletions.
1 change: 1 addition & 0 deletions schemaish.egg-info/SOURCES.txt
Expand Up @@ -8,4 +8,5 @@ schemaish.egg-info/SOURCES.txt
schemaish.egg-info/dependency_links.txt
schemaish.egg-info/entry_points.txt
schemaish.egg-info/not-zip-safe
schemaish.egg-info/requires.txt
schemaish.egg-info/top_level.txt
41 changes: 32 additions & 9 deletions schemaish/attr.py
Expand Up @@ -10,11 +10,17 @@
import itertools
from formencode import Invalid

from schemaish import validators


# Internal counter used to ensure the order of a meta structure's attributes is
# maintained.
_meta_order = itertools.count()


_MISSING = object()


class Attribute(object):
"""
Abstract base class for all attribute types in the package.
Expand All @@ -24,6 +30,10 @@ class Attribute(object):
@ivar validator: Optional FormEncode validator.
"""

title = None
description = None
validator = validators.Always()

def __init__(self, **k):
"""
Create a new attribute.
Expand All @@ -32,16 +42,22 @@ def __init__(self, **k):
@keyword description: Optional description.
@keyword validator: Optional FormEncode validator.
"""
self.title = k.pop('title', None)
self.description = k.pop('description', None)
self.validator = k.pop('validator', None)
self._meta_order = _meta_order.next()
title = k.pop('title', _MISSING)
if title is not _MISSING:
self.title = title
description = k.pop('description', _MISSING)
if description is not _MISSING:
self.description = description
validator = k.pop('validator', _MISSING)
if validator is not _MISSING:
self.validator = validator

def validate(self, value):
"""
Validate the value if a validator has been provided.
"""
if self.validator is None:
if not self.validator:
return
self.validator.to_python(value)

Expand Down Expand Up @@ -94,6 +110,7 @@ class DateTime(Attribute):
"""
pass


class Boolean(Attribute):
"""
A Python Boolean instance.
Expand All @@ -108,14 +125,17 @@ class Sequence(Attribute):
@ivar attr: Attribute type of items in the sequence.
"""

def __init__(self, attr, **k):
attr = None

def __init__(self, attr=None, **k):
"""
Create a new Sequence instance.
@keyword attr: Attribute type of items in the sequence.
"""
super(Sequence, self).__init__(**k)
self.attr = attr
if attr is not None:
self.attr = attr

def validate(self, value):
"""
Expand All @@ -142,22 +162,24 @@ def validate(self, value):
raise Invalid(e.message, value, None, error_dict = errors)



class Tuple(Attribute):
"""
A Python tuple of attributes of specific types.
@ivar attrs: List of Attributes that define the items in the tuple.
"""

def __init__(self, attrs, **k):
attrs = None

def __init__(self, attrs=None, **k):
"""
Create a Tuple instance.
@param attrs: List of Attributes that define the items in the tuple.
"""
super(Tuple, self).__init__(**k)
self.attrs = attrs
if attrs is not None:
self.attrs = attrs

def validate(self, value):
"""
Expand Down Expand Up @@ -280,3 +302,4 @@ class File(Attribute):
A File Object
"""
pass

65 changes: 60 additions & 5 deletions schemaish/tests/test_attr.py
Expand Up @@ -2,15 +2,18 @@
import unittest

from schemaish import *
from schemaish.attr import Attribute


class TestCore(unittest.TestCase):

def test_blank(self):
a = String()
self.assertTrue(a.title is None)
self.assertTrue(a.description is None)
self.assertTrue(a.validator is None)
def test_defaults(self):
attr = Attribute()
assert not attr.title
assert not attr.description
assert not attr.description
assert not attr.validator
assert isinstance(attr.validator, Always)

def test_positional(self):
self.assertRaises(TypeError, String, "a")
Expand All @@ -26,6 +29,35 @@ def test_meta_order(self):
b = String()
self.assertTrue(a._meta_order < b._meta_order)

def test_subclass(self):
class Something(Attribute):
title = 'Title'
description = 'Description'
validator = NotEmpty()
attr = Something()
assert attr.title is Something.title
assert attr.description is Something.description
assert attr.validator is Something.validator
attr = Something(title=None, description=None, validator=None)
assert attr.title is None
assert attr.description is None
assert attr.validator is None

def test_extend_default_validator(self):
"""
Check that the default validator can be extended without testing that
the default validator is anything useful..
# XXX: There's no point in testing Any because it's a silly way to
# extend the default.
"""
attr = String(validator=All(String.validator, NotEmpty()))
attr.validate('foo')
self.assertRaises(Invalid, attr.validate, '')
attr = String(validator=(NotEmpty() if not String.validator else All(String.validator, NotEmpty())))
attr.validate('foo')
self.assertRaises(Invalid, attr.validate, '')


class TestString(unittest.TestCase):

Expand Down Expand Up @@ -68,6 +100,14 @@ def test_nested_validation(self):
s.validate([{'str': 'one'}])
self.assertRaises(Invalid, s.validate, [{}])

def test_subclass(self):
class StringSequence(Sequence):
attr = String()
class DateSequence(Sequence):
attr = Date()
assert isinstance(StringSequence().attr, String)
assert isinstance(DateSequence().attr, Date)


class TestTuple(unittest.TestCase):

Expand All @@ -87,6 +127,21 @@ def test_validate(self):
def test_num_items(self):
self.assertRaises(Invalid, Tuple([String(), String()]).validate, ("one",))

def test_subclass(self):
class Tuple1(Tuple):
attrs = [String(), String(), String()]
class Tuple2(Tuple):
attrs = [String(), Date()]
t1 = Tuple1()
assert len(t1.attrs) == 3
assert isinstance(t1.attrs[0], String)
assert isinstance(t1.attrs[1], String)
assert isinstance(t1.attrs[2], String)
t2 = Tuple2()
assert len(t2.attrs) == 2
assert isinstance(t2.attrs[0], String)
assert isinstance(t2.attrs[1], Date)


class TestStructure(unittest.TestCase):

Expand Down
18 changes: 18 additions & 0 deletions schemaish/tests/test_validators.py
@@ -0,0 +1,18 @@
import unittest
import schemaish


class TestValidators(unittest.TestCase):

def test_always(self):
v = schemaish.Always()
v.to_python('foo')
v.to_python('')
v.to_python(1)
v.to_python([1,2,3])
v.to_python(None)


if __name__ == '__main__':
unittest.main()

24 changes: 21 additions & 3 deletions schemaish/validators.py
Expand Up @@ -9,9 +9,12 @@

__all__ = [
# Re-export FormEncode's validators.
'All', 'Any', 'CIDR', 'DateValidator', 'Email', 'Empty', 'MACAddress',
'MinLength', 'MaxLength', 'NotEmpty', 'OneOf', 'PlainText', 'Regex',
'URL', 'Wrapper'
'All', 'Any',
'CIDR', 'DateValidator', 'Email', 'Empty', 'MACAddress', 'MinLength',
'MaxLength', 'NotEmpty', 'OneOf', 'PlainText', 'Regex', 'URL',
'Wrapper',
# Export our validators.
'Always',
]


Expand All @@ -20,3 +23,18 @@
MinLength, MaxLength, MACAddress, NotEmpty, OneOf, PlainText, Regex, \
URL, Wrapper


class Always(object):
"""
A validator that always passes, mostly useful as a default.
This validator tests False to make it seem "invisible" to discourage anyone
bothering actually calling it.
"""

def to_python(self, value, state=None):
pass

def __nonzero__(self):
return False

0 comments on commit b08936a

Please sign in to comment.