Skip to content

# Representations of Values

Python's strings, byte strings, integers, booleans, and double-precision floats stand directly for their Preserves counterparts. Wrapper objects for Float and Symbol complete the suite of atomic types.

Python's lists and tuples correspond to Preserves `Sequence`s, and dicts and sets to `Dictionary` and `Set` values, respectively. Preserves `Record`s are represented by Record objects. Finally, embedded values are represented by Embedded objects.

The preserves.values module implements the core representations of Preserves `Value`s as Python values.

## `Annotated(item)`

Bases: `object`

A Preserves `Value` along with a sequence of `Value`s annotating it. Compares equal to the underlying `Value`, ignoring the annotations. See the specification document for more about annotations.

    >>> import preserves
    >>> a = preserves.parse('''
    ... ; A comment
    ... [1 2 3]
    ... ''', include_annotations=True)
    >>> a
    @' A comment' (1, 2, 3)
    >>> a.item
    (1, 2, 3)
    >>> a.annotations
    [' A comment']
    >>> a == (1, 2, 3)
    True
    >>> a == preserves.parse('@xyz [1 2 3]', include_annotations=True)
    True
    >>> a[0]
    Traceback (most recent call last):
      ...
    TypeError: 'Annotated' object is not subscriptable
    >>> a.item[0]
    1
    >>> type(a.item[0])
    <class 'preserves.values.Annotated'>
    >>> a.item[0].annotations
    []
    >>> print(preserves.stringify(a))
    @" A comment" [1 2 3]
    >>> print(preserves.stringify(a, include_annotations=False))
    [1 2 3]

Attributes:

Name | Type | Description
--- | --- | ---
`item` | `Value` | the underlying annotated `Value`
`annotations` | `list[Value]` | the annotations attached to `self.item`

Source code in `preserves/values.py`

    573
    574
    575

    def __init__(self, item):
        self.annotations = []
        self.item = item

### `peel()`

Calls strip_annotations on `self` with `depth=1`.

Source code in `preserves/values.py`

    596
    597
    598

    def peel(self):
        """Calls [strip_annotations][preserves.values.strip_annotations] on `self` with `depth=1`."""
        return strip_annotations(self, 1)

### `strip(depth=inf)`

Calls strip_annotations on `self` and `depth`.

Source code in `preserves/values.py`

    592
    593
    594

    def strip(self, depth=inf):
        """Calls [strip_annotations][preserves.values.strip_annotations] on `self` and `depth`."""
        return strip_annotations(self, depth)

## `Embedded(embeddedValue)`

Representation of a Preserves `Embedded` value. For more on the meaning and use of embedded values, see the specification.

    >>> import io
    >>> e = Embedded(io.StringIO('some text'))
    >>> e                                        # doctest: +ELLIPSIS
    #!<_io.StringIO object at ...>
    >>> e.embeddedValue                          # doctest: +ELLIPSIS
    <_io.StringIO object at ...>

    >>> import preserves
    >>> print(preserves.stringify(Embedded(None)))
    Traceback (most recent call last):
      ...
    TypeError: Cannot preserves-format: None
    >>> print(preserves.stringify(Embedded(None), format_embedded=lambda x: 'abcdef'))
    #!"abcdef"

Attributes:

Name | Type | Description
--- | --- | ---
`embeddedValue` |  | any Python value; could be a platform object, could be a representation of a Preserves `Value`, could be `None`, could be anything!

Source code in `preserves/values.py`

    718
    719

    def __init__(self, embeddedValue):
        self.embeddedValue = embeddedValue

## `Float(value)`

Bases: `object`

Wrapper for treating a Python double-precision floating-point value as a single-precision (32-bit) float, from Preserves' perspective. (Python lacks native single-precision floating point support.)

    >>> Float(3.45)
    Float(3.45)
    >>> import preserves
    >>> preserves.stringify(Float(3.45))
    '3.45f'
    >>> preserves.stringify(3.45)
    '3.45'
    >>> preserves.parse('3.45f')
    Float(3.45)
    >>> preserves.parse('3.45')
    3.45
    >>> preserves.encode(Float(3.45))
    b'\x82@\\\xcc\xcd'
    >>> preserves.encode(3.45)
    b'\x83@\x0b\x99\x99\x99\x99\x99\x9a'

