Custom Types
The predefined types we've seen before are actually just simple functions, here
is for instance the definition of enotype's color
type:
import re
COLOR_RE = re.compile(r'^\s*#[0-9a-f]{3}([0-9a-f]{3})?\s*$', re.IGNORECASE)
def color(value):
if not COLOR_RE.match(value):
raise ValueError('A color is required, for instance \'#B6D918\', \'#fff\' or \'#01b\'.')
return value
So as we see, all that's required for a custom type definition is a function that takes a single parameter - the value - and returns something!
Additionally, depending on the type, we might also include validation as demonstrated above - we simply raise a string message if there is a problem. When that happens the message is intercepted by enolib and embedded within a rich error message with metadata and a code snippet from the document.
import enolib
extra_foo = lambda value: value + ' with extra foo!'
document = enolib.parse('foo: bar')
document.field('foo').required_value(extra_foo) # returns 'bar with extra foo!'
And just like that we are using our own type. Here's another example using the global registering magic from the previous chapter:
import enolib
def foo_only(value):
if value != 'foo':
raise ValueError('Only foo is accepted!')
return value
enolib.register(foo_only)
document = enolib.parse('foo: bar')
document.field('foo').foo_only_key() # returns 'foo'
document.field('foo').required_foo_only_value() # raises "Only foo is accepted!" with a code snippet
As a final note why it is sometimes preferable to use the less magicky
function parameter type passing: On the one hand you might dynamically construct
a type loader multiple times in your application when it depends on some local
state (closure). On the other hand your type might not be a type per se but more of
a conversion or modification, something like replace_newlines
or render_markdown
,
and in that case .required_value(render_markdown)
looks a bit more sensible than
.required_render_markdown_value()
.
Next page: Querying elements