Give AlbumentationsX a star on GitHub — it powers this leaderboard

Star on GitHub

pyrsistent

Persistent/Functional/Immutable data structures

Downloads: 0 (30 days)

Description

Pyrsistent
==========
.. image:: https://github.com/tobgu/pyrsistent/actions/workflows/tests.yaml/badge.svg
    :target: https://github.com/tobgu/pyrsistent/actions/workflows/tests.yaml


.. _Pyrthon: https://www.github.com/tobgu/pyrthon
.. _Pyrsistent_extras: https://github.com/mingmingrr/pyrsistent-extras

Pyrsistent is a number of persistent collections (by some referred to as functional data structures). Persistent in
the sense that they are immutable.

All methods on a data structure that would normally mutate it instead return a new copy of the structure containing the
requested updates. The original structure is left untouched.

This will simplify the reasoning about what a program does since no hidden side effects ever can take place to these
data structures. You can rest assured that the object you hold a reference to will remain the same throughout its
lifetime and need not worry that somewhere five stack levels below you in the darkest corner of your application
someone has decided to remove that element that you expected to be there.

Pyrsistent is influenced by persistent data structures such as those found in the standard library of Clojure. The
data structures are designed to share common elements through path copying.
It aims at taking these concepts and make them as pythonic as possible so that they can be easily integrated into any python
program without hassle.

If you want use literal syntax to define them in your code rather
than function calls check out Pyrthon_. Be aware, that one is experimental, unmaintained and alpha software. 

If you cannot find the persistent data structure you're looking for here you may want to take a look at
Pyrsistent_extras_ which is maintained by @mingmingrr. If you still don't find what you're looking for please
open an issue for discussion. If we agree that functionality is missing you may want to go ahead and create
a Pull Request implement the missing functionality.

Examples
--------
.. _Sequence: collections_
.. _Hashable: collections_
.. _Mapping: collections_
.. _Mappings: collections_
.. _Set: collections_
.. _collections: https://docs.python.org/3/library/collections.abc.html
.. _documentation: http://pyrsistent.readthedocs.org/

The collection types and key features currently implemented are:

* PVector_, similar to a python list
* PMap_, similar to dict
* PSet_, similar to set
* PRecord_, a PMap on steroids with fixed fields, optional type and invariant checking and much more
* PClass_, a Python class fixed fields, optional type and invariant checking and much more
* `Checked collections`_, PVector, PMap and PSet with optional type and invariance checks and more
* PBag, similar to collections.Counter
* PList, a classic singly linked list
* PDeque, similar to collections.deque
* Immutable object type (immutable) built on the named tuple
* freeze_ and thaw_ functions to convert between pythons standard collections and pyrsistent collections.
* Flexible transformations_ of arbitrarily complex structures built from PMaps and PVectors.

Below are examples of common usage patterns for some of the structures and features. More information and
full documentation for all data structures is available in the documentation_.

.. _PVector:

PVector
~~~~~~~
With full support for the Sequence_ protocol PVector is meant as a drop in replacement to the built in list from a readers
point of view. Write operations of course differ since no in place mutation is done but naming should be in line
with corresponding operations on the built in list.

Support for the Hashable_ protocol also means that it can be used as key in Mappings_.

Appends are amortized O(1). Random access and insert is log32(n) where n is the size of the vector.

.. code:: python

    >>> from pyrsistent import v, pvector

    # No mutation of vectors once created, instead they
    # are "evolved" leaving the original untouched
    >>> v1 = v(1, 2, 3)
    >>> v2 = v1.append(4)
    >>> v3 = v2.set(1, 5)
    >>> v1
    pvector([1, 2, 3])
    >>> v2
    pvector([1, 2, 3, 4])
    >>> v3
    pvector([1, 5, 3, 4])

    # Random access and slicing
    >>> v3[1]
    5
    >>> v3[1:3]
    pvector([5, 3])

    # Iteration
    >>> list(x + 1 for x in v3)
    [2, 6, 4, 5]
    >>> pvector(2 * x for x in range(3))
    pvector([0, 2, 4])

.. _PMap:

PMap
~~~~
With full support for the Mapping_ protocol PMap is meant as a drop in replacement to the built in dict from a readers point
of view. Support for the Hashable_ protocol also means that it can be used as key in other Mappings_.

Random access and insert is log32(n) where n is the size of the map.

.. code:: python

    >>> from pyrsistent import m, pmap, v

    # No mutation of maps once created, instead they are
    # "evolved" leaving the original untouched
    >>> m1 = m(a=1, b=2)
    >>> m2 = m1.set('c', 3)
    >>> m3 = m2.set('a', 5)
    >>> m1
    pmap({'a': 1, 'b': 2})
    >>> m2
    pmap({'a': 1, 'c': 3, 'b': 2})
    >>> m3
    pmap({'a': 5, 'c': 3, 'b': 2})
    >>> m3['a']
    5

    # Evolution of nested persistent structures
    >>> m4 = m(a=5, b=6, c=v(1, 2))
    >>> m4.transform(('c', 1), 17)
    pmap({'a': 5, 'c': pvector([1, 17]), 'b': 6})
    >>> m5 = m(a=1, b=2)

    # Evolve by merging with other mappings
    >>> m5.update(m(a=2, c=3), {'a': 17, 'd': 35})
    pmap({'a': 17, 'c': 3, 'b': 2, 'd': 35})
    >>> pmap({'x': 1, 'y': 2}) + pmap({'y': 3, 'z': 4})
    pmap({'y': 3, 'x': 1, 'z': 4})

    # Dict-like methods to convert to list and iterate
    >>> m3.items()
    pvector([('a', 5), ('c', 3), ('b', 2)])
    >>> list(m3)
    ['a', 'c', 'b']