Attributes:

Name | Type | Description
--- | --- | ---
`value` | `float` | the double-precision representation of intended single-precision value

Source code in `preserves/values.py`

    69
    70

    def __init__(self, value):
        self.value = value

### `from_bytes(bs)` `staticmethod`

Converts a 4-byte-long byte string to a 32-bit single-precision floating point value wrapped in a Float instance. Takes care to preserve the quiet/signalling bit-pattern of NaN values, unlike its `struct.unpack('>f', ...)` equivalent.

    >>> Float.from_bytes(b'\x7f\x80\x00{')
    Float(nan)
    >>> Float.from_bytes(b'\x7f\x80\x00{').to_bytes()
    b'\x7f\x80\x00{'

    >>> struct.unpack('>f', b'\x7f\x80\x00{')[0]
    nan
    >>> Float(struct.unpack('>f', b'\x7f\x80\x00{')[0]).to_bytes()
    b'\x7f\xc0\x00{'
    >>> struct.pack('>f', struct.unpack('>f', b'\x7f\x80\x00{')[0])
    b'\x7f\xc0\x00{'

(Note well the difference between `7f80007b` and `7fc0007b`!)

Source code in `preserves/values.py`

    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168

    @staticmethod
    def from_bytes(bs):
        """Converts a 4-byte-long byte string to a 32-bit single-precision floating point value
        wrapped in a [Float][preserves.values.Float] instance. Takes care to preserve the
        quiet/signalling bit-pattern of NaN values, unlike its `struct.unpack('>f', ...)`
        equivalent.

        ```python
        >>> Float.from_bytes(b'\\x7f\\x80\\x00{')
        Float(nan)
        >>> Float.from_bytes(b'\\x7f\\x80\\x00{').to_bytes()
        b'\\x7f\\x80\\x00{'

        >>> struct.unpack('>f', b'\\x7f\\x80\\x00{')[0]
        nan
        >>> Float(struct.unpack('>f', b'\\x7f\\x80\\x00{')[0]).to_bytes()
        b'\\x7f\\xc0\\x00{'
        >>> struct.pack('>f', struct.unpack('>f', b'\\x7f\\x80\\x00{')[0])
        b'\\x7f\\xc0\\x00{'

        ```

        (Note well the difference between `7f80007b` and `7fc0007b`!)

        """
        vf = struct.unpack('>I', bs)[0]
        if (vf & 0x7f800000) == 0x7f800000:
            # NaN or inf. Preserve quiet/signalling bit by manually expanding to double-precision.
            sign = vf >> 31
            payload = vf & 0x007fffff
            dbs = struct.pack('>Q', (sign << 63) | 0x7ff0000000000000 | (payload << 29))
            return Float(struct.unpack('>d', dbs)[0])
        else:
            return Float(struct.unpack('>f', bs)[0])

### `to_bytes()`

Converts this 32-bit single-precision floating point value to its binary32 format, taking care to preserve the quiet/signalling bit-pattern of NaN values, unlike its `struct.pack('>f', ...)` equivalent.

    >>> Float.from_bytes(b'\x7f\x80\x00{')
    Float(nan)
    >>> Float.from_bytes(b'\x7f\x80\x00{').to_bytes()
    b'\x7f\x80\x00{'

    >>> struct.unpack('>f', b'\x7f\x80\x00{')[0]
    nan
    >>> Float(struct.unpack('>f', b'\x7f\x80\x00{')[0]).to_bytes()
    b'\x7f\xc0\x00{'
    >>> struct.pack('>f', struct.unpack('>f', b'\x7f\x80\x00{')[0])
    b'\x7f\xc0\x00{'

