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 root`Value` 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 `Record`s 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 `Value`s
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 value = strip(annotatedValue),"
    "                  forward = value,"
    "                  back = value,"
    "except where test-case-specific values of `forward` and/or `back`"
    "are provided by the executing harness, and check the following"
    "numbered expectations according to the table above:"
    "Implementations may vary in their treatment of the difference between expectations"
    "13/14 and 16/17, 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 `Record`s anywhere inside `testdata` that have either `Test` or `NondeterministicTest` as their labels.

3. `[. 1 rec]` filters these `Record`s by another path expression:

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

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 #[tLWzBnRpdGxlZLMGcGVyc29ukrMFdGhpbmeRhKBlsQlCbGFja3dlbGy0swRkYXRloQcdkpOEsQJEcoQ=] <[titled person 2 thing 1] 101 "Blackwell" <date 1821 2 3> "Dr">>
    <Test #[tLMHZGlzY2FyZIQ=] <discard>>
    <Test #[tJe1hIQ=] <7 []>>
    <Test #[tLMHZGlzY2FyZLMIc3VycHJpc2WE] <discard surprise>>
    <Test #[tLEHYVN0cmluZ5OUhA==] <"aString" 3 4>>
    <Test #[tLSzB2Rpc2NhcmSEk5SE] <<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 `Step`s.

## `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`

    521
    522
    523
    524
    525
    526
    527
    528
    529
    530
    531
    532
    533
    534
    @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`

    129
    130
    131
    132
    133
    134
    135
    136
    137
    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))

Last update: March 16, 2023
Created: March 16, 2023