.. _PSet:

PSet
~~~~
With full support for the Set_ protocol PSet is meant as a drop in replacement to the built in set from a readers point
of view. Support for the Hashable_ protocol also means that it can be used as key in Mappings_.

Random access and insert is log32(n) where n is the size of the set.

.. code:: python

    >>> from pyrsistent import s

    # No mutation of sets once created, you know the story...
    >>> s1 = s(1, 2, 3, 2)
    >>> s2 = s1.add(4)
    >>> s3 = s1.remove(1)
    >>> s1
    pset([1, 2, 3])
    >>> s2
    pset([1, 2, 3, 4])
    >>> s3
    pset([2, 3])

    # Full support for set operations
    >>> s1 | s(3, 4, 5)
    pset([1, 2, 3, 4, 5])
    >>> s1 & s(3, 4, 5)
    pset([3])
    >>> s1 < s2
    True
    >>> s1 < s(3, 4, 5)
    False

.. _PRecord:

PRecord
~~~~~~~
A PRecord is a PMap with a fixed set of specified fields. Records are declared as python classes inheriting
from PRecord. Because it is a PMap it has full support for all Mapping methods such as iteration and element
access using subscript notation.

.. code:: python

    >>> from pyrsistent import PRecord, field
    >>> class ARecord(PRecord):
    ...     x = field()
    ...
    >>> r = ARecord(x=3)
    >>> r
    ARecord(x=3)
    >>> r.x
    3
    >>> r.set(x=2)
    ARecord(x=2)
    >>> r.set(y=2)
    Traceback (most recent call last):
    AttributeError: 'y' is not among the specified fields for ARecord

Type information
****************
It is possible to add type information to the record to enforce type checks. Multiple allowed types can be specified
by providing an iterable of types.

.. code:: python

    >>> class BRecord(PRecord):
    ...     x = field(type=int)
    ...     y = field(type=(int, type(None)))
    ...
    >>> BRecord(x=3, y=None)
    BRecord(y=None, x=3)
    >>> BRecord(x=3.0)
    Traceback (most recent call last):
    PTypeError: Invalid type for field BRecord.x, was float


Custom types (classes) that are iterable should be wrapped in a tuple to prevent their
members being added to the set of valid types.  Although Enums in particular are now
supported without wrapping, see #83 for more information.

Mandatory fields
****************
Fields are not mandatory by default but can be specified as such. If fields are missing an
*InvariantException* will be thrown which contains information about the missing fields.

.. code:: python

    >>> from pyrsistent import InvariantException
    >>> class CRecord(PRecord):
    ...     x = field(mandatory=True)
    ...
    >>> r = CRecord(x=3)
    >>> try:
    ...    r.discard('x')
    ... except InvariantException as e:
    ...    print(e.missing_fields)
    ...
    ('CRecord.x',)

Invariants
**********
It is possible to add invariants that must hold when evolving the record. Invariants can be
specified on both field and record level. If invariants fail an *InvariantException* will be
thrown which contains information about the failing invariants. An invariant function should
return a tuple consisting of a boolean that tells if the invariant holds or not and an object
describing the invariant. This object can later be used to identify which invariant that failed.

The global invariant function is only executed if all field invariants hold.

Global invariants are inherited to subclasses.

.. code:: python

    >>> class RestrictedVector(PRecord):
    ...     __invariant__ = lambda r: (r.y >= r.x, 'x larger than y')
    ...     x = field(invariant=lambda x: (x > 0, 'x negative'))
    ...     y = field(invariant=lambda y: (y > 0, 'y negative'))
    ...
    >>> r = RestrictedVector(y=3, x=2)
    >>> try:
    ...    r.set(x=-1, y=-2)
    ... except InvariantException as e:
    ...    print(e.invariant_errors)
    ...
    ('y negative', 'x negative')
    >>> try:
    ...    r.set(x=2, y=1)
    ... except InvariantException as e:
    ...    print(e.invariant_errors)
    ...
    ('x larger than y',)

Invariants may also contain multiple assertions. For those cases the invariant function should
return a tuple of invariant tuples as described above. This structure is reflected in the
invariant_errors attribute of the exception which will contain tuples with data from all failed
invariants. Eg:

.. code:: python

    >>> class EvenX(PRecord):
    ...     x = field(invariant=lambda x: ((x > 0, 'x negative'), (x % 2 == 0, 'x odd')))
    ...
    >>> try:
    ...