(Note well the difference between `7f80007b` and `7fc0007b`!)

Source code in `preserves/values.py`

     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123

    def to_bytes(self):
        """Converts this 32-bit single-precision floating point value to its binary32 format,
        taking care to preserve the quiet/signalling bit-pattern of NaN values, unlike its
        `struct.pack('>f', ...)` equivalent.

        ```python
        >>> Float.from_bytes(b'\\x7f\\x80\\x00{')
        Float(nan)
        >>> Float.from_bytes(b'\\x7f\\x80\\x00{').to_bytes()
        b'\\x7f\\x80\\x00{'

        >>> struct.unpack('>f', b'\\x7f\\x80\\x00{')[0]
        nan
        >>> Float(struct.unpack('>f', b'\\x7f\\x80\\x00{')[0]).to_bytes()
        b'\\x7f\\xc0\\x00{'
        >>> struct.pack('>f', struct.unpack('>f', b'\\x7f\\x80\\x00{')[0])
        b'\\x7f\\xc0\\x00{'

        ```

        (Note well the difference between `7f80007b` and `7fc0007b`!)

        """

        if math.isnan(self.value) or math.isinf(self.value):
            dbs = struct.pack('>d', self.value)
            vd = struct.unpack('>Q', dbs)[0]
            sign = vd >> 63
            payload = (vd >> 29) & 0x007fffff
            vf = (sign << 31) | 0x7f800000 | payload
            return struct.pack('>I', vf)
        else:
            return struct.pack('>f', self.value)

## `ImmutableDict(*args, **kwargs)`

Bases: `dict`

A subclass of Python's built-in `dict` that overrides methods that could mutate the dictionary, causing them to raise `TypeError('Immutable')` if called.

Implements the `__hash__` method, allowing ImmutableDict instances to be used whereever immutable data are permitted; in particular, as keys in other dictionaries.

    >>> d = ImmutableDict([('a', 1), ('b', 2)])
    >>> d
    {'a': 1, 'b': 2}
    >>> d['c'] = 3
    Traceback (most recent call last):
      ...
    TypeError: Immutable
    >>> del d['b']
    Traceback (most recent call last):
      ...
    TypeError: Immutable

Source code in `preserves/values.py`

    459
    460
    461
    462

    def __init__(self, *args, **kwargs):
        if hasattr(self, '__hash'): raise TypeError('Immutable')
        super(ImmutableDict, self).__init__(*args, **kwargs)
        self.__hash = None

### `from_kvs(kvs)` `staticmethod`

Constructs an ImmutableDict from a sequence of alternating keys and values; compare to the ImmutableDict constructor, which takes a sequence of key-value pairs.

    >>> ImmutableDict.from_kvs(['a', 1, 'b', 2])
    {'a': 1, 'b': 2}
    >>> ImmutableDict.from_kvs(['a', 1, 'b', 2])['c'] = 3
    Traceback (most recent call last):
      ...
    TypeError: Immutable

Source code in `preserves/values.py`

    480
    481
    482
    483
    484
    485
    486
    487
    488
    489
    490
    491
    492
    493
    494
    495
    496
    497
    498
    499
    500
    501
    502
    503
    504
    505
    506
    507
    508
    509
    510
    511
    512

    @staticmethod
    def from_kvs(kvs):
        """Constructs an [ImmutableDict][preserves.values.ImmutableDict] from a sequence of
        alternating keys and values; compare to the
        [ImmutableDict][preserves.values.ImmutableDict] constructor, which takes a sequence of
        key-value pairs.

        ```python
        >>> ImmutableDict.from_kvs(['a', 1, 'b', 2])
        {'a': 1, 'b': 2}
        >>> ImmutableDict.from_kvs(['a', 1, 'b', 2])['c'] = 3
        Traceback (most recent call last):
          ...
        TypeError: Immutable

        ```

        """

        i = iter(kvs)
        result = ImmutableDict()
        result_proxy = super(ImmutableDict, result)
        try:
            while True:
                k = next(i)
                try:
                    v = next(i)
                except StopIteration:
                    raise DecodeError("Missing dictionary value")
                result_proxy.__setitem__(k, v)
        except StopIteration:
            pass
        return result

