123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723 |
- Pyrsistent
- ==========
- .. image:: https://travis-ci.org/tobgu/pyrsistent.png?branch=master
- :target: https://travis-ci.org/tobgu/pyrsistent
- .. image:: https://badge.fury.io/py/pyrsistent.svg
- :target: https://badge.fury.io/py/pyrsistent
- .. image:: https://coveralls.io/repos/tobgu/pyrsistent/badge.svg?branch=master&service=github
- :target: https://coveralls.io/github/tobgu/pyrsistent?branch=master
- .. _Pyrthon: https://www.github.com/tobgu/pyrthon/
- 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 to go all in on persistent data structures and use literal syntax to define them in your code rather
- than function calls check out Pyrthon_.
- 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:
- ... EvenX(x=-1)
- ... except InvariantException as e:
- ... print(e.invariant_errors)
- ...
- (('x negative', 'x odd'),)
- Factories
- *********
- It's possible to specify factory functions for fields. The factory function receives whatever
- is supplied as field value and the actual returned by the factory is assigned to the field
- given that any type and invariant checks hold.
- PRecords have a default factory specified as a static function on the class, create(). It takes
- a *Mapping* as argument and returns an instance of the specific record.
- If a record has fields of type PRecord the create() method of that record will
- be called to create the "sub record" if no factory has explicitly been specified to override
- this behaviour.
- .. code:: python
- >>> class DRecord(PRecord):
- ... x = field(factory=int)
- ...
- >>> class ERecord(PRecord):
- ... d = field(type=DRecord)
- ...
- >>> ERecord.create({'d': {'x': '1'}})
- ERecord(d=DRecord(x=1))
- Collection fields
- *****************
- It is also possible to have fields with ``pyrsistent`` collections.
- .. code:: python
- >>> from pyrsistent import pset_field, pmap_field, pvector_field
- >>> class MultiRecord(PRecord):
- ... set_of_ints = pset_field(int)
- ... map_int_to_str = pmap_field(int, str)
- ... vector_of_strs = pvector_field(str)
- ...
-
- Serialization
- *************
- PRecords support serialization back to dicts. Default serialization will take keys and values
- "as is" and output them into a dict. It is possible to specify custom serialization functions
- to take care of fields that require special treatment.
- .. code:: python
- >>> from datetime import date
- >>> class Person(PRecord):
- ... name = field(type=unicode)
- ... birth_date = field(type=date,
- ... serializer=lambda format, d: d.strftime(format['date']))
- ...
- >>> john = Person(name=u'John', birth_date=date(1985, 10, 21))
- >>> john.serialize({'date': '%Y-%m-%d'})
- {'birth_date': '1985-10-21', 'name': u'John'}
- .. _instar: https://github.com/boxed/instar/
- .. _PClass:
- PClass
- ~~~~~~
- A PClass is a python class with a fixed set of specified fields. PClasses are declared as python classes inheriting
- from PClass. It is defined the same way that PRecords are and behaves like a PRecord in all aspects except that it
- is not a PMap and hence not a collection but rather a plain Python object.
- .. code:: python
- >>> from pyrsistent import PClass, field
- >>> class AClass(PClass):
- ... x = field()
- ...
- >>> a = AClass(x=3)
- >>> a
- AClass(x=3)
- >>> a.x
- 3
- Checked collections
- ~~~~~~~~~~~~~~~~~~~
- Checked collections currently come in three flavors: CheckedPVector, CheckedPMap and CheckedPSet.
- .. code:: python
- >>> from pyrsistent import CheckedPVector, CheckedPMap, CheckedPSet, thaw
- >>> class Positives(CheckedPSet):
- ... __type__ = (long, int)
- ... __invariant__ = lambda n: (n >= 0, 'Negative')
- ...
- >>> class Lottery(PRecord):
- ... name = field(type=str)
- ... numbers = field(type=Positives, invariant=lambda p: (len(p) > 0, 'No numbers'))
- ...
- >>> class Lotteries(CheckedPVector):
- ... __type__ = Lottery
- ...
- >>> class LotteriesByDate(CheckedPMap):
- ... __key_type__ = date
- ... __value_type__ = Lotteries
- ...
- >>> lotteries = LotteriesByDate.create({date(2015, 2, 15): [{'name': 'SuperLotto', 'numbers': {1, 2, 3}},
- ... {'name': 'MegaLotto', 'numbers': {4, 5, 6}}],
- ... date(2015, 2, 16): [{'name': 'SuperLotto', 'numbers': {3, 2, 1}},
- ... {'name': 'MegaLotto', 'numbers': {6, 5, 4}}]})
- >>> lotteries
- LotteriesByDate({datetime.date(2015, 2, 15): Lotteries([Lottery(numbers=Positives([1, 2, 3]), name='SuperLotto'), Lottery(numbers=Positives([4, 5, 6]), name='MegaLotto')]), datetime.date(2015, 2, 16): Lotteries([Lottery(numbers=Positives([1, 2, 3]), name='SuperLotto'), Lottery(numbers=Positives([4, 5, 6]), name='MegaLotto')])})
- # The checked versions support all operations that the corresponding
- # unchecked types do
- >>> lottery_0215 = lotteries[date(2015, 2, 15)]
- >>> lottery_0215.transform([0, 'name'], 'SuperDuperLotto')
- Lotteries([Lottery(numbers=Positives([1, 2, 3]), name='SuperDuperLotto'), Lottery(numbers=Positives([4, 5, 6]), name='MegaLotto')])
- # But also makes asserts that types and invariants hold
- >>> lottery_0215.transform([0, 'name'], 999)
- Traceback (most recent call last):
- PTypeError: Invalid type for field Lottery.name, was int
- >>> lottery_0215.transform([0, 'numbers'], set())
- Traceback (most recent call last):
- InvariantException: Field invariant failed
- # They can be converted back to python built ins with either thaw()
- # or serialize() (which provides possibilities to customize serialization)
- >>> thaw(lottery_0215)
- [{'numbers': set([1, 2, 3]), 'name': 'SuperLotto'}, {'numbers': set([4, 5, 6]), 'name': 'MegaLotto'}]
- >>> lottery_0215.serialize()
- [{'numbers': set([1, 2, 3]), 'name': 'SuperLotto'}, {'numbers': set([4, 5, 6]), 'name': 'MegaLotto'}]
- .. _transformations:
- Transformations
- ~~~~~~~~~~~~~~~
- Transformations are inspired by the cool library instar_ for Clojure. They let you evolve PMaps and PVectors
- with arbitrarily deep/complex nesting using simple syntax and flexible matching syntax.
- The first argument to transformation is the path that points out the value to transform. The
- second is the transformation to perform. If the transformation is callable it will be applied
- to the value(s) matching the path. The path may also contain callables. In that case they are
- treated as matchers. If the matcher returns True for a specific key it is considered for transformation.
- .. code:: python
- # Basic examples
- >>> from pyrsistent import inc, freeze, thaw, rex, ny, discard
- >>> v1 = freeze([1, 2, 3, 4, 5])
- >>> v1.transform([2], inc)
- pvector([1, 2, 4, 4, 5])
- >>> v1.transform([lambda ix: 0 < ix < 4], 8)
- pvector([1, 8, 8, 8, 5])
- >>> v1.transform([lambda ix, v: ix == 0 or v == 5], 0)
- pvector([0, 2, 3, 4, 0])
- # The (a)ny matcher can be used to match anything
- >>> v1.transform([ny], 8)
- pvector([8, 8, 8, 8, 8])
- # Regular expressions can be used for matching
- >>> scores = freeze({'John': 12, 'Joseph': 34, 'Sara': 23})
- >>> scores.transform([rex('^Jo')], 0)
- pmap({'Joseph': 0, 'Sara': 23, 'John': 0})
- # Transformations can be done on arbitrarily deep structures
- >>> news_paper = freeze({'articles': [{'author': 'Sara', 'content': 'A short article'},
- ... {'author': 'Steve', 'content': 'A slightly longer article'}],
- ... 'weather': {'temperature': '11C', 'wind': '5m/s'}})
- >>> short_news = news_paper.transform(['articles', ny, 'content'], lambda c: c[:25] + '...' if len(c) > 25 else c)
- >>> very_short_news = news_paper.transform(['articles', ny, 'content'], lambda c: c[:15] + '...' if len(c) > 15 else c)
- >>> very_short_news.articles[0].content
- 'A short article'
- >>> very_short_news.articles[1].content
- 'A slightly long...'
- # When nothing has been transformed the original data structure is kept
- >>> short_news is news_paper
- True
- >>> very_short_news is news_paper
- False
- >>> very_short_news.articles[0] is news_paper.articles[0]
- True
- # There is a special transformation that can be used to discard elements. Also
- # multiple transformations can be applied in one call
- >>> thaw(news_paper.transform(['weather'], discard, ['articles', ny, 'content'], discard))
- {'articles': [{'author': 'Sara'}, {'author': 'Steve'}]}
- Evolvers
- ~~~~~~~~
- PVector, PMap and PSet all have support for a concept dubbed *evolvers*. An evolver acts like a mutable
- view of the underlying persistent data structure with "transaction like" semantics. No updates of the original
- data structure is ever performed, it is still fully immutable.
- The evolvers have a very limited API by design to discourage excessive, and inappropriate, usage as that would
- take us down the mutable road. In principle only basic mutation and element access functions are supported.
- Check out the documentation_ of each data structure for specific examples.
- Examples of when you may want to use an evolver instead of working directly with the data structure include:
- * Multiple updates are done to the same data structure and the intermediate results are of no
- interest. In this case using an evolver may be a more efficient and easier to work with.
- * You need to pass a vector into a legacy function or a function that you have no control
- over which performs in place mutations. In this case pass an evolver instance
- instead and then create a new pvector from the evolver once the function returns.
- .. code:: python
- >>> from pyrsistent import v
- # In place mutation as when working with the built in counterpart
- >>> v1 = v(1, 2, 3)
- >>> e = v1.evolver()
- >>> e[1] = 22
- >>> e = e.append(4)
- >>> e = e.extend([5, 6])
- >>> e[5] += 1
- >>> len(e)
- 6
- # The evolver is considered *dirty* when it contains changes compared to the underlying vector
- >>> e.is_dirty()
- True
- # But the underlying pvector still remains untouched
- >>> v1
- pvector([1, 2, 3])
- # Once satisfied with the updates you can produce a new pvector containing the updates.
- # The new pvector will share data with the original pvector in the same way that would have
- # been done if only using operations on the pvector.
- >>> v2 = e.persistent()
- >>> v2
- pvector([1, 22, 3, 4, 5, 7])
- # The evolver is now no longer considered *dirty* as it contains no differences compared to the
- # pvector just produced.
- >>> e.is_dirty()
- False
- # You may continue to work with the same evolver without affecting the content of v2
- >>> e[0] = 11
- # Or create a new evolver from v2. The two evolvers can be updated independently but will both
- # share data with v2 where possible.
- >>> e2 = v2.evolver()
- >>> e2[0] = 1111
- >>> e.persistent()
- pvector([11, 22, 3, 4, 5, 7])
- >>> e2.persistent()
- pvector([1111, 22, 3, 4, 5, 7])
- .. _freeze:
- .. _thaw:
- freeze and thaw
- ~~~~~~~~~~~~~~~
- These functions are great when your cozy immutable world has to interact with the evil mutable world outside.
- .. code:: python
- >>> from pyrsistent import freeze, thaw, v, m
- >>> freeze([1, {'a': 3}])
- pvector([1, pmap({'a': 3})])
- >>> thaw(v(1, m(a=3)))
- [1, {'a': 3}]
- Compatibility
- -------------
- Pyrsistent is developed and tested on Python 2.7, 3.5, 3.6, 3.7 and PyPy (Python 2 and 3 compatible). It will most
- likely work on all other versions >= 3.4 but no guarantees are given. :)
- Compatibility issues
- ~~~~~~~~~~~~~~~~~~~~
- .. _27: https://github.com/tobgu/pyrsistent/issues/27
- There is currently one known compatibility issue when comparing built in sets and frozensets to PSets as discussed in 27_.
- It affects python 2 versions < 2.7.8 and python 3 versions < 3.4.0 and is due to a bug described in
- http://bugs.python.org/issue8743.
- Comparisons will fail or be incorrect when using the set/frozenset as left hand side of the comparison. As a workaround
- you need to either upgrade Python to a more recent version, avoid comparing sets/frozensets with PSets or always make
- sure to convert both sides of the comparison to the same type before performing the comparison.
- Performance
- -----------
- Pyrsistent is developed with performance in mind. Still, while some operations are nearly on par with their built in,
- mutable, counterparts in terms of speed, other operations are slower. In the cases where attempts at
- optimizations have been done, speed has generally been valued over space.
- Pyrsistent comes with two API compatible flavors of PVector (on which PMap and PSet are based), one pure Python
- implementation and one implemented as a C extension. The latter generally being 2 - 20 times faster than the former.
- The C extension will be used automatically when possible.
- The pure python implementation is fully PyPy compatible. Running it under PyPy speeds operations up considerably if
- the structures are used heavily (if JITed), for some cases the performance is almost on par with the built in counterparts.
- Type hints
- ----------
- PEP 561 style type hints for use with mypy and various editors are available for most types and functions in pyrsistent.
- Type classes for annotating your own code with pyrsistent types are also available under pyrsistent.typing.
- Installation
- ------------
- pip install pyrsistent
- Documentation
- -------------
- Available at http://pyrsistent.readthedocs.org/
- Brief presentation available at http://slides.com/tobiasgustafsson/immutability-and-python/
- Contributors
- ------------
- Tobias Gustafsson https://github.com/tobgu
- Christopher Armstrong https://github.com/radix
- Anders Hovmöller https://github.com/boxed
- Itamar Turner-Trauring https://github.com/itamarst
- Jonathan Lange https://github.com/jml
- Richard Futrell https://github.com/Futrell
- Jakob Hollenstein https://github.com/jkbjh
- David Honour https://github.com/foolswood
- David R. MacIver https://github.com/DRMacIver
- Marcus Ewert https://github.com/sarum90
- Jean-Paul Calderone https://github.com/exarkun
- Douglas Treadwell https://github.com/douglas-treadwell
- Travis Parker https://github.com/teepark
- Julian Berman https://github.com/Julian
- Dennis Tomas https://github.com/dtomas
- Neil Vyas https://github.com/neilvyas
- doozr https://github.com/doozr
- Kamil Galuszka https://github.com/galuszkak
- Tsuyoshi Hombashi https://github.com/thombashi
- nattofriends https://github.com/nattofriends
- agberk https://github.com/agberk
- Waleed Khan https://github.com/arxanas
- Jean-Louis Fuchs https://github.com/ganwell
- Carlos Corbacho https://github.com/ccorbacho
- Felix Yan https://github.com/felixonmars
- benrg https://github.com/benrg
- Jere Lahelma https://github.com/je-l
- Max Taggart https://github.com/MaxTaggart
- Vincent Philippon https://github.com/vphilippon
- Semen Zhydenko https://github.com/ss18
- Till Varoquaux https://github.com/till-varoquaux
- Michal Kowalik https://github.com/michalvi
- ossdev07 https://github.com/ossdev07
- Kerry Olesen https://github.com/qhesz
- Contributing
- ------------
- Want to contribute? That's great! If you experience problems please log them on GitHub. If you want to contribute code,
- please fork the repository and submit a pull request.
- Run tests
- ~~~~~~~~~
- .. _tox: https://tox.readthedocs.io/en/latest/
- Tests can be executed using tox_.
- Install tox: ``pip install tox``
- Run test for Python 2.7: ``tox -epy27``
- Release
- ~~~~~~~
- * Update CHANGES.txt
- * Update README with any new contributors and potential info needed.
- * Update _pyrsistent_version.py
- * python setup.py sdist upload
- * Commit and tag with new version: git add -u . && git commit -m 'Prepare version vX.Y.Z' && git tag -a vX.Y.Z -m 'vX.Y.Z'
- * Push commit and tags: git push && git push --tags
- Project status
- --------------
- Pyrsistent can be considered stable and mature (who knows, there may even be a 1.0 some day :-)). The project is
- maintained, bugs fixed, PRs reviewed and merged and new releases made. I currently do not have time for development
- of new features or functionality which I don't have use for myself. I'm more than happy to take PRs for new
- functionality though!
- There are a bunch of issues marked with ``enhancement`` and ``help wanted`` that contain requests for new functionality
- that would be nice to include. The level of difficulty and extend of the issues varies, please reach out to me if you're
- interested in working on any of them.
- If you feel that you have a grand master plan for where you would like Pyrsistent to go and have the time to put into
- it please don't hesitate to discuss this with me and submit PRs for it. If all goes well I'd be more than happy to add
- additional maintainers to the project!
|