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


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