Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
1935 lines (1443 sloc) 47.4 KB
language: python
title: enopy
version: 0.9.1
-- intro
> Everyone is welcome to improve this documentation by editing [python.eno](https://github.com/eno-lang/eno-lang.org/blob/master/src/docs/python.eno) and submitting a Pull Request!
## Installation
``` shell
pip install enopy
```
## Getting started
Create an eno document, for instance `intro.eno`:
```eno
Greeting: Hello World!
```
A minimal example to read this file with `enopy`:
```python
>>> from enopy import parse
>>> with open('intro.eno', 'r') as file:
... document = parse(file.read())
>>> document.field('Greeting')
'Hello World!'
```
## Links
Package on PyPI - <https://pypi.org/project/enopy/>
Repository on github - <https://github.com/eno-lang/enopy/>
-- intro
# Modules
## enopy
-- class description
The main module. You'll be using `parse` from this, and possibly supplying a custom
locale (such as `'de'`, `'es'`, ...), reporter type (`eno.reporters.Text`, `eno.reporters.HTML`, `eno.reporters.Terminal` are available)
or source label (usually to have filename appear in error messages) to that call.
-- class description
### parse
syntax:
- parse(input) → Section
- parse(input, options) → Section
-- description
Parse a string in eno notation.
-- description
-- eno
color: blue
-- eno
-- python
>>> enopy.parse(input)
<class Section document length="1">
# Errors will be ...
>>> enopy.parse(input, locale='es') # In spanish - ¡hola!
>>> enopy.parse(input, reporter=eno.reporters.HTML) # HTML for e.g. web integration
>>> enopy.parse(input, reporter=eno.reporters.Terminal) # Colored for terminal output
>>> enopy.parse(input, source_label='my-file.eno') # Annotated with a label as context
>>> enopy.parse(input, zero_indexing=True) # Counting line and column numbers from 0
-- python
#### parameters
input: A string containing text in eno notation.
options:
locale = A string specifying the code of the locale to use for error messages (e.g. `'de'`, `'es'`), by default `'en'`.
reporter = One of `enopy.reporters.Text`, `enopy.reporters.HTML`, `enopy.reporters.Terminal`.
source_label = A string that labels where the input comes from (e.g. a filename), this is included with error messages to help the user located the file.
zero_indexing = `True` or `False` (default), set `True` to display `0,1,2,..` line and column numbers in all error messages instead of the default `1,2,3,..` indexing.
#### return value
description: A `Section` representing the document.
## loaders
-- class description
enopy provides a set of core loaders for important types that are available
out of the box (in addition to the possiblity to pass your own custom loaders
as functions/lambdas on all accessors).
The loaders are exposed through the API as drop in replacements to the standard accessors:
- `Field`
- `value()` => `[loader_name]()`
- `Fieldset`
- `entry()` => `[loader_name]()`
- `List`
- `items()` => `[loader_name]_items()`
- `Section`
- `field()` => `[loader_name]()`
- `list()` => `[loader_name]_list()`
So for instance, instead of calling ...
```python
>>> document.field('done', required=True)
'yes'
>>> document.list('visitor_counts', required=True)
[ '476', '213', '330', ... ]
```
... you can just replace `field` or augment `list` with the loader name ...
```python
>>> document.boolean('done', required=True)
True
>>> document.integer_list('visitor_counts', required=True)
[ 476, 213, 330, ... ]
```
... and the method signature stays exactly the same as for the original accessor (except you can't provide a loader as an argument anymore).
Here's another full example:
```python
>>> from enopy import parse
>>> doc = parse("""
... publish: yes
... location: 36.987094, -25.091719
... contact: contact@faulty
... """)
>>> doc.boolean('publish')
True
>>> doc.lat_lng('location')
{ 'lat': 36.987094, 'lng': -25.091719 }
>>> doc.email('contact')
ValidationError: 'contact' must contain a valid email address, for instance 'jane.doe@eno-lang.org'.
```
Note that some loaders only perform validation and return their input unaltered
as string (e.g. `color`, `email`), while others both validate and transform the
value into a new type (e.g. `float`, `boolean`) or even object (e.g. `lat_lng`).
-- class description
### boolean
-- description
Accepts `true`, `false`, `yes` and `no`, returns a `boolean` (or raises an error for invalid values).
-- description
syntax:
- boolean() → boolean
- boolean_items() → list
- boolean_list() → list
-- eno
enabled: true
publish: no
visible: not
-- eno
-- python
>>> document = Eno.parse(input)
>>> document.boolean('enabled')
True
>>> document.boolean('publish')
False
>>> document.boolean('visible')
ValidationError: 'visible' must contain a boolean - allowed values are 'true', 'false', 'yes' and 'no'.
-- python
### color
-- description
Accepts values formatted as `#RRGGBB` or `#RGB` (case insensitive), returns the value unchanged (or raises an error for invalid values).
-- description
syntax:
- color() → string
- color_items() → list
- color_list() → list
-- eno
active: #F00
hover: #ff0000
focus: blue
-- eno
-- python
>>> document = Eno.parse(input)
>>> document.color('active')
'#F00'
>>> document.color('hover')
'#ff0000'
>>> document.color('focus')
ValidationError: 'focus' must contain a color, for instance '#B6D918', '#fff' or '#01b'.
-- python
### comma_separated
-- description
Splits a comma-separated listing (single line, think a list of tags e.g.) into a list and strips whitespace.
-- description
syntax:
- comma_separated() → list
- comma_separated_items() → list
- comma_separated_list() → list
-- eno
Tags: Coffee, Desk, Flavor
Categories: Computing , High performance , Cluster
-- eno
-- python
>>> document = parse(input)
>>> document.comma_separated('Tags')
['Coffee', 'Desk', 'Flavor']
>>> document.comma_separated('Categories')
['Computing', 'High performance', 'Cluster']
-- python
### date
-- description
Accepts `YYYY-MM-DD` and returns a python `date` (or raises an error for invalid values).
-- description
syntax:
- date → date
- date_items → list
- date_list → list
-- eno
start: 1992-12-01
end: 1994-03-04
invalid: 2018-03-14 13:10
-- eno
-- python
document = parse(input)
document.date('start') #=> <class date>
document.date('end') #=> <class date>
document.date('invalid')
# raises an error: 'invalid' must contain a valid date, for instance '1993-11-18'.
-- python
### datetime
-- description
Accepts a subset of ISO 8601 date/time formats described in this W3C note: <https://www.w3.org/TR/NOTE-datetime>
Returns a python `datetime` object (or raises an error for invalid values).
Some possible example values that match the format:
- `1990`
- `1990-12`
- `1990-12-31`
- `1990-12-31T23:59Z`
- `1990-12-31T23:59:00+01:00`
- `1990-12-31T23:59:00.999+01:00`
Note that spaces as separators are not allowed.
Development note:
A more appealing, less technical datetime format or an additional loader to serve that requirement would be generally welcome,
the current format is basically just an initial draft that should cover many usecases and is standardized through its
ISO origin, feel free to get in touch and propose an extension/update if you'd be interested in such development.
-- description
syntax:
- datetime() → datetime
- datetime_items() → list
- datetime_list() → list
-- eno
start: 1992-12-01
end: 1994-03-04T13:14Z
invalid: 2018-03-14 13:10
-- eno
-- python
>>> document = Eno.parse(input)
>>> document.datetime('start')
<class datetime ...>
>>> document.datetime('end')
<class datetime ...>
>>> document.datetime('invalid')
ValidationError: 'invalid' must contain a valid date or date and time, for instance '1997-07-16' or '1994-11-05T13:15Z' (see https://www.w3.org/TR/NOTE-datetime).
-- python
### float
-- description
Accepts floats (decimal part can be missing though), returns a float (or raises an error for malformed values).
-- description
syntax:
- float() → float
- float_items() → list
- float_list() → list
-- eno
good: 49.9
fine: -2
bad: three
-- eno
-- python
>>> document = Eno.parse(input)
>>> document.float('good')
49.9
>>> document.float('fine')
-2.0
>>> document.float('bad')
ValidationError: 'bad' must contain a decimal number, for instance '13.0', '-9.159' or '42'.
-- python
### integer
-- description
Accepts integers only (float is not clipped but triggers an error), returns an integer.
-- description
syntax:
- integer() → Integer
- integer_items() → list
- integer_list() → list
-- eno
good: -13
bad: 0.3
-- eno
-- python
>>> document = Eno.parse(input)
>>> document.integer('good')
-13
>>> document.integer('bad')
ValidationError: 'bad' must contain an integer, for instance '42' or '-21'.
-- python
### json
-- description
Parses the value as JSON and returns the result (or passes on the parser error for malformed JSON).
-- description
syntax:
- json() → dict/list/value
- json_items() → list
- json_list() → list
-- eno
-- json payload
{
"status": 500
}
-- json payload
-- eno
-- python
>>> document = Eno.parse(input)
>>> document.json('json payload')
{ 'status': 500 }
-- python
### lat_lng
-- description
Accepts lat/lng coordinates of the form `dd.ddddd, dd.ddddd` (but with any number of decimal digits), returns a `dict` (see example below) (or raises an error for invalid values).
-- description
syntax:
- lat_lng() → dict
- lat_lng_items() → list
- lat_lng_list() → list
-- eno
location: 36.987094, -25.091719
-- eno
-- python
>>> document = Eno.parse(input)
>>> document.lat_lng('location')
{ lat: 36.987094, lng: -25.091719 }
-- python
### number
-- description
An alias for the `integer` loader.
-- description
syntax:
- number() → Integer
- number_items() → list
- number_list() → list
-- eno
good: -13
bad: 0.3
-- eno
-- python
>>> document = Eno.parse(input)
>>> document.number('good')
-13
>>> document.number('bad')
ValidationError: 'bad' must contain an integer, for instance '42' or '-21'.
-- python
### string
-- description
Technically not a loader, but an alias for the standard accessors (which always
return strings), you can use it if you prefer a 100% type-oriented value access
terminology in your code. Note that this is not even a noop function, but really
just a direct alias, so using it instead of e.g. `field` does not incur any
performance drawback.
-- description
syntax:
- string → string
- string_items → list
- string_list → list
-- eno
name: Alice
number: 13
-- eno
-- ruby
>>> document = parse(input)
>>> document.string('name')
'Alice'
>>> document.string('number')
'13'
# .... these are completely identical to ...
>>> document.field('name')
'Alice'
>>> document.field('number')
'13'
-- ruby
### url
-- description
Validates a URL and returns it unaltered (or raises an error for an invalid URL).
-- description
syntax:
- url() → string
- url_items() → list
- url_list() → list
-- eno
good: https://json.org
bad: www.boom
-- eno
-- python
>>> document = Eno.parse(input)
>>> document.url('good')
'https://json.org'
>>> document.url('bad')
ValidationError: 'bad' must contain valid URL, for instance 'https://eno-lang.org'.
-- python
## Section
-- class description
Every section in an eno document, such as for instance `# notes` or `### Appendix`, maps to a `Section`.
More prominently though **the document itself is represented as a `Section`**,
consequently this is the interface you'll be utilizing most for many usecases.
-- class description
### assert_all_touched
syntax:
- assert_all_touched
- assert_all_touched(options)
- assert_all_touched(message, options)
- assert_all_touched(message_function, options)
-- description
Assert that all elements of this section (and also, recursively, of all
subsections) that were present in the parsed eno document were also queried (and
therefore *touched*) by the application. This, combined with eno's query
methods, serves to ensure a two-way guarantee for both users and developers: No
data that the application requires can be left out, and no data that the
application does not process can be supplied.
-- description
-- eno
Important data A: I need to be processed!
Important data B: Me too!
-- eno
-- python
>>> document = enopy.parse(input)
>>> data_a = document.field('Important data A')
# ... processing happens only for data_a
>>> document.assert_all_touched() # raises an error with the default message
>>> document.assert_all_touched(only=['Important data A']) # passes
>>> document.assert_all_touched(skip=['Important data B']) # passes
>>> custom_message = lambda name, value: f"{name} is not supported by the application, please contact support if you think it should be"
>>> document.assert_all_touched(custom_message) # raises an error "Important data B is not ..."
-- python
#### parameters
-- message or message_function
Optional, usually the default message (*An excess element named [NAME] was
found, is it possibly a typo?*) will do fine. If you want to override it,
provide either a static message as a string, or alternatively a function
returning a string. (The arguments passed are `name` and `value`, although
`value` can be `None` if the untouched element is a fieldset or section)
-- message or message_function
options:
only = A list of strings, e.g. `['name', 'email']`, which specifies to only check these elements for whether they've been touched.
skip = A list of strings, e.g. `['phone number']`, which specifies to exclude these elements from checking whether they've been touched.
### fieldsets
syntax: fieldsets(name) → list
-- description
Retrieve fieldsets with the specified name from this current section.
-- description
-- eno
image:
src = red-roses.jpg
image:
src = white-roses.jpg
-- eno
-- python
>>> document = enopy.parse(input)
>>> images = document.fieldsets('image')
>>> for image in images:
... print(image.entry('src'))
-- python
#### parameters
name: A string specifying the name of the fieldsets to retrieve.
#### return value
description: A list of `Fieldset`s.
### fieldset
syntax:
- fieldset(name) → Fieldset or None
- fieldset(name, options) → Fieldset or None
-- description
Retrieve a fieldset from the section, optionally supplying an options.
-- description
-- eno
color ratings:
red = 5
green = 7
blue = 3
-- eno
-- python
>>> document = enopy.parse(input)
>>> document.fieldset('color ratings')
<class Fieldset name="color ratings" entries=3>
>>> document.fieldset('temperature ratings')
ValidationError: ...
>>> document.fieldset('temperature ratings', required=False)
None
>>> document.fieldset('temperature ratings', enforce_element=True)
ValidationError: ...
-- python
#### parameters
name: A string representing the name of the field to return.
options:
enforce_element = A boolean stating whether the fieldset must exist in the document. (defaults to `False`)
required = Alias for `enforce_element` (this exists on many methods and depending on context refers to either element or value)
#### return value
description: An `Fieldset`, or `None`.
### element
syntax:
- element(name) → element or None
- element(name, options) → element or None
-- description
Retrieve a single element of any allowed type from the section by its name.
Optionally specify whether it's optional or mandatory for the element to be
present in the eno document (by default it's optional).
-- description
-- eno
title: Artfest 2018
tags:
- art
- code
# content
> ...
-- eno
-- python
>>> document = enopy.parse(input)
>>> document.element('title')
<class Field name="title" value="Artfest 2018">
>>> document.element('tags')
<class List name="tags" items=2>
>>> document.element('content')
<class Section name="content" items=14>
>>> document.element('fantasy')
None
>>> document.element('fantasy', enforce_element=True)
ValidationError: ...
>>> document.element('fantasy', required=True)
ValidationError: ...
-- python
#### parameters
name: The name of the element to fetch from the section as a string.
options:
enforce_element = Whether the element must be present in the document (`True` or `False`, defaults to `False`)
required = Alias for `enforce_element` (this exists on many methods and depending on context refers to either element or value)
#### return value
description: An element (e.g. `List`, `Field`, etc.) or `None`.
### elements
syntax: elements() → list
-- description
Retrieve the elements of this section in sequential order. This seamlessly lets you switch from
associative readout to sequential readout, allowing sections within, or the whole document, to
represent document-like structures (think generic markup) instead of just associative data stores.
-- description
-- eno
date: 2018-03-01
title: My blog post
# page
h1: Hello world
p: This is my first post, I'm so happy!
-- eno
-- python
>>> document = enopy.parse(input)
>>> metadata = {
... 'date': document.field('date', my_date_loader_function),
... 'title': document.field('title')
... }
>>> html = ''
>>> for element in document.section('page').elements():
... html += f"<{element.name}>{element.value}</{element.name}>"
>>> with open('index.html', 'w') as file:
... file.write(html)
-- python
#### return value
description: A list containing the elements of this section.
### enforce_all_elements
syntax:
- enforce_all_elements
- enforce_all_elements(enforce)
-- description
Set the default for all following queries on this section of whether the
presence of elements in the eno input text should be enforced (by default it is
not). This can be used to prevent "template decay" - with presence enforcement
enabled elements may be empty, but they (at least their declaration) must be
there in the eno text and consequently they can not disappear from a template
over time without triggering an error.
-- description
-- eno
color: blue
-- eno
-- python
>>> document = enopy.parse(input)
>>> document.field('sound')
None
>>> document.enforce_all_elements()
>>> document.field('sound')
ValidationError: ...
>>> document.field('sound', enforce_element=False)
None
-- python
#### parameters
enforce: An optional boolean indicating whether to enforce or not. (otherwise `True` is assumed)
### field
syntax:
- field(name) → value or None
- field(name, options) → object/value or None
- field(name, loader, options) → value or None
-- description
Retrieve a field's value from the section, optionally supplying a loader to
validate and/or transform the value, and/or an options object.
-- description
-- eno
color: blue
sound:
-- eno
-- python
>>> document = enopy.parse(input)
>>> document.field('color')
'blue'
>>> document.field('sound')
None
>>> document.field('sound', required=True)
ValidationError: ...
>>> document.field('sound', enforce_value=True)
ValidationError: ...
>>> document.field('sound', enforce_element=True)
None
>>> document.field('texture', enforce_element=True)
ValidationError: ...
>>> document.field('color', lambda name, value: value.upper())
'BLUE'
>>> def color_validator(name, value):
... if value != 'green':
... raise ValueError('Only green is allowed!')
... return value
>>> document.field('color', color_validator)
ValidationError: Only green is allowed!
>>> document.field('color', with_element=True)
[ <class Field name="color" value="blue">, 'blue' ]
-- python
#### parameters
name: A string representing the name of the field to return.
loader: A function returning the transformed/validated value or raising an error.
options:
enforce_element = Whether the field must be present in the document (`True` or `False`, defaults to `False`)
enforce_value = Whether there must be a value or the field is allowed to be empty (defaults to `False`)
required = Alias for `enforce_value` (this exists on many methods and depending on context refers to either element or value)
with_element = Whether to return a list of the form `[element, value]` instead of just the value (default `False`)
#### return value
description:
| The value of the field, or `None` if empty, or a list of the form `[element, value]`
| when the option `with_element` is specified.
### fields
syntax: fields(name) → list
-- description
Retrieve fields with the specified name from this current section.
-- description
-- eno
color: blue
color: red
color: orange
-- eno
-- python
>>> document = enopy.parse(input)
>>> colors = document.fields('color')
>>> for color in colors:
... print(color.value())
-- python
#### parameters
name: A string specifying the name of the fields to retrieve.
#### return value
description: A list of `Field`s.
### list
syntax:
- list(name) → list
- list(name, options) → list
- list(name, loader, options) → list
-- description
Retrieve the items of a list, optionally supplying a loader and options.
-- description
-- eno
colors:
- pink
- peach
textures:
-
-- eno
-- python
>>> document = enopy.parse(input)
>>> document.list('colors')
[ 'pink', 'peach' ]
>>> document.list('sounds')
[]
>>> document.list('textures')
ValidationError: ...
>>> document.list('textures', enforce_values=False)
[ None ]
>>> document.list('sounds', required=True)
ValidationError: ...
>>> document.list('sounds', enforce_element=True)
ValidationError: ...
>>> document.list('colors', lambda name, value: value.upper())
[ 'PINK', 'PEACH' ]
>>> def color_validator(name, value):
... if value == 'peach':
... raise ValueError('Peach may not be on the list!')
... return f"I like {value}"
>>> document.list('colors', color_validator)
ValidationError: Peach may not be on the list!
>>> document.list('colors', with_elements=True)
[[ <class Field value="pink">, 'pink' ],
[ <class Field value="peach">, 'peach' ]]
>>> document.list('colors', min_count=3)
ValidationError: ...
-- python
#### parameters
name: The name of the list to return as a string.
loader:
| A function returning the transformed/validated value or raising an error.
| (The function is applied to each list item on its own, and for the parameter
| signature you can choose between just `value` as single parameter or `name`, `value`
| - in that order - if you need both)
options:
enforce_element = Whether the list must be present in the document (defaults to `False`)
enforce_values = Whether empty list items (`- ` in the document, mapping to `None`) are disallowed (defaults to `True`)
exact_count = Validate that there are exactly `n` items in the list (takes a number, defaults to `None`)
min_count = Validate that there are at least `n` items in the list (takes a number, defaults to `None`)
max_count = Validate that there are at most `n` items in the list (takes a number, defaults to `None`)
required = Alias for `enforce_element` (this exists on many methods and depending on context refers to either element or value)
with_elements = Whether to return a list of lists of the form `[element, value]` instead of just the values (defaults to `False`)
#### return value
description:
| The (optionally transformed/validated) values of the items of this list as a list.
| With `with_elements` set to `True` it returns a list of lists of the form `[element, value]` instead.
### lists
syntax: lists(name) → list
-- description
Retrieve lists with the specified name from this current section.
-- description
-- eno
route:
- vienna
- paris
- rome
route:
- moscow
- riga
-- eno
-- python
>>>document = enopy.parse(input)
>>>routes = document.lists('route')
>>> for route in routes:
... print(route.items())
[ 'vienna', 'paris', 'rome']
[ 'moscow', 'riga' ]
-- python
#### parameters
name: A string specifying the name of the lists to retrieve.
#### return value
description: A list of `List`s.
### lookup
syntax:
- lookup(index) → Hash or None
- lookup(line, column) → Hash or None
-- description
Ask the document *Hey what's at column X in line Y in my eno file?*. The lookup **always** returns an element for valid indices
(the document/section in case of an empty line/space), only indices outside the range of the document return `None`. Note that
all arguments are zero-indexed, i.e. the first lines and columns are numbered `0`, not `1`.
-- description
-- eno
color: blue
# notes
-- eno
-- python
>>> document = enopy.parse(input)
>>> document.lookup(3) # 'o'
{ 'element': <class Field name="color" value="blue">, 'zone': 'name' }
>>> document.lookup(7) # 'b'
{ 'element': <class Field name="color" value="blue">, 'zone': 'value' }
>>> document.lookup(0, 3) # 'o'
{ 'element': <class Field name="color" value="blue">, 'zone': 'name' }
>>> document.lookup(0, 7) # 'b'
{ 'element': <class Field name="color" value="blue">, 'zone': 'value' }
>>> document.lookup(13) # '#'
{ 'element': <class Section name="notes" elements=0>, 'zone': 'section_operator' }
>>> document.lookup(19) # 's'
{ 'element': <class Section name="notes" elements=0>, 'zone': 'name' }
>>> document.lookup(2, 0) # '#'
{ 'element': <class Section name="notes" elements=0>, 'zone': 'section_operator' }
>>> document.lookup(2, 6) # 's'
{ 'element': <class Section name="notes" elements=0>, 'zone': 'name' }
-- python
#### parameters
index: A one-dimensional index to look up (0-indexed, i.e. the first position is 0, not 1) - only applies when it's the single only given argument.
line: The line to look up (0-indexed, i.e. the first line is 0, not 1) - only applies when you supply a `column` as well.
column: The column to look up (0-indexed, i.e. the first column is 0, not 1) - only applies when you supply a `line` as well.
#### return value
-- description
For all valid indices within the document returns a Hash with two keys:
`element`: An `Section`, `Fieldset`, `Field`.. . For empty lines you get the containing section.
`zone`: A string denoting the token at the specified position, here's the full list:
`'[empty-lines-and-spaces-between-tokens]'` => `element`
`'[inside-blocks]'` => `content`
`'[inside-comments]'` => `comment`
`'[name]'` => `name`
`'[template]'` => `value`
`'[value]'` => `value`
`':'` => `name_operator`
`'-'` => `item_operator`
`'='` => `entry_operator`
`'--'` => `block_operator`
`'>'` => `comment_operator`
``'`'`` => `escape_begin_operator`
``'`'`` => `escape_end_operator`
`'|'` => `newline_continuation_operator`
`'\'` => `line_continuation_operator`
`'#'` => `section_operator`
`'<'` => `copy_operator`
`'<<'` => `deep_copy_operator`
When an index outside the document is supplied, `None` is returned.
-- description
### raw
syntax: raw → list
-- description
Retrieve a python native representation of the section.
-- description
-- eno
color: blue
-- eno
-- python
>>> document = enopy.parse(input)
>>> document.raw()
[{ 'color': 'blue' }]
-- python
#### return value
description: A list of elements (also returned in their raw representation).
### section
syntax:
- section(name) → Section or None
- section(name, options) → Section or None
-- description
Retrieve the subsection with the specified name from this current section.
-- description
-- eno
# content
title: A Manifest
# notes
-- eno
-- python
>>> document = enopy.parse(input)
>>> document.section('content')
<class Section name="content" elements=1>
>>> document.section('notes')
<class Section name="notes" elements=0>
>>> document.section('metadata')
ValidationError: ...
>>> document.section('metadata', required=False)
None
>>> document.section('notes', enforce_element=True)
<class Section name="notes" elements=0>
>>> document.section('metadata', enforce_element=True)
ValidationError: ...
-- python
#### parameters
name: A string specifying the name of the section to retrieve.
options:
enforce_element = Whether the section must be present in the document (defaults to `False`)
required = Alias for `enforce_element` (this exists on many methods and depending on context refers to either element or value)
#### return value
description: An `Section`, or `None`.
### sections
syntax: sections(name) → list
-- description
Retrieve subsections with the specified name from this current section.
-- description
-- eno
# Article
> ...
# Article
> ...
-- eno
-- python
>>> document = enopy.parse(input)
>>> sections = document.sections('Article')
>>> for section in sections:
... # do something with each Article
-- python
#### parameters
name: A string specifying the name of the sections to retrieve.
#### return value
description: A list of `Section`s.
## Field
-- class description
All values in an eno document, such as the value of a field like `name: value`,
the value of a block, the value of a list item, the value of a fieldset
entry, and so on, map to a `Field`. Usually you don't interact with this
because you directly get the values from a section or fieldset in most cases.
When you sequentially iterate a section (with `Section`'s
'`#elements` method) you will get to interact with this class.
-- class description
### is_empty
syntax: is_empty() → boolean
-- description
Query whether the value is empty (e.g. `comment: ` in an eno document), which in python terms is equivalent of `None`.
-- description
#### return value
description: `True` if empty, otherwise `False`.
### error
syntax:
- error → ValidationError
- error(message) → ValidationError
- error(message_function) → ValidationError
-- description
Generate an error in the context of the element. The error includes a generic
message by default, or a custom one if you supply it (which is the recommended
practice). You can also pass a message function which (like loaders
too) gets the name and value as arguments and returns a message string. This
serves to create highly informative error messages that pin-point to the exact
origin of the error, even when the initial reading of data is already past, e.g.
when the error condition is only apparent later, when more processing has
occurred or other data is available.
-- description
-- eno
color: cozy
-- eno
-- python
>>> document = enopy.parse(input)
>>> element, value = document.field('color', with_element=True)
# ...
>>> if value == 'cozy'
... raise element.error('Postprocessing determined that "cozy" is not a color after all.')
ValidationError: Postprocessing determined that "cozy" is not a color after all.
-- python
#### parameters
message or message_function:
| Highly recommended to provide one (but it's optional).
|
| Either directly pass a string, or alternatively a function returning a string.
| (The function is passed `name` and `value` as arguments.)
#### return value
description:
| A `ValidationError` in the context of the element's value
| (and with an optional custom message).
### raw
syntax: raw → object or value
-- description
Retrieve a native object representation of the value.
The representation differs depending on whether there is a name (e.g. for a field value),
or not (applies only to list item values).
-- description
-- eno
color: blue
numbers:
- 13
- 42
-- eno
-- python
>>> document = enopy.parse(input)
>>> document.element('color').raw()
{ 'color': 'blue' }
>>> list_items = document.element('numbers').elements()
>>> list_items.first.raw()
'13'
-- python
#### return value
description: A native representation of the value element.
### value
syntax:
- value → value or None
- value(options) → value or None
- value(loader, options) → value or None
-- description
Retrieve the value of a `Field`, optionally passing it through a loader and/or supplying options.
-- description
-- eno
flag color: beige
towel color:
|
-- eno
-- python
>>> document = enopy.parse(input)
>>> flag_color = document.element('flag color')
>>> towel_color = document.element('towel color')
>>> flag_color.value()
'beige'
>>> towel_color.value()
None
>>> flag_color.value(lambda name, value: value.replace('eig', 'lu'))
'blue'
-- python
#### parameters
loader:
| A function returning the transformed/validated value or raising an error.
| The function is passed either `name` and `value`, in that order, or just
| `value` if your loader only takes a single argument.
options:
enforce_value = Whether there must be a value or the field is allowed to be empty (default to `False`)
required = Alias for `enforce_value` (this exists on many methods and depending on context refers to either element or value)
#### return value
description: The (optionally transformed/validated) value of this `Field`.
## List
-- class description
Lists such as the one below are represented as an `List`:
```eno
things:
- toys
- plants
```
Like `Field`, you will seldom interact with this class, and instead use the `list`
method on the document or its sections to directly obtain the values of a list.
-- class description
### elements
syntax: elements() → list
-- description
Retrieve the items of this list as `Field` elements (instead of
their direct values) in sequential order.
-- description
-- eno
numbers:
- 6
- 8
- 19
-- eno
-- python
>>> list = enopy.parse(input).element('numbers')
>>> list.elements()
[ <class Field value="6">, <class Field value="8">, <class Field value="19"> ]
-- python
#### return value
description: A list containing the items of this list as `Field` elements.
### items
syntax:
- items → list
- items(options) → list
- items(loader, options) → list
-- description
Retrieve the items of the list, optionally passing them through a loader.
-- description
-- eno
colors:
- pink
- peach
-- eno
-- python
>>> document = enopy.parse(input)
>>> list = document.element('colors')
>>> list.items()
['pink', 'peach']
>>> list.items(lambda name, value: f"{value}!!")
['pink!!', 'peach!!']
-- python
#### parameters
loader:
| A function returning the transformed/validated value or raising an error.
| (The function is applied to each list item on its own, being passed the
| arguments `name` and `value`, in that order, or just `value` if your loader
| only takes a single parameter.
options:
elements = Whether to return the elements (as `Field`) instead of the values of the list items. (defaults to `False`)
enforce_values = Whether empty list items (`- ` in the document, mapping to `None`) are disallowed (defaults to `True`)
with_elements = Whether to return a list of lists of the form `[element, value]` instead of just the values (defaults to `False`)
#### return value
description: The (optionally transformed/validated) items of this list as a list.
### length
syntax: length → number
-- description
Returns the count of items in the list.
-- description
-- eno
colors:
- pink
- peach
-- eno
-- python
>>> list = enopy.parse(input).element('colors')
>>> list.length()
2
-- python
#### return value
description: The number of items in the list as a number.
### raw
syntax: raw → object
-- description
Retrieve a native representation of the list.
-- description
-- eno
colors:
- pink
- peach
-- eno
-- python
>>> list = enopy.parse(input).element('colors')
>>> list.first.raw()
{ 'colors': ['pink', 'peach'] }
-- python
#### return value
description: A native representation of the list.
## Fieldset
-- class description
Fieldsets are represented as `Fieldset` instances:
```eno
rated things:
toys = 5 stars
plants = 3 stars
```
You will mostly obtain an instance of this through the `#fieldset` method on a document/section.
-- class description
### assert_all_touched
syntax:
- assert_all_touched
- assert_all_touched(options)
- assert_all_touched(message, options)
- assert_all_touched(message_function, options)
-- description
Assert that all entries of this fieldset that were present in the parsed eno document
were also queried (and therefore *touched*) by the application. This, combined with eno's
query methods, serves to ensure a two-way guarantee for both users and developers:
No data that the application requires can be left out, and no data that the application
does not process can be supplied.
-- description
-- eno
Important data:
A = I need to be processed!
B = Me too!
-- eno
-- python
>>> document = enopy.parse(input)
>>> fieldset = document.fieldset('Important data')
>>> data_a = fieldset.entry('A')
# ... processing happens only for data_a
>>> fieldset.assert_all_touched()
ValidationError: An excess element named 'B' was found, is it possibly a typo?
>>> fieldset.assert_all_touched(only=['A']) # passes
>>> fieldset.assert_all_touched(skip=['B']) # passes
>>> custom_message = lambda name, value: f"{name} is not supported by the application, please contact support if you think it should be"
>>> fieldset.assert_all_touched(custom_message)
ValidationError: B is not supported by the application, please contact support if you think it should be
-- python
#### parameters
-- message or message_function
Optional, usually the default message (*An excess element named [NAME] was
found, is it possibly a typo?*) will do fine. If you want to override it,
provide either a static message as a string, or alternatively a function
returning a string. (`name` and `value` are provided as arguments)
-- message or message_function
options:
only = A list of strings, e.g. `['name', 'email']`, which specifies to only check these elements for whether they've been touched.
skip = A list of strings, e.g. `['phone number']`, which specifies to exclude these elements from checking whether they've been touched.
### element
syntax:
- element(name) → element or None
- element(name, options) → element or None
-- description
Retrieve a single entry as `Field` (instead of
its direct value) from the fieldset by its name.
Optionally specify whether it's optional or mandatory for the entry to be
present in the eno document (by default it's optional).
-- description
-- eno
years:
2007 = Year of the gooseberry
2010 = Year of the awesome beanstalk
2011 = Year of yearning
-- eno
-- python
>>> fieldset = enopy.parse(input).fieldset('years')
>>> fieldset.element('2010')
<class Field name="2010" value="Year of the awesome beanstalk">
>>> fieldset.element('2020')
None
>>> fieldset.element('2020', enforce_element=True)
ValidationError: ...
>>> fieldset.element('2020', required=True)
ValidationError: ...
-- python
#### parameters
name: The name of the entry to fetch from the fieldset as a string.
options:
enforce_element = Whether the element must be present in the document (`True` or `False`, defaults to `False`)
required = Alias for `enforce_element` (this exists on many methods and depending on context refers to either element or value)
#### return value
description: An element (`Field`) or `None`.
### elements
syntax: elements() → list
-- description
Retrieve the entries of this fieldset as `Field` elements (instead of
their direct values) in sequential order. This seamlessly lets you switch from
associative readout to sequential readout, allowing fieldsets to also represent
document-like structures instead of just associative data.
-- description
-- eno
dialogue:
alice = hi
bob = hey
alice = sup?
bob = nothin'
-- eno
-- python
>>> document = enopy.parse(input)
>>> dialogue = document.fieldset('dialogue')
>>> html = '<h1>Observe this fascinating dialogue, presented in html!</h1>'
>>> for element in dialogue.elements():
... html += f"<strong>{element.name}</strong>: \"{element.value}\""
>>> with open('index.html', 'w') as file:
... file.write(html)
-- python
#### return value
description: A list containing the elements of this fieldset.
### enforce_all_elements
syntax:
- enforce_all_elements
- enforce_all_elements(enforce)
-- description
Set the default for all following queries on this fieldset of whether the
presence of elements in the eno input text should be enforced (by default it is
not). This can be used to prevent "template decay" - with presence enforcement
enabled entries may be empty, but they (at least their declaration) must be
there in the eno document and consequently they can not disappear from a
template at any point without triggering an error.
-- description
-- eno
conversions:
0001 = 1
0010 = 2
-- eno
-- python
>>> document = enopy.parse(input)
>>> conversions = document.fieldset('conversions')
>>> conversions.enforce_all_elements()
>>> conversions.entry('0011')
ValidationError: ...
>>> conversions.entry('0011', enforce_element=False)
None
-- python
### entry
syntax:
- entry(name) → value or None
- entry(name, loader) → value or None
- entry(name, enforce_element: False, required: False, with_element: False) → object/value or None
-- description
Retrieve an entry's value from the fieldset, optionally supplying a loader to
validate and/or transform the value, and/or an options object.
-- description
-- eno
Q&A:
Meaning = 42
Green = Yes
Purpose =
-- eno
-- python
>>> document = enopy.parse(input)
>>> qa = document.fieldset('Q&A')
>>> qa.entry('Meaning')
'42'
>>> qa.entry('Purpose')
None
>>> qa.entry('Purpose', required=True)
ValidationError: ...
>>> qa.entry('Purpose', enforce_element=True)
None
>>> qa.entry('Beige', enforce_element=True)
ValidationError: ...
>>> qa.entry('Green', lambda name, value: value.upper())
'YES'
>>> def check_meaning(name, value):
... if value == '42':
... raise ValueError("That one's getting old!")
... return value
>>> qa.entry('Meaning', check_meaning)
ValidationError: That one's getting old!
>>> qa.entry('Meaning', with_element=True)
[ <class Field name="Meaning" value="42"> , '42' ]
-- python
#### parameters
name: The name of the entry as a string.
loader:
| A function returning the transformed/validated value or raising an error.
| The function is passed `name` and `value`, in that order, or just `value`
| if your function only takes a single parameter.
options:
enforce_element = Whether the entry must be present in the document (`True` or `False`, defaults to `False`)
enforce_value = Whether there must be a value or the entry is allowed to be empty (default to `False`)
required = Alias for `enforce_value` (this exists on many methods and depending on context refers to either element or value)
with_element = Whether to return a list with both the element and the value (defaults to `False`)
#### return value
description: The entry's value, or `None` if empty.
### raw
syntax: raw → object
-- description
Retrieve a native representation of the fieldset.
-- description
-- eno
weights:
apple = 100g
pineapple = 800g
-- eno
-- python
>>> document = enopy.parse(input)
>>> document.fieldset('weights').raw()
{ 'weights': [{ 'apple': '100g' }, { 'pineapple': '800g' }] }
-- python
#### return value
description: A native object representation of the fieldset.
## Empty
-- class description
This represents empty elements such as `color:`, where it is not clear if it is an empty field, list, or fieldset.
-- class description
### error
syntax:
- error → ValidationError
- error(message) → ValidationError
- error(message_function) → ValidationError
-- description
Generate an error in the context of the element. The error includes a generic
message by default, or a custom one if you supply it (which is the recommended
practice). You can also pass a message function which (like loaders too)
gets the name and value as arguments and returns a message string. This serves
to create highly informative error messages that pin-point to the exact origin
of the error, even when the initial reading of data is already past, e.g. when
the error condition is only apparent later, when more processing has occurred or
other data is available.
-- description
-- eno
color:
-- eno
-- python
>>> document = enopy.parse(input)
>>> element, value = document.field('color', with_element=True)
# ...
>>> if value in None:
... raise element.error('Postprocessing determined that this value needs to be provided.')
ValidationError: Postprocessing determined that this value needs to be provided.
-- python
#### parameters
message or message_function:
| Highly recommended to provide one (but it's optional).
|
| Either directly pass a string, or alternatively a function returning a string.
| (The function is passed `name` and `value` as arguments)
#### return value
description:
| A `ValidationError` in the context of the element's value
| (and with an optional custom message).
### raw
syntax: raw → object
-- description
Retrieve a native representation of the empty element.
-- description
-- eno
color:
-- eno
-- python
>>> empty = enopy.parse(input).element('color')
>>> empty.raw()
{ 'color': None }
-- python
#### return value
description: A native representation of the empty element.
### value
syntax: value → None
-- description
Retrieve the value (always returns `None`).
-- description
-- eno
note:
-- eno
-- python
>>> empty = enopy.parse(input).element('note')
>>> empty.value()
None
-- python
#### return value
description: Always `None`.
## Error
-- class description
The single generic error interface for all (user) errors that eno generates.
Note that this is never raised by itself, but only in one of it's subclassed
variants (`ParseError` and `ValidationError`). However, you can still
utilize this generic class in cases where you want to catch both parser and
validation errors indiscriminately, like so:
```python
>>> try:
... # ...
... except Error as e: # catches both ParseError and ValidationError
... # ...
```
-- class description
### cursor
syntax: cursor → [line, column]
-- description
Returns a cursor position as a list of the form [line, column], indicating
where a cursor should be placed if an application wants to offer the user a way
to move the cursor straight to the error location.
-- description
-- python
>>> try:
... # ...
... except Error as e:
... return e.cursor
[3, 14]
-- python
#### return value
description: A list, where [0] is the line number, and [1] is the column number.
### selection
syntax: selection → [[line, column], [line, column]]
-- description
Returns a selection as a list of the form [[line, column], [line, column]],
indicating a text range to select if an application wants to offer the user a
way to get a selection for the error in the input.
-- description
-- python
>>> try:
... # ...
... except Error as e:
... return e.selection
[[3, 14], [3, 23]]
-- python
#### return value
description:
| A list, where [0] is the begin of the selection and [1] is the end,
| and both begin and end are each again a list, where [0] is the
| line number, and [1] is the column number.
### message
syntax: message → string
-- description
Contains both the error text as well as the snippet. This is also what you
get in the console when you don't catch the error.
-- description
-- python
>>> try:
... # ...
... except Error as e:
... return e.message
"In line 4 'a' is copied into itself.\n\n Line | Content\n ..."
-- python
#### return value
description: Both the error text as well as the snippet.
### snippet
syntax: snippet → string
-- description
Returns a formatted excerpt of those passage(s) from the input where the error occurred.
-- description
-- python
>>> try:
... # ...
... except Error as e:
... return e.snippet
Line | Content
1 |
* 2 | # a
* 3 | ## b
> 4 | ### c < a
5 |
-- python
#### return value
description: A formatted excerpt of those passage(s) from the input where the error occurred.
### text
syntax: text → string
-- description
Returns a one-liner that describes the error in human language.
-- description
-- python
>>> try:
... # ...
... except Error as e:
... return e.message
"In line 4 'a' is copied into itself."
-- python
#### return value
description: A single sentence that describes the error in human language.
## ParseError
-- class description
When this is raised, it indicates an error regarding syntax or grammatical semantics of the document.
Functionally this behaves exactly like `Error`, therefore the interface methods are not repeated here and can
be looked up on the `Error` documentation.
-- class description
## ValidationError
-- class description
When this is raised, it indicates an error regarding application-specific requirements for the document.
Functionally this behaves exactly like `Error`, therefore the interface methods are not repeated here and can
be looked up on the `Error` documentation.
-- class description