## `Record(key, fields)`

Bases: `object`

Representation of Preserves `Record`s, which are a pair of a label `Value` and a sequence of field `Value`s.

    >>> r = Record(Symbol('label'), ['field1', ['field2item1', 'field2item2']])
    >>> r
    #label('field1', ['field2item1', 'field2item2'])
    >>> r.key
    #label
    >>> r.fields
    ('field1', ['field2item1', 'field2item2'])
    >>> import preserves
    >>> preserves.stringify(r)
    '<label "field1" ["field2item1" "field2item2"]>'
    >>> r == preserves.parse('<label "field1" ["field2item1" "field2item2"]>')
    True

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`key` | `Value` | the `Record`'s label | required
`fields` | `iterable[Value]` | the fields of the `Record` | required

Attributes:

Name | Type | Description
--- | --- | ---
`key` | `Value` | the `Record`'s label
`fields` | `tuple[Value]` | the fields of the `Record`

Source code in `preserves/values.py`

    271
    272
    273
    274

    def __init__(self, key, fields):
        self.key = key
        self.fields = tuple(fields)
        self.__hash = None

### `makeBasicConstructor(label, fieldNames)` `staticmethod`

Constructs and returns a "constructor" for `Record`s having a certain `label` and number of fields.

Deprecated

Use preserves.schema definitions instead.

The "constructor" is a callable function that accepts `len(fields)` arguments and returns a Record with `label` as its label and the arguments to the constructor as field values.

In addition, the "constructor" has a `constructorInfo` attribute holding a RecordConstructorInfo object, an `isClassOf` attribute holding a unary function that returns `True` iff its argument is a Record with label `label` and arity `len(fieldNames)`, and an `ensureClassOf` attribute that raises an `Exception` if `isClassOf` returns false on its argument and returns the argument otherwise.

Finally, for each field name `f` in `fieldNames`, the "constructor" object has an attribute `_f` that is a unary function that retrieves the `f` field from the passed in argument.

    >>> c = Record.makeBasicConstructor(Symbol('date'), 'year month day')
    >>> c(1969, 7, 16)
    #date(1969, 7, 16)
    >>> c.constructorInfo
    #date/3
    >>> c.isClassOf(c(1969, 7, 16))
    True
    >>> c.isClassOf(Record(Symbol('date'), [1969, 7, 16]))
    True
    >>> c.isClassOf(Record(Symbol('date'), [1969]))
    False
    >>> c.ensureClassOf(c(1969, 7, 16))
    #date(1969, 7, 16)
    >>> c.ensureClassOf(Record(Symbol('date'), [1969]))
    Traceback (most recent call last):
      ...
    TypeError: Record: expected #date/3, got #date(1969)
    >>> c._year(c(1969, 7, 16))
    1969
    >>> c._month(c(1969, 7, 16))
    7
    >>> c._day(c(1969, 7, 16))
    16

Parameters:

Name | Type | Description | Default
--- | --- | --- | ---
`label` | `Value` | Label to use for constructed/matched `Record`s | required
`fieldNames` | `tuple[str] | list[str] | str` | Names of the `Record`'s fields | required

