Skip to content

Preserves Path

The preserves.path module implements Preserves Path.

Preserves Path is roughly analogous to XPath, but for Preserves values: just as XPath selects portions of an XML document, a Preserves Path uses path expressions to select portions of a Value.

Use parse to compile a path expression, and then use the exec method on the result to apply it to a given input:

parse(PATH_EXPRESSION_STRING).exec(PRESERVES_VALUE)
    -> SEQUENCE_OF_PRESERVES_VALUES

Command-line usage

When preserves.path is run as a __main__ module, sys.argv[1] is parsed, interpreted as a path expression, and run against human-readable values read from standard input. Each matching result is passed to stringify and printed to standard output.

Examples

Setup: Loading test data

The following examples use testdata:

>>> with open('tests/samples.bin', 'rb') as f:
...     testdata = decode_with_annotations(f.read())

Recall that samples.bin contains a binary-syntax form of the human-readable [samples.pr](https://preserves.dev/tests/samples.pr) test data file, intended to exercise most of the features of Preserves. In particular, the rootValue` in the file has a number of annotations (for documentation and other purposes).

Example 1: Selecting string-valued documentation annotations

The path expression .annotations ^ Documentation . 0 / string proceeds in five steps:

  1. .annotations selects each annotation on the root document
  2. ^ Documentation retains only those values (each an annotation of the root) that are Records with label equal to the symbol Documentation
  3. . 0 moves into the first child (the first field) of each such Record, which in our case is a list of other Values
  4. / selects all immediate children of these lists
  5. string retains only those values that are strings

The result of evaluating it on testdata is as follows:

>>> selector = parse('.annotations ^ Documentation . 0 / string')
>>> for result in selector.exec(testdata):
...     print(stringify(result))
"Individual test cases may be any of the following record types:"
"In each test, let stripped = strip(annotatedValue),"
"                  encodeBinary(·) produce canonical ordering and no annotations,"
"                  looseEncodeBinary(·) produce any ordering, but with annotations,"
"                  annotatedBinary(·) produce canonical ordering, but with annotations,"
"                  decodeBinary(·) include annotations,"
"                  encodeText(·) include annotations,"
"                  decodeText(·) include annotations,"
"and check the following numbered expectations according to the table above:"
"Implementations may vary in their treatment of the difference between expectations"
"21/22 and 31/32, depending on how they wish to treat end-of-stream conditions."

Example 2: Selecting tests with Records as their annotatedValues

The path expression // [.^ [= Test + = NondeterministicTest]] [. 1 rec] proceeds in three steps:

  1. // recursively decomposes the input, yielding all direct and indirect descendants of each input value

  2. [.^ [= Test + = NondeterministicTest]] retains only those inputs (each a descendant of the root) that yield more than zero results when executed against the expression within the brackets:

    1. .^ selects only labels of values that are Records, filtering by type and transforming in a single step
    2. [= Test + = NondeterministicTest] again filters by a path expression:
      1. the infix + operator takes the union of matches of its arguments
      2. the left-hand argument, = Test selects values (remember, record labels) equal to the symbol Test
      3. the right-hand argument = NondeterministicTest selects values equal to NondeterministicTest

    The result is thus all Records anywhere inside testdata that have either Test or NondeterministicTest as their labels.

  3. [. 1 rec] filters these Records by another path expression:

    1. . 1 selects their second field (fields are numbered from 0)
    2. rec retains only values that are Records

Evaluating the expression against testdata yields the following:

>>> selector = parse('// [.^ [= Test + = NondeterministicTest]] [. 1 rec]')
>>> for result in selector.exec(testdata):
...     print(stringify(result))
<Test #[tLMHY2FwdHVyZbSzB2Rpc2NhcmSEhA==] <capture <discard>>>
<Test #[tLMHb2JzZXJ2ZbSzBXNwZWFrtLMHZGlzY2FyZIS0swdjYXB0dXJltLMHZGlzY2FyZISEhIQ=] <observe <speak <discard> <capture <discard>>>>>
<Test #[tLWzBnRpdGxlZLMGcGVyc29usAECswV0aGluZ7ABAYSwAWWxCUJsYWNrd2VsbLSzBGRhdGWwAgcdsAECsAEDhLECRHKE] <[titled person 2 thing 1] 101 "Blackwell" <date 1821 2 3> "Dr">>
<Test #[tLMHZGlzY2FyZIQ=] <discard>>
<Test #[tLABB7WEhA==] <7 []>>
<Test #[tLMHZGlzY2FyZLMIc3VycHJpc2WE] <discard surprise>>
<Test #[tLEHYVN0cmluZ7ABA7ABBIQ=] <"aString" 3 4>>
<Test #[tLSzB2Rpc2NhcmSEsAEDsAEEhA==] <<discard> 3 4>>
<Test #[hbMCYXK0swFShbMCYWazAWaE] @ar <R @af f>>
<Test #[tIWzAmFyswFShbMCYWazAWaE] <@ar R @af f>>

Predicate = syntax.Predicate module-attribute

Schema definition for representing a Preserves Path Predicate.

Selector = syntax.Selector module-attribute

Schema definition for representing a sequence of Preserves Path Steps.

syntax = load_schema_file(pathlib.Path(__file__).parent / 'path.prb').path module-attribute

This value is a Python representation of a Preserves Schema definition for the Preserves Path expression language. The language is defined in the file path.prs.

exec(self, v)

WARNING: This is not a function: it is a method on Selector, Predicate, and so on.

>>> sel = parse('/ [.length gt 1]')
>>> sel.exec(['', 'a', 'ab', 'abc', 'abcd', 'bcd', 'cd', 'd', ''])
('ab', 'abc', 'abcd', 'bcd', 'cd')
Source code in preserves/path.py
513
514
515
516
517
518
519
520
521
522
523
524
525
526
@extend(syntax.Function)
def exec(self, v):
    """WARNING: This is not a *function*: it is a *method* on
    [Selector][preserves.path.Selector], [Predicate][preserves.path.Predicate], and so on.

    ```python
    >>> sel = parse('/ [.length gt 1]')
    >>> sel.exec(['', 'a', 'ab', 'abc', 'abcd', 'bcd', 'cd', 'd', ''])
    ('ab', 'abc', 'abcd', 'bcd', 'cd')

    ```

    """
    return (len(self.selector.exec(v)),)

parse(s)

Parse s as a Preserves Path path expression, yielding a Selector object. Selectors (and Predicates etc.) have an exec method defined on them.

Raises ValueError if s is not a valid path expression.

Source code in preserves/path.py
131
132
133
134
135
136
137
138
139
def parse(s):
    """Parse `s` as a Preserves Path path expression, yielding a
    [Selector][preserves.path.Selector] object. Selectors (and Predicates etc.) have an
    [exec][preserves.path.exec] method defined on them.

    Raises `ValueError` if `s` is not a valid path expression.

    """
    return parse_selector(Parser(s))