Source code in `preserves/values.py`

    319
    320
    321
    322
    323
    324
    325
    326
    327
    328
    329
    330
    331
    332
    333
    334
    335
    336
    337
    338
    339
    340
    341
    342
    343
    344
    345
    346
    347
    348
    349
    350
    351
    352
    353
    354
    355
    356
    357
    358
    359
    360
    361
    362
    363
    364
    365
    366
    367
    368
    369
    370
    371
    372
    373
    374
    375
    376
    377
    378
    379
    380
    381
    382
    383
    384
    385
    386
    387
    388
    389
    390
    391
    392
    393
    394
    395
    396
    397
    398

    @staticmethod
    def makeBasicConstructor(label, fieldNames):
        """Constructs and returns a "constructor" for `Record`s having a certain `label` and
        number of fields.

        Deprecated:
           Use [preserves.schema][] definitions instead.

        The "constructor" is a callable function that accepts `len(fields)` arguments and
        returns a [Record][preserves.values.Record] with `label` as its label and the arguments
        to the constructor as field values.

        In addition, the "constructor" has a `constructorInfo` attribute holding a
        [RecordConstructorInfo][preserves.values.RecordConstructorInfo] object, an `isClassOf`
        attribute holding a unary function that returns `True` iff its argument is a
        [Record][preserves.values.Record] with label `label` and arity `len(fieldNames)`, and
        an `ensureClassOf` attribute that raises an `Exception` if `isClassOf` returns false on
        its argument and returns the argument otherwise.

        Finally, for each field name `f` in `fieldNames`, the "constructor" object has an
        attribute `_f` that is a unary function that retrieves the `f` field from the passed in
        argument.

        ```python
        >>> c = Record.makeBasicConstructor(Symbol('date'), 'year month day')
        >>> c(1969, 7, 16)
        #date(1969, 7, 16)
        >>> c.constructorInfo
        #date/3
        >>> c.isClassOf(c(1969, 7, 16))
        True
        >>> c.isClassOf(Record(Symbol('date'), [1969, 7, 16]))
        True
        >>> c.isClassOf(Record(Symbol('date'), [1969]))
        False
        >>> c.ensureClassOf(c(1969, 7, 16))
        #date(1969, 7, 16)
        >>> c.ensureClassOf(Record(Symbol('date'), [1969]))
        Traceback (most recent call last):
          ...
        TypeError: Record: expected #date/3, got #date(1969)
        >>> c._year(c(1969, 7, 16))
        1969
        >>> c._month(c(1969, 7, 16))
        7
        >>> c._day(c(1969, 7, 16))
        16

        ```

        Args:
            label (Value): Label to use for constructed/matched `Record`s
            fieldNames (tuple[str] | list[str] | str): Names of the `Record`'s fields

        """
        if type(fieldNames) == str:
            fieldNames = fieldNames.split()
        arity = len(fieldNames)
        def ctor(*fields):
            if len(fields) != arity:
                raise Exception("Record: cannot instantiate %r expecting %d fields with %d fields"%(
                    label,
                    arity,
                    len(fields)))
            return Record(label, fields)
        ctor.constructorInfo = RecordConstructorInfo(label, arity)
        ctor.isClassOf = lambda v: \
                         isinstance(v, Record) and v.key == label and len(v.fields) == arity
        def ensureClassOf(v):
            if not ctor.isClassOf(v):
                raise TypeError("Record: expected %r/%d, got %r" % (label, arity, v))
            return v
        ctor.ensureClassOf = ensureClassOf
        for fieldIndex in range(len(fieldNames)):
            fieldName = fieldNames[fieldIndex]
            # Stupid python scoping bites again
            def getter(fieldIndex):
                return lambda v: ensureClassOf(v)[fieldIndex]
            setattr(ctor, '_' + fieldName, getter(fieldIndex))
        return ctor

### `makeConstructor(labelSymbolText, fieldNames)` `staticmethod`

Equivalent to `Record.makeBasicConstructor(Symbol(labelSymbolText), fieldNames)`.

Deprecated

Use preserves.schema definitions instead.

Source code in `preserves/values.py`

    309
    310
    311
    312
    313
    314
    315
    316
    317

    @staticmethod
    def makeConstructor(labelSymbolText, fieldNames):
        """
        Equivalent to `Record.makeBasicConstructor(Symbol(labelSymbolText), fieldNames)`.

        Deprecated:
           Use [preserves.schema][] definitions instead.
        """
        return Record.makeBasicConstructor(Symbol(labelSymbolText), fieldNames)

## `RecordConstructorInfo(key, arity)`

Bases: `object`

Describes the shape of a `Record` constructor, namely its label and its arity (field count).

    >>> RecordConstructorInfo(Symbol('label'), 3)
    #label/3

Attributes:

Name | Type | Description
--- | --- | ---
`key` | `Value` | the label of matching `Record`s
`arity` | `int` | the number of fields in matching `Record`s

Source code in `preserves/values.py`

    414
    415
    416

    def __init__(self, key, arity):
        self.key = key
        self.arity = arity

## `Symbol(name)`

Bases: `object`

Representation of Preserves `Symbol`s.

    >>> Symbol('xyz')
    #xyz
    >>> Symbol('xyz').name
    'xyz'
    >>> import preserves
    >>> preserves.stringify(Symbol('xyz'))
    'xyz'
    >>> preserves.stringify(Symbol('hello world'))
    '|hello world|'
    >>> preserves.parse('xyz')
    #xyz
    >>> preserves.parse('|hello world|')
    #hello world

Attributes:

Name | Type | Description
--- | --- | ---
`name` | `str` | the symbol's text label

Source code in `preserves/values.py`

    200
    201

    def __init__(self, name):
        self.name = name.name if isinstance(name, Symbol) else name

## `annotate(v, *anns)`

Wraps `v` in an Annotated object, if it isn't already wrapped, and appends each of the `anns` to the Annotated's `annotations` sequence. NOTE: Does not recursively ensure that any parts of the argument `v` are themselves wrapped in Annotated objects!

    >>> import preserves
    >>> print(preserves.stringify(annotate(123, "A comment", "Another comment")))
    @"A comment" @"Another comment" 123

Source code in `preserves/values.py`

    662
    663
    664
    665
    666
    667
    668
    669
    670
    671
    672
    673
    674
    675
    676
    677
    678
    679

    def annotate(v, *anns):
        """Wraps `v` in an [Annotated][preserves.values.Annotated] object, if it isn't already
        wrapped, and appends each of the `anns` to the [Annotated][preserves.values.Annotated]'s
        `annotations` sequence. NOTE: Does not recursively ensure that any parts of the argument
        `v` are themselves wrapped in [Annotated][preserves.values.Annotated] objects!

        ```python
        >>> import preserves
        >>> print(preserves.stringify(annotate(123, "A comment", "Another comment")))
        @"A comment" @"Another comment" 123

        ```
        """
        if not is_annotated(v):
            v = Annotated(v)
        for a in anns:
            v.annotations.append(a)
        return v

## `cmp_floats(a, b)`

Implements the `totalOrder` predicate defined in section 5.10 of IEEE Std 754-2008.

Source code in `preserves/values.py`

    31
    32
    33
    34
    35
    36
    37
    38
    39
    40

    def cmp_floats(a, b):
        """Implements the `totalOrder` predicate defined in section 5.10 of [IEEE Std
        754-2008](https://dx.doi.org/10.1109/IEEESTD.2008.4610935).

        """
        a = float_to_int(a)
        b = float_to_int(b)
        if a & 0x8000000000000000: a = a ^ 0x7fffffffffffffff
        if b & 0x8000000000000000: b = b ^ 0x7fffffffffffffff
        return a - b

## `dict_kvs(d)`

Generator function yielding a sequence of alternating keys and values from `d`. In some sense the inverse of ImmutableDict.from_kvs.

    >>> list(dict_kvs({'a': 1, 'b': 2}))
    ['a', 1, 'b', 2]

Source code in `preserves/values.py`

    514
    515
    516
    517
    518
    519
    520
    521
    522
    523
    524
    525
    526

    def dict_kvs(d):
        """Generator function yielding a sequence of alternating keys and values from `d`. In some
        sense the inverse of [ImmutableDict.from_kvs][preserves.values.ImmutableDict.from_kvs].

        ```python
        >>> list(dict_kvs({'a': 1, 'b': 2}))
        ['a', 1, 'b', 2]

        ```
        """
        for k in d:
            yield k
            yield d[k]

## `is_annotated(v)`

`True` iff `v` is an instance of Annotated.

Source code in `preserves/values.py`

    612
    613
    614

    def is_annotated(v):
        """`True` iff `v` is an instance of [Annotated][preserves.values.Annotated]."""
        return isinstance(v, Annotated)

## `preserve(v)`

Converts `v` to a representation of a Preserves `Value` by (repeatedly) setting

    v = v.__preserve__()

while `v` has a `__preserve__` method. Parsed Schema values are able to render themselves to their serialized representations this way.

Source code in `preserves/values.py`

    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

    def preserve(v):
        """Converts `v` to a representation of a Preserves `Value` by (repeatedly) setting

        ```python
        v = v.__preserve__()
        ```

        while `v` has a `__preserve__` method. Parsed [Schema][preserves.schema]
        values are able to render themselves to their serialized representations this way.

        """
        while hasattr(v, '__preserve__'):
            v = v.__preserve__()
        return v

## `strip_annotations(v, depth=inf)`

Exposes `depth` layers of raw structure of potentially-Annotated `Value`s. If `depth==0` or `v` is not Annotated, just returns `v`. Otherwise, descends recursively into the structure of `v.item`.

    >>> import preserves
    >>> a = preserves.parse('@"A comment" [@a 1 @b 2 @c 3]', include_annotations=True)
    >>> is_annotated(a)
    True
    >>> print(preserves.stringify(a))
    @"A comment" [@a 1 @b 2 @c 3]
    >>> print(preserves.stringify(strip_annotations(a)))
    [1 2 3]
    >>> print(preserves.stringify(strip_annotations(a, depth=1)))
    [@a 1 @b 2 @c 3]

Source code in `preserves/values.py`

    616
    617
    618
    619
    620
    621
    622
    623
    624
    625
    626
    627
    628
    629
    630
    631
    632
    633
    634
    635
    636
    637
    638
    639
    640
    641
    642
    643
    644
    645
    646
    647
    648
    649
    650
    651
    652
    653
    654
    655
    656
    657
    658
    659
    660

    def strip_annotations(v, depth=inf):
        """Exposes `depth` layers of raw structure of
        potentially-[Annotated][preserves.values.Annotated] `Value`s. If `depth==0` or `v` is not
        [Annotated][preserves.values.Annotated], just returns `v`. Otherwise, descends recursively
        into the structure of `v.item`.

        ```python
        >>> import preserves
        >>> a = preserves.parse('@"A comment" [@a 1 @b 2 @c 3]', include_annotations=True)
        >>> is_annotated(a)
        True
        >>> print(preserves.stringify(a))
        @"A comment" [@a 1 @b 2 @c 3]
        >>> print(preserves.stringify(strip_annotations(a)))
        [1 2 3]
        >>> print(preserves.stringify(strip_annotations(a, depth=1)))
        [@a 1 @b 2 @c 3]

        ```
        """

        if depth == 0: return v
        if not is_annotated(v): return v

        next_depth = depth - 1
        def walk(v):
            return strip_annotations(v, next_depth)

        v = v.item
        if isinstance(v, Record):
            return Record(strip_annotations(v.key, depth), tuple(walk(f) for f in v.fields))
        elif isinstance(v, list):
            return tuple(walk(f) for f in v)
        elif isinstance(v, tuple):
            return tuple(walk(f) for f in v)
        elif isinstance(v, set):
            return frozenset(walk(f) for f in v)
        elif isinstance(v, frozenset):
            return frozenset(walk(f) for f in v)
        elif isinstance(v, dict):
            return ImmutableDict.from_kvs(walk(f) for f in dict_kvs(v))
        elif is_annotated(v):
            raise ValueError('Improper annotation structure')
        else:
            return v

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