rpds_py-0.12.0/Cargo.toml 0000644 00000000345 00000000000 0010513 ustar [package]
name = "rpds-py"
version = "0.12.0"
edition = "2021"
[lib]
name = "rpds"
crate-type = ["cdylib"]
[dependencies]
rpds = "1.0.1"
archery = "1.0.0"
[dependencies.pyo3]
version = "0.20.0"
features = ["extension-module"]
rpds_py-0.12.0/.github/SECURITY.md 0100644 0001751 0000177 00000001160 14521272702 0014567 0 ustar 0000000 0000000 # Security Policy
## Supported Versions
In general, only the latest released `rpds-py` version is supported and will receive updates.
## Reporting a Vulnerability
To report a security vulnerability, please send an email to `Julian+Security` at `GrayVines.com` with subject line `SECURITY (rpds-py)`.
I will do my best to respond within 48 hours to acknowledge the message and discuss further steps.
If the vulnerability is accepted, an advisory will be sent out via GitHub's security advisory functionality.
For non-sensitive discussion related to this policy itself, feel free to open an issue on the issue tracker.
rpds_py-0.12.0/.github/dependabot.yml 0100644 0001751 0000177 00000000320 14521272702 0015623 0 ustar 0000000 0000000 version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
rpds_py-0.12.0/.github/release.yml 0100644 0001751 0000177 00000000114 14521272702 0015137 0 ustar 0000000 0000000 changelog:
exclude:
authors:
- dependabot
- pre-commit-ci
rpds_py-0.12.0/.github/workflows/CI.yml 0100644 0001751 0000177 00000012705 14521272702 0016060 0 ustar 0000000 0000000 name: CI
on:
push:
branches:
- main
tags:
- "v[0-9].*"
pull_request:
release:
types: [published]
schedule:
# Daily at 5:33
- cron: "33 5 * * *"
workflow_dispatch:
jobs:
pre-commit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- uses: pre-commit/action@v3.0.0
list:
runs-on: ubuntu-latest
outputs:
noxenvs: ${{ steps.noxenvs-matrix.outputs.noxenvs }}
steps:
- uses: actions/checkout@v4
- name: Set up nox
uses: wntrblm/nox@2023.04.22
- id: noxenvs-matrix
run: |
echo >>$GITHUB_OUTPUT noxenvs=$(
nox --list-sessions --json | jq '[.[].session]'
)
test:
needs: list
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
noxenv: ${{ fromJson(needs.list.outputs.noxenvs) }}
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install -y libenchant-2-dev
if: runner.os == 'Linux' && startsWith(matrix.noxenv, 'docs')
- name: Install dependencies
run: brew install enchant
if: runner.os == 'macOS' && startsWith(matrix.noxenv, 'docs')
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: |
3.8
3.9
3.10
3.11
3.12
pypy3.10
allow-prereleases: true
- name: Set up nox
uses: wntrblm/nox@2023.04.22
- name: Run nox
run: nox -s "${{ matrix.noxenv }}"
manylinux:
needs: test
runs-on: ubuntu-latest
strategy:
matrix:
target: [x86_64, x86, aarch64, armv7, s390x, ppc64le]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --interpreter '3.8 3.9 3.10 3.11 3.12 pypy3.8 pypy3.9 pypy3.10'
sccache: "true"
manylinux: auto
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
musllinux:
needs: test
runs-on: ubuntu-latest
strategy:
matrix:
target:
- aarch64-unknown-linux-musl
- i686-unknown-linux-musl
- x86_64-unknown-linux-musl
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --interpreter '3.8 3.9 3.10 3.11 3.12 pypy3.8 pypy3.9 pypy3.10'
manylinux: musllinux_1_2
sccache: "true"
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
windows:
needs: test
runs-on: windows-latest
strategy:
matrix:
target: [x64, x86]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: |
3.8
3.9
3.10
3.11
3.12
architecture: ${{ matrix.target }}
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --interpreter '3.8 3.9 3.10 3.11 3.12'
sccache: "true"
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
macos:
needs: test
runs-on: macos-latest
strategy:
matrix:
target: [x86_64, aarch64]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.x"
- name: Build wheels
uses: PyO3/maturin-action@v1
with:
target: ${{ matrix.target }}
args: --release --out dist --interpreter '3.8 3.9 3.10 3.11 3.12 pypy3.8 pypy3.9 pypy3.10'
sccache: "true"
- name: Upload wheels
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
sdist:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build an sdist
uses: PyO3/maturin-action@v1
with:
command: sdist
args: --out dist
- name: Upload sdist
uses: actions/upload-artifact@v3
with:
name: wheels
path: dist
release:
needs: [manylinux, musllinux, windows, macos]
runs-on: ubuntu-latest
if: "startsWith(github.ref, 'refs/tags/')"
environment:
name: PyPI
url: https://pypi.org/p/rpds-py
permissions:
contents: write
id-token: write
steps:
- uses: actions/download-artifact@v3
with:
name: wheels
- name: Publish to PyPI
uses: PyO3/maturin-action@v1
with:
command: upload
args: --non-interactive --skip-existing *
- name: Create a GitHub Release
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags')
uses: softprops/action-gh-release@v1
with:
files: |
*
generate_release_notes: true
rpds_py-0.12.0/.gitignore 0100644 0001751 0000177 00000001256 14521272702 0013434 0 ustar 0000000 0000000 /target
# Byte-compiled / optimized / DLL files
__pycache__/
.pytest_cache/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
.venv/
env/
bin/
build/
develop-eggs/
dist/
eggs/
lib/
lib64/
parts/
sdist/
var/
include/
man/
venv/
*.egg-info/
.installed.cfg
*.egg
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
pip-selfcheck.json
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# Rope
.ropeproject
# Django stuff:
*.log
*.pot
.DS_Store
# Sphinx documentation
docs/_build/
# PyCharm
.idea/
# VSCode
.vscode/
# Pyenv
.python-version
rpds_py-0.12.0/.pre-commit-config.yaml 0100644 0001751 0000177 00000001633 14521272702 0015724 0 ustar 0000000 0000000 repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: check-ast
- id: check-docstring-first
- id: check-toml
- id: check-vcs-permalinks
- id: check-yaml
- id: debug-statements
- id: end-of-file-fixer
- id: mixed-line-ending
args: [--fix, lf]
- id: trailing-whitespace
- repo: https://github.com/doublify/pre-commit-rust
rev: "v1.0"
hooks:
- id: fmt
- id: clippy
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.0
hooks:
- id: pyupgrade
- repo: https://github.com/psf/black
rev: 23.10.1
hooks:
- name: black
id: black
args: ["--line-length", "79"]
- repo: https://github.com/pre-commit/mirrors-prettier
rev: "v3.0.3"
hooks:
- id: prettier
rpds_py-0.12.0/LICENSE 0100644 0001751 0000177 00000002041 14521272702 0012442 0 ustar 0000000 0000000 Copyright (c) 2023 Julian Berman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
rpds_py-0.12.0/README.rst 0100644 0001751 0000177 00000004720 14521272702 0013132 0 ustar 0000000 0000000 ===========
``rpds.py``
===========
|PyPI| |Pythons| |CI|
.. |PyPI| image:: https://img.shields.io/pypi/v/rpds-py.svg
:alt: PyPI version
:target: https://pypi.org/project/rpds-py/
.. |Pythons| image:: https://img.shields.io/pypi/pyversions/rpds-py.svg
:alt: Supported Python versions
:target: https://pypi.org/project/rpds-py/
.. |CI| image:: https://github.com/crate-py/rpds/workflows/CI/badge.svg
:alt: Build status
:target: https://github.com/crate-py/rpds/actions?query=workflow%3ACI
Python bindings to the `Rust rpds crate `_ for persistent data structures.
What's here is quite minimal (in transparency, it was written initially to support replacing ``pyrsistent`` in the `referencing library `_).
If you see something missing (which is very likely), a PR is definitely welcome to add it.
Installation
------------
The distribution on PyPI is named ``rpds.py`` (equivalently ``rpds-py``), and thus can be installed via e.g.:
.. code:: sh
$ pip install rpds-py
Note that if you install ``rpds-py`` from source, you will need a Rust toolchain installed, as it is a build-time dependency.
An example of how to do so in a ``Dockerfile`` can be found `here `_.
If you believe you are on a common platform which should have wheels built (i.e. and not need to compile from source), feel free to file an issue or pull request modifying the GitHub action used here to build wheels via ``maturin``.
Usage
-----
Methods in general are named similarly to their ``rpds`` counterparts (rather than ``pyrsistent``\ 's conventions, though probably a full drop-in ``pyrsistent``\ -compatible wrapper module is a good addition at some point).
.. code:: python
>>> from rpds import HashTrieMap, HashTrieSet, List
>>> m = HashTrieMap({"foo": "bar", "baz": "quux"})
>>> m.insert("spam", 37) == HashTrieMap({"foo": "bar", "baz": "quux", "spam": 37})
True
>>> m.remove("foo") == HashTrieMap({"baz": "quux"})
True
>>> s = HashTrieSet({"foo", "bar", "baz", "quux"})
>>> s.insert("spam") == HashTrieSet({"foo", "bar", "baz", "quux", "spam"})
True
>>> s.remove("foo") == HashTrieSet({"bar", "baz", "quux"})
True
>>> L = List([1, 3, 5])
>>> L.push_front(-1) == List([-1, 1, 3, 5])
True
>>> L.rest == List([3, 5])
True
rpds_py-0.12.0/noxfile.py 0100644 0001751 0000177 00000002520 14521272702 0013455 0 ustar 0000000 0000000 from pathlib import Path
from tempfile import TemporaryDirectory
import nox
ROOT = Path(__file__).parent
TESTS = ROOT / "tests"
PYPROJECT = ROOT / "pyproject.toml"
nox.options.sessions = []
def session(default=True, **kwargs):
def _session(fn):
if default:
nox.options.sessions.append(kwargs.get("name", fn.__name__))
return nox.session(**kwargs)(fn)
return _session
@session(python=["3.8", "3.9", "3.10", "3.11", "3.12", "pypy3"])
def tests(session):
session.install(ROOT, "-r", TESTS / "requirements.txt")
if session.posargs == ["coverage"]:
session.install("coverage[toml]")
session.run("coverage", "run", "-m", "pytest")
session.run("coverage", "report")
else:
session.run("pytest", *session.posargs, TESTS)
@session(tags=["build"])
def build(session):
session.install("build", "twine")
with TemporaryDirectory() as tmpdir:
session.run("python", "-m", "build", ROOT, "--outdir", tmpdir)
session.run("twine", "check", "--strict", tmpdir + "/*")
@session(default=False)
def requirements(session):
session.install("pip-tools")
for each in [TESTS / "requirements.in"]:
session.run(
"pip-compile",
"--resolver",
"backtracking",
"-U",
each.relative_to(ROOT),
)
rpds_py-0.12.0/rpds.pyi 0100644 0001751 0000177 00000003231 14521272702 0013132 0 ustar 0000000 0000000 from typing import (
FrozenSet,
ItemsView,
Iterable,
Iterator,
KeysView,
Mapping,
TypeVar,
ValuesView,
)
T = TypeVar("T")
KT = TypeVar("KT", covariant=True)
VT = TypeVar("VT", covariant=True)
class HashTrieMap(Mapping[KT, VT]):
def __init__(
self,
value: Mapping[KT, VT] | Iterable[tuple[KT, VT]] = {},
**kwds: Mapping[KT, VT],
): ...
def __getitem__(self, key: KT) -> VT: ...
def __iter__(self) -> Iterator[KT]: ...
def __len__(self) -> int: ...
def discard(self, key: KT) -> "HashTrieMap[KT, VT]": ...
def items(self) -> ItemsView[KT, VT]: ...
def keys(self) -> KeysView[KT]: ...
def values(self) -> ValuesView[VT]: ...
def remove(self, key: KT) -> "HashTrieMap[KT, VT]": ...
def insert(self, key: KT, val: VT) -> "HashTrieMap[KT, VT]": ...
def update(self, *args: Mapping): ...
@classmethod
def convert(
cls,
value: Mapping[KT, VT] | Iterable[tuple[KT, VT]],
) -> "HashTrieMap[KT, VT]": ...
class HashTrieSet(FrozenSet[T]):
def __init__(self, value: Iterable[T] = ()): ...
def __iter__(self) -> Iterator[T]: ...
def __len__(self) -> int: ...
def discard(self, value: T) -> "HashTrieSet[T]": ...
def remove(self, value: T) -> "HashTrieSet[T]": ...
def insert(self, value: T) -> "HashTrieSet[T]": ...
def update(self, *args: Iterable[T]) -> "HashTrieSet[T]": ...
class List(Iterable[T]):
def __init__(self, value: Iterable[T] = (), *more: T): ...
def __iter__(self) -> Iterator[T]: ...
def __len__(self) -> int: ...
def push_front(self, value: T) -> "List[T]": ...
def drop_front(self) -> "List[T]": ...
rpds_py-0.12.0/src/lib.rs 0100644 0001751 0000177 00000041376 14521272702 0013356 0 ustar 0000000 0000000 use std::hash::{Hash, Hasher};
use std::vec::IntoIter;
use pyo3::exceptions::PyIndexError;
use pyo3::pyclass::CompareOp;
use pyo3::types::{PyDict, PyIterator, PyTuple, PyType};
use pyo3::{exceptions::PyKeyError, types::PyMapping};
use pyo3::{prelude::*, AsPyPointer};
use rpds::{HashTrieMap, HashTrieMapSync, HashTrieSet, HashTrieSetSync, List, ListSync};
#[derive(Clone, Debug)]
struct Key {
hash: isize,
inner: PyObject,
}
impl Hash for Key {
fn hash(&self, state: &mut H) {
state.write_isize(self.hash);
}
}
impl Eq for Key {}
impl PartialEq for Key {
fn eq(&self, other: &Self) -> bool {
Python::with_gil(|py| {
self.inner
.call_method1(py, "__eq__", (&other.inner,))
.and_then(|value| value.extract(py))
.expect("__eq__ failed!")
})
}
}
impl IntoPy for Key {
fn into_py(self, py: Python<'_>) -> PyObject {
self.inner.into_py(py)
}
}
unsafe impl AsPyPointer for Key {
fn as_ptr(&self) -> *mut pyo3::ffi::PyObject {
self.inner.as_ptr()
}
}
impl<'source> FromPyObject<'source> for Key {
fn extract(ob: &'source PyAny) -> PyResult {
Ok(Key {
hash: ob.hash()?,
inner: ob.into(),
})
}
}
#[repr(transparent)]
#[pyclass(name = "HashTrieMap", module = "rpds", frozen, mapping)]
struct HashTrieMapPy {
inner: HashTrieMapSync,
}
impl From> for HashTrieMapPy {
fn from(map: HashTrieMapSync) -> Self {
HashTrieMapPy { inner: map }
}
}
impl<'source> FromPyObject<'source> for HashTrieMapPy {
fn extract(ob: &'source PyAny) -> PyResult {
let mut ret = HashTrieMap::new_sync();
if let Ok(mapping) = ob.downcast::() {
for each in mapping.items()?.iter()? {
let (k, v): (Key, PyObject) = each?.extract()?;
ret.insert_mut(k, v);
}
} else {
for each in ob.iter()? {
let (k, v): (Key, PyObject) = each?.extract()?;
ret.insert_mut(k, v);
}
}
Ok(HashTrieMapPy { inner: ret })
}
}
#[pymethods]
impl HashTrieMapPy {
#[new]
#[pyo3(signature = (value=None, **kwds))]
fn init(value: Option, kwds: Option<&PyDict>) -> PyResult {
let mut map: HashTrieMapPy;
if let Some(value) = value {
map = value;
} else {
map = HashTrieMapPy {
inner: HashTrieMap::new_sync(),
};
}
if let Some(kwds) = kwds {
for (k, v) in kwds {
map.inner.insert_mut(Key::extract(k)?, v.into());
}
}
Ok(map)
}
fn __contains__(&self, key: Key) -> bool {
self.inner.contains_key(&key)
}
fn __iter__(slf: PyRef<'_, Self>) -> PyResult> {
Py::new(
slf.py(),
KeyIterator {
inner: slf.keys().into_iter(),
},
)
}
fn __getitem__(&self, key: Key) -> PyResult {
match self.inner.get(&key) {
Some(value) => Ok(value.to_owned()),
None => Err(PyKeyError::new_err(key)),
}
}
fn __len__(&self) -> usize {
self.inner.size()
}
fn __repr__(&self, py: Python) -> String {
let contents = self.inner.into_iter().map(|(k, v)| {
format!(
"{}: {}",
k.clone().into_py(py),
v.call_method0(py, "__repr__")
.and_then(|r| r.extract(py))
.unwrap_or("".to_owned())
)
});
format!(
"HashTrieMap({{{}}})",
contents.collect::>().join(", ")
)
}
fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> PyResult {
match op {
CompareOp::Eq => Ok((self.inner.size() == other.inner.size()
&& self
.inner
.iter()
.map(|(k1, v1)| (v1, other.inner.get(k1)))
.map(|(v1, v2)| PyAny::eq(v1.extract(py)?, v2))
.all(|r| r.unwrap_or(false)))
.into_py(py)),
CompareOp::Ne => Ok((self.inner.size() != other.inner.size()
|| self
.inner
.iter()
.map(|(k1, v1)| (v1, other.inner.get(k1)))
.map(|(v1, v2)| PyAny::ne(v1.extract(py)?, v2))
.all(|r| r.unwrap_or(true)))
.into_py(py)),
_ => Ok(py.NotImplemented()),
}
}
#[classmethod]
fn convert(_cls: &PyType, value: &PyAny, py: Python) -> PyResult {
if value.is_instance_of::() {
Ok(value.into())
} else {
Ok(HashTrieMapPy::extract(value)?.into_py(py))
}
}
fn get(&self, key: Key) -> Option<&PyObject> {
self.inner.get(&key)
}
fn keys(&self) -> Vec {
self.inner.keys().cloned().collect()
}
fn values(&self) -> Vec<&PyObject> {
self.inner.values().collect::>()
}
fn items(&self) -> Vec<(Key, &PyObject)> {
self.inner.iter().map(|(k, v)| (k.clone(), v)).collect()
}
fn discard(&self, key: Key) -> PyResult {
match self.inner.contains_key(&key) {
true => Ok(HashTrieMapPy {
inner: self.inner.remove(&key),
}),
false => Ok(HashTrieMapPy {
inner: self.inner.clone(),
}),
}
}
fn insert(&self, key: Key, value: &PyAny) -> HashTrieMapPy {
HashTrieMapPy {
inner: self.inner.insert(key, value.into()),
}
}
fn remove(&self, key: Key) -> PyResult {
match self.inner.contains_key(&key) {
true => Ok(HashTrieMapPy {
inner: self.inner.remove(&key),
}),
false => Err(PyKeyError::new_err(key)),
}
}
#[pyo3(signature = (*maps, **kwds))]
fn update(&self, maps: &PyTuple, kwds: Option<&PyDict>) -> PyResult {
let mut inner = self.inner.clone();
for value in maps {
let map = HashTrieMapPy::extract(value)?;
for (k, v) in &map.inner {
inner.insert_mut(k.to_owned(), v.to_owned());
}
}
if let Some(kwds) = kwds {
for (k, v) in kwds {
inner.insert_mut(Key::extract(k)?, v.extract()?);
}
}
Ok(HashTrieMapPy { inner })
}
}
#[pyclass(module = "rpds")]
struct KeyIterator {
inner: IntoIter,
}
#[pymethods]
impl KeyIterator {
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
slf
}
fn __next__(mut slf: PyRefMut<'_, Self>) -> Option {
slf.inner.next()
}
}
#[repr(transparent)]
#[pyclass(name = "HashTrieSet", module = "rpds", frozen)]
struct HashTrieSetPy {
inner: HashTrieSetSync,
}
impl<'source> FromPyObject<'source> for HashTrieSetPy {
fn extract(ob: &'source PyAny) -> PyResult {
let mut ret = HashTrieSet::new_sync();
for each in ob.iter()? {
let k: Key = each?.extract()?;
ret.insert_mut(k);
}
Ok(HashTrieSetPy { inner: ret })
}
}
fn is_subset(one: &HashTrieSetSync, two: &HashTrieSetSync) -> bool {
one.iter().all(|v| two.contains(v))
}
#[pymethods]
impl HashTrieSetPy {
#[new]
fn init(value: Option) -> Self {
if let Some(value) = value {
value
} else {
HashTrieSetPy {
inner: HashTrieSet::new_sync(),
}
}
}
fn __and__(&self, other: &Self) -> Self {
self.intersection(other)
}
fn __or__(&self, other: &Self) -> Self {
self.union(other)
}
fn __sub__(&self, other: &Self) -> Self {
self.difference(other)
}
fn __xor__(&self, other: &Self) -> Self {
self.symmetric_difference(other)
}
fn __iter__(slf: PyRef<'_, Self>) -> PyResult> {
let iter = slf
.inner
.iter()
.map(|k| k.to_owned())
.collect::>()
.into_iter();
Py::new(slf.py(), KeyIterator { inner: iter })
}
fn __len__(&self) -> usize {
self.inner.size()
}
fn __repr__(&self, py: Python) -> String {
let contents = self.inner.into_iter().map(|k| {
k.clone()
.into_py(py)
.call_method0(py, "__repr__")
.and_then(|r| r.extract(py))
.unwrap_or("".to_owned())
});
format!(
"HashTrieSet({{{}}})",
contents.collect::>().join(", ")
)
}
fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> PyResult {
match op {
CompareOp::Eq => Ok((self.inner.size() == other.inner.size()
&& is_subset(&self.inner, &other.inner))
.into_py(py)),
CompareOp::Ne => Ok((self.inner.size() != other.inner.size()
|| self.inner.iter().any(|k| !other.inner.contains(k)))
.into_py(py)),
CompareOp::Lt => Ok((self.inner.size() < other.inner.size()
&& is_subset(&self.inner, &other.inner))
.into_py(py)),
CompareOp::Le => Ok(is_subset(&self.inner, &other.inner).into_py(py)),
_ => Ok(py.NotImplemented()),
}
}
fn insert(&self, value: Key) -> HashTrieSetPy {
HashTrieSetPy {
inner: self.inner.insert(value),
}
}
fn discard(&self, value: Key) -> PyResult {
match self.inner.contains(&value) {
true => Ok(HashTrieSetPy {
inner: self.inner.remove(&value),
}),
false => Ok(HashTrieSetPy {
inner: self.inner.clone(),
}),
}
}
fn remove(&self, value: Key) -> PyResult {
match self.inner.contains(&value) {
true => Ok(HashTrieSetPy {
inner: self.inner.remove(&value),
}),
false => Err(PyKeyError::new_err(value)),
}
}
fn difference(&self, other: &Self) -> HashTrieSetPy {
let mut inner = self.inner.clone();
for value in other.inner.iter() {
inner.remove_mut(value);
}
HashTrieSetPy { inner }
}
fn intersection(&self, other: &Self) -> HashTrieSetPy {
let mut inner: HashTrieSetSync = HashTrieSet::new_sync();
let larger: &HashTrieSetSync;
let iter;
if self.inner.size() > other.inner.size() {
larger = &self.inner;
iter = other.inner.iter();
} else {
larger = &other.inner;
iter = self.inner.iter();
}
for value in iter {
if larger.contains(value) {
inner.insert_mut(value.to_owned());
}
}
HashTrieSetPy { inner }
}
fn symmetric_difference(&self, other: &Self) -> HashTrieSetPy {
let mut inner: HashTrieSetSync;
let iter;
if self.inner.size() > other.inner.size() {
inner = self.inner.clone();
iter = other.inner.iter();
} else {
inner = other.inner.clone();
iter = self.inner.iter();
}
for value in iter {
if inner.contains(value) {
inner.remove_mut(value);
} else {
inner.insert_mut(value.to_owned());
}
}
HashTrieSetPy { inner }
}
fn union(&self, other: &Self) -> HashTrieSetPy {
let mut inner: HashTrieSetSync;
let iter;
if self.inner.size() > other.inner.size() {
inner = self.inner.clone();
iter = other.inner.iter();
} else {
inner = other.inner.clone();
iter = self.inner.iter();
}
for value in iter {
inner.insert_mut(value.to_owned());
}
HashTrieSetPy { inner }
}
#[pyo3(signature = (*iterables))]
fn update(&self, iterables: &PyTuple) -> PyResult {
let mut inner = self.inner.clone();
for each in iterables {
let iter = each.iter()?;
for value in iter {
inner.insert_mut(Key::extract(value?)?.to_owned());
}
}
Ok(HashTrieSetPy { inner })
}
}
#[repr(transparent)]
#[pyclass(name = "List", module = "rpds", frozen, sequence)]
struct ListPy {
inner: ListSync,
}
impl From> for ListPy {
fn from(elements: ListSync) -> Self {
ListPy { inner: elements }
}
}
impl<'source> FromPyObject<'source> for ListPy {
fn extract(ob: &'source PyAny) -> PyResult {
let mut ret = List::new_sync();
let reversed = PyModule::import(ob.py(), "builtins")?.getattr("reversed")?;
let rob: &PyIterator = reversed.call1((ob,))?.iter()?;
for each in rob {
ret.push_front_mut(each?.extract()?);
}
Ok(ListPy { inner: ret })
}
}
#[pymethods]
impl ListPy {
#[new]
#[pyo3(signature = (*elements))]
fn init(elements: &PyTuple) -> PyResult {
let mut ret: ListPy;
if elements.len() == 1 {
ret = elements.get_item(0)?.extract()?;
} else {
ret = ListPy {
inner: List::new_sync(),
};
if elements.len() > 1 {
for each in (0..elements.len()).rev() {
ret.inner
.push_front_mut(elements.get_item(each)?.extract()?);
}
}
}
Ok(ret)
}
fn __len__(&self) -> usize {
self.inner.len()
}
fn __repr__(&self, py: Python) -> String {
let contents = self.inner.into_iter().map(|k| {
k.into_py(py)
.call_method0(py, "__repr__")
.and_then(|r| r.extract(py))
.unwrap_or("".to_owned())
});
format!("List([{}])", contents.collect::>().join(", "))
}
fn __richcmp__(&self, other: &Self, op: CompareOp, py: Python<'_>) -> PyResult {
match op {
CompareOp::Eq => Ok((self.inner.len() == other.inner.len()
&& self
.inner
.iter()
.zip(other.inner.iter())
.map(|(e1, e2)| PyAny::eq(e1.extract(py)?, e2))
.all(|r| r.unwrap_or(false)))
.into_py(py)),
CompareOp::Ne => Ok((self.inner.len() != other.inner.len()
|| self
.inner
.iter()
.zip(other.inner.iter())
.map(|(e1, e2)| PyAny::ne(e1.extract(py)?, e2))
.any(|r| r.unwrap_or(true)))
.into_py(py)),
_ => Ok(py.NotImplemented()),
}
}
fn __iter__(slf: PyRef<'_, Self>) -> PyResult> {
let iter = slf
.inner
.iter()
.map(|k| k.to_owned())
.collect::>()
.into_iter();
Py::new(slf.py(), ListIterator { inner: iter })
}
fn __reversed__(&self) -> ListPy {
ListPy {
inner: self.inner.reverse(),
}
}
#[getter]
fn first(&self) -> PyResult<&PyObject> {
self.inner
.first()
.ok_or_else(|| PyIndexError::new_err("empty list has no first element"))
}
#[getter]
fn rest(&self) -> ListPy {
let mut inner = self.inner.clone();
inner.drop_first_mut();
ListPy { inner }
}
fn push_front(&self, other: PyObject) -> ListPy {
ListPy {
inner: self.inner.push_front(other),
}
}
fn drop_first(&self) -> PyResult {
if let Some(inner) = self.inner.drop_first() {
Ok(ListPy { inner })
} else {
Err(PyIndexError::new_err("empty list has no first element"))
}
}
}
#[pyclass(module = "rpds")]
struct ListIterator {
inner: IntoIter,
}
#[pymethods]
impl ListIterator {
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
slf
}
fn __next__(mut slf: PyRefMut<'_, Self>) -> Option {
slf.inner.next()
}
}
#[pymodule]
#[pyo3(name = "rpds")]
fn rpds_py(py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::()?;
PyMapping::register::(py)?;
m.add_class::()?;
m.add_class::()?;
Ok(())
}
rpds_py-0.12.0/tests/requirements.in 0100644 0001751 0000177 00000000007 14521272702 0015652 0 ustar 0000000 0000000 pytest
rpds_py-0.12.0/tests/requirements.txt 0100644 0001751 0000177 00000000431 14521272702 0016064 0 ustar 0000000 0000000 #
# This file is autogenerated by pip-compile with Python 3.11
# by the following command:
#
# pip-compile tests/requirements.in
#
iniconfig==2.0.0
# via pytest
packaging==23.1
# via pytest
pluggy==1.3.0
# via pytest
pytest==7.4.2
# via -r tests/requirements.in
rpds_py-0.12.0/tests/test_hash_trie_map.py 0100644 0001751 0000177 00000020211 14521272702 0017012 0 ustar 0000000 0000000 """
Modified from the pyrsistent test suite.
Pre-modification, these were MIT licensed, and are copyright:
Copyright (c) 2022 Tobias Gustafsson
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
"""
from collections.abc import Hashable, Mapping
import pytest
from rpds import HashTrieMap
HASH_MSG = "Not sure HashTrieMap implements Hash, it has mutable methods"
@pytest.mark.xfail(reason=HASH_MSG)
def test_instance_of_hashable():
assert isinstance(HashTrieMap(), Hashable)
def test_instance_of_map():
assert isinstance(HashTrieMap(), Mapping)
def test_literalish_works():
assert HashTrieMap() == HashTrieMap()
assert HashTrieMap(a=1, b=2) == HashTrieMap({"a": 1, "b": 2})
def test_empty_initialization():
a_map = HashTrieMap()
assert len(a_map) == 0
def test_initialization_with_one_element():
the_map = HashTrieMap({"a": 2})
assert len(the_map) == 1
assert the_map["a"] == 2
assert "a" in the_map
empty_map = the_map.remove("a")
assert len(empty_map) == 0
assert "a" not in empty_map
def test_get_non_existing_raises_key_error():
m1 = HashTrieMap()
with pytest.raises(KeyError) as error:
m1["foo"]
assert str(error.value) == "'foo'"
def test_remove_non_existing_element_raises_key_error():
m1 = HashTrieMap(a=1)
with pytest.raises(KeyError) as error:
m1.remove("b")
assert str(error.value) == "'b'"
def test_various_iterations():
assert {"a", "b"} == set(HashTrieMap(a=1, b=2))
assert ["a", "b"] == sorted(HashTrieMap(a=1, b=2).keys())
assert [1, 2] == sorted(HashTrieMap(a=1, b=2).values())
assert {("a", 1), ("b", 2)} == set(HashTrieMap(a=1, b=2).items())
pm = HashTrieMap({k: k for k in range(100)})
assert len(pm) == len(pm.keys())
assert len(pm) == len(pm.values())
assert len(pm) == len(pm.items())
ks = pm.keys()
assert all(k in pm for k in ks)
assert all(k in ks for k in ks)
us = pm.items()
assert all(pm[k] == v for (k, v) in us)
vs = pm.values()
assert all(v in vs for v in vs)
def test_initialization_with_two_elements():
map1 = HashTrieMap({"a": 2, "b": 3})
assert len(map1) == 2
assert map1["a"] == 2
assert map1["b"] == 3
map2 = map1.remove("a")
assert "a" not in map2
assert map2["b"] == 3
def test_initialization_with_many_elements():
init_dict = {str(x): x for x in range(1700)}
the_map = HashTrieMap(init_dict)
assert len(the_map) == 1700
assert the_map["16"] == 16
assert the_map["1699"] == 1699
assert the_map.insert("256", 256) == the_map
new_map = the_map.remove("1600")
assert len(new_map) == 1699
assert "1600" not in new_map
assert new_map["1601"] == 1601
# Some NOP properties
assert new_map.discard("18888") == new_map
assert "19999" not in new_map
assert new_map["1500"] == 1500
assert new_map.insert("1500", new_map["1500"]) == new_map
def test_access_non_existing_element():
map1 = HashTrieMap()
assert len(map1) == 0
map2 = map1.insert("1", 1)
assert "1" not in map1
assert map2["1"] == 1
assert "2" not in map2
def test_overwrite_existing_element():
map1 = HashTrieMap({"a": 2})
map2 = map1.insert("a", 3)
assert len(map2) == 1
assert map2["a"] == 3
@pytest.mark.xfail(reason=HASH_MSG)
def test_hash():
x = HashTrieMap(a=1, b=2, c=3)
y = HashTrieMap(a=1, b=2, c=3)
assert hash(x) == hash(y)
def test_same_hash_when_content_the_same_but_underlying_vector_size_differs():
x = HashTrieMap({x: x for x in range(1000)})
y = HashTrieMap({10: 10, 200: 200, 700: 700})
for z in x:
if z not in y:
x = x.remove(z)
assert x == y
# assert hash(x) == hash(y)
class HashabilityControlled:
hashable = True
def __hash__(self):
if self.hashable:
return 4 # Proven random
raise ValueError("I am not currently hashable.")
@pytest.mark.xfail(reason=HASH_MSG)
def test_map_does_not_hash_values_on_second_hash_invocation():
hashable = HashabilityControlled()
x = HashTrieMap(dict(el=hashable))
hash(x)
hashable.hashable = False
hash(x)
def test_equal():
x = HashTrieMap(a=1, b=2, c=3)
y = HashTrieMap(a=1, b=2, c=3)
assert x == y
assert not (x != y)
assert y == x
assert not (y != x)
def test_equal_with_different_insertion_order():
x = HashTrieMap([(i, i) for i in range(50)])
y = HashTrieMap([(i, i) for i in range(49, -1, -1)])
assert x == y
assert not (x != y)
assert y == x
assert not (y != x)
def test_not_equal():
x = HashTrieMap(a=1, b=2, c=3)
y = HashTrieMap(a=1, b=2)
assert x != y
assert not (x == y)
assert y != x
assert not (y == x)
def test_not_equal_to_dict():
x = HashTrieMap(a=1, b=2, c=3)
y = dict(a=1, b=2, d=4)
assert x != y
assert not (x == y)
assert y != x
assert not (y == x)
def test_update_with_multiple_arguments():
# If same value is present in multiple sources, the rightmost is used.
x = HashTrieMap(a=1, b=2, c=3)
y = x.update(HashTrieMap(b=4, c=5), {"c": 6})
assert y == HashTrieMap(a=1, b=4, c=6)
def test_update_one_argument():
x = HashTrieMap(a=1)
assert x.update({"b": 2}) == HashTrieMap(a=1, b=2)
def test_update_no_arguments():
x = HashTrieMap(a=1)
assert x.update() == x
class HashDummy:
def __hash__(self):
return 6528039219058920 # Hash of '33'
def __eq__(self, other):
return self is other
def test_iteration_with_many_elements():
values = list(range(0, 2000))
keys = [str(x) for x in values]
init_dict = dict(zip(keys, values))
hash_dummy1 = HashDummy()
hash_dummy2 = HashDummy()
# Throw in a couple of hash collision nodes to tests
# those properly as well
init_dict[hash_dummy1] = 12345
init_dict[hash_dummy2] = 54321
a_map = HashTrieMap(init_dict)
actual_values = set()
actual_keys = set()
for k, v in a_map.items():
actual_values.add(v)
actual_keys.add(k)
assert actual_keys == set(keys + [hash_dummy1, hash_dummy2])
assert actual_values == set(values + [12345, 54321])
def test_str():
s = str(HashTrieMap({1: 2, 3: 4}))
assert s == "HashTrieMap({1: 2, 3: 4})" or s == "HashTrieMap({3: 4, 1: 2})"
def test_empty_truthiness():
assert HashTrieMap(a=1)
assert not HashTrieMap()
def test_iterable():
m = HashTrieMap((i, i * 2) for i in range(3))
assert m == HashTrieMap({0: 0, 1: 2, 2: 4})
def test_convert_hashtriemap():
m = HashTrieMap({i: i * 2 for i in range(3)})
assert HashTrieMap.convert({i: i * 2 for i in range(3)}) == m
def test_fast_convert_hashtriemap():
m = HashTrieMap({i: i * 2 for i in range(3)})
assert HashTrieMap.convert(m) is m
def test_more_eq():
# Non-pyrsistent-test-suite test
o = object()
assert HashTrieMap([(o, o), (1, o)]) == HashTrieMap([(o, o), (1, o)])
assert HashTrieMap([(o, "foo")]) == HashTrieMap([(o, "foo")])
assert HashTrieMap() == HashTrieMap([])
assert HashTrieMap({1: 2}) != HashTrieMap({1: 3})
assert HashTrieMap({o: 1}) != HashTrieMap({o: o})
assert HashTrieMap([]) != HashTrieMap([(o, 1)])
rpds_py-0.12.0/tests/test_hash_trie_set.py 0100644 0001751 0000177 00000011463 14521272702 0017041 0 ustar 0000000 0000000 """
Modified from the pyrsistent test suite.
Pre-modification, these were MIT licensed, and are copyright:
Copyright (c) 2022 Tobias Gustafsson
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
"""
import pytest
from rpds import HashTrieSet
HASH_MSG = "Not sure HashTrieSet implements Hash, it has mutable methods"
def test_key_is_tuple():
with pytest.raises(KeyError):
HashTrieSet().remove((1, 1))
def test_key_is_not_tuple():
with pytest.raises(KeyError):
HashTrieSet().remove("asdf")
@pytest.mark.xfail(reason=HASH_MSG)
def test_supports_hash():
assert hash(HashTrieSet((1, 2))) == hash(HashTrieSet(1, 2))
def test_empty_truthiness():
assert HashTrieSet([1])
assert not HashTrieSet()
def test_contains_elements_that_it_was_initialized_with():
initial = [1, 2, 3]
s = HashTrieSet(initial)
assert set(s) == set(initial)
assert len(s) == len(set(initial))
def test_is_immutable():
s1 = HashTrieSet([1])
s2 = s1.insert(2)
assert s1 == HashTrieSet([1])
assert s2 == HashTrieSet([1, 2])
s3 = s2.remove(1)
assert s2 == HashTrieSet([1, 2])
assert s3 == HashTrieSet([2])
def test_remove_when_not_present():
s1 = HashTrieSet([1, 2, 3])
with pytest.raises(KeyError):
s1.remove(4)
def test_discard():
s1 = HashTrieSet((1, 2, 3))
assert s1.discard(3) == HashTrieSet((1, 2))
assert s1.discard(4) == s1
def test_is_iterable():
assert sum(HashTrieSet([1, 2, 3])) == 6
def test_contains():
s = HashTrieSet([1, 2, 3])
assert 2 in s
assert 4 not in s
def test_supports_set_operations():
s1 = HashTrieSet([1, 2, 3])
s2 = HashTrieSet([3, 4, 5])
assert s1 | s2 == HashTrieSet([1, 2, 3, 4, 5])
assert s1.union(s2) == s1 | s2
assert s1 & s2 == HashTrieSet([3])
assert s1.intersection(s2) == s1 & s2
assert s1 - s2 == HashTrieSet([1, 2])
assert s1.difference(s2) == s1 - s2
assert s1 ^ s2 == HashTrieSet([1, 2, 4, 5])
assert s1.symmetric_difference(s2) == s1 ^ s2
def test_supports_set_comparisons():
s1 = HashTrieSet([1, 2, 3])
s3 = HashTrieSet([1, 2])
s4 = HashTrieSet([1, 2, 3])
assert HashTrieSet([1, 2, 3, 3, 5]) == HashTrieSet([1, 2, 3, 5])
assert s1 != s3
assert s3 < s1
assert s3 <= s1
assert s3 <= s4
assert s1 > s3
assert s1 >= s3
assert s4 >= s3
def test_repr():
rep = repr(HashTrieSet([1, 2]))
assert rep == "HashTrieSet({1, 2})" or rep == "HashTrieSet({2, 1})"
rep = repr(HashTrieSet(["1", "2"]))
assert rep == "HashTrieSet({'1', '2'})" or rep == "HashTrieSet({'2', '1'})"
def test_update():
assert HashTrieSet([1, 2, 3]).update([3, 4, 4, 5]) == HashTrieSet(
[1, 2, 3, 4, 5]
)
def test_update_no_elements():
s1 = HashTrieSet([1, 2])
assert s1.update([]) == s1
def test_iterable():
assert HashTrieSet(iter("a")) == HashTrieSet(iter("a"))
def test_more_eq():
# Non-pyrsistent-test-suite test
o = object()
assert HashTrieSet([o]) == HashTrieSet([o])
assert HashTrieSet([o, o]) == HashTrieSet([o, o])
assert HashTrieSet([o]) == HashTrieSet([o, o])
assert HashTrieSet() == HashTrieSet([])
assert not (HashTrieSet([1, 2]) == HashTrieSet([1, 3]))
assert not (HashTrieSet([o, 1]) == HashTrieSet([o, o]))
assert not (HashTrieSet([]) == HashTrieSet([o]))
assert HashTrieSet([1, 2]) != HashTrieSet([1, 3])
assert HashTrieSet([]) != HashTrieSet([o])
assert not (HashTrieSet([o]) != HashTrieSet([o]))
assert not (HashTrieSet([o, o]) != HashTrieSet([o, o]))
assert not (HashTrieSet([o]) != HashTrieSet([o, o]))
assert not (HashTrieSet() != HashTrieSet([]))
def test_more_set_comparisons():
s = HashTrieSet([1, 2, 3])
assert s == s
assert not (s < s)
assert s <= s
assert not (s > s)
assert s >= s
rpds_py-0.12.0/tests/test_list.py 0100644 0001751 0000177 00000007133 14521272702 0015172 0 ustar 0000000 0000000 """
Modified from the pyrsistent test suite.
Pre-modification, these were MIT licensed, and are copyright:
Copyright (c) 2022 Tobias Gustafsson
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
"""
import pytest
from rpds import List
HASH_MSG = "Not sure List implements Hash, it has mutable methods"
def test_literalish_works():
assert List(1, 2, 3) == List([1, 2, 3])
def test_first_and_rest():
pl = List([1, 2])
assert pl.first == 1
assert pl.rest.first == 2
assert pl.rest.rest == List()
def test_instantiate_large_list():
assert List(range(1000)).first == 0
def test_iteration():
assert list(List()) == []
assert list(List([1, 2, 3])) == [1, 2, 3]
def test_push_front():
assert List([1, 2, 3]).push_front(0) == List([0, 1, 2, 3])
def test_push_front_empty_list():
assert List().push_front(0) == List([0])
def test_truthiness():
assert List([1])
assert not List()
def test_len():
assert len(List([1, 2, 3])) == 3
assert len(List()) == 0
def test_first_illegal_on_empty_list():
with pytest.raises(IndexError):
List().first
def test_rest_return_self_on_empty_list():
assert List().rest == List()
def test_reverse():
assert reversed(List([1, 2, 3])) == List([3, 2, 1])
assert reversed(List()) == List()
def test_inequality():
assert List([1, 2]) != List([1, 3])
assert List([1, 2]) != List([1, 2, 3])
assert List() != List([1, 2, 3])
def test_repr():
assert str(List()) == "List([])"
assert str(List([1, 2, 3])) in "List([1, 2, 3])"
@pytest.mark.xfail(reason=HASH_MSG)
def test_hashing():
assert hash(List([1, 2])) == hash(List([1, 2]))
assert hash(List([1, 2])) != hash(List([2, 1]))
def test_sequence():
m = List("asdf")
assert m == List(["a", "s", "d", "f"])
# Non-pyrsistent-test-suite tests
def test_drop_first():
assert List([1, 2, 3]).drop_first() == List([2, 3])
def test_drop_first_empty():
"""
rpds itself returns an Option here but we try IndexError instead.
"""
with pytest.raises(IndexError):
List([]).drop_first()
def test_more_eq():
o = object()
assert List([o, o]) == List([o, o])
assert List([o]) == List([o])
assert List() == List([])
assert not (List([1, 2]) == List([1, 3]))
assert not (List([o]) == List([o, o]))
assert not (List([]) == List([o]))
assert List([1, 2]) != List([1, 3])
assert List([o]) != List([o, o])
assert List([]) != List([o])
assert not (List([o, o]) != List([o, o]))
assert not (List([o]) != List([o]))
assert not (List() != List([]))
rpds_py-0.12.0/Cargo.lock 0100644 0001751 0000177 00000020142 14521272702 0013344 0 ustar 0000000 0000000 # This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "archery"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab7d8a6d00b222909638a01ddcc8c533219e9d5bfada1613afae43481f2fc699"
dependencies = [
"static_assertions",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "indoc"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8"
[[package]]
name = "libc"
version = "0.2.147"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3"
[[package]]
name = "lock_api"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "memoffset"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
dependencies = [
"autocfg",
]
[[package]]
name = "once_cell"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
[[package]]
name = "parking_lot"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "proc-macro2"
version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pyo3"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04e8453b658fe480c3e70c8ed4e3d3ec33eb74988bd186561b0cc66b85c3bc4b"
dependencies = [
"cfg-if",
"indoc",
"libc",
"memoffset",
"parking_lot",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
"unindent",
]
[[package]]
name = "pyo3-build-config"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a96fe70b176a89cff78f2fa7b3c930081e163d5379b4dcdf993e3ae29ca662e5"
dependencies = [
"once_cell",
"target-lexicon",
]
[[package]]
name = "pyo3-ffi"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "214929900fd25e6604661ed9cf349727c8920d47deff196c4e28165a6ef2a96b"
dependencies = [
"libc",
"pyo3-build-config",
]
[[package]]
name = "pyo3-macros"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dac53072f717aa1bfa4db832b39de8c875b7c7af4f4a6fe93cdbf9264cf8383b"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn",
]
[[package]]
name = "pyo3-macros-backend"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7774b5a8282bd4f25f803b1f0d945120be959a36c72e08e7cd031c792fdfd424"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "quote"
version = "1.0.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29"
dependencies = [
"bitflags",
]
[[package]]
name = "rpds"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99334e9410cf4d16241bb88b27bc282e140327a4c4759be76f8a96e6d0cd0f35"
dependencies = [
"archery",
]
[[package]]
name = "rpds-py"
version = "0.12.0"
dependencies = [
"archery",
"pyo3",
"rpds",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "smallvec"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9"
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
version = "2.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "239814284fd6f1a4ffe4ca893952cdd93c224b6a1571c9a9eadd670295c0c9e2"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "target-lexicon"
version = "0.12.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8e77cb757a61f51b947ec4a7e3646efd825b73561db1c232a8ccb639e611a0"
[[package]]
name = "unicode-ident"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
[[package]]
name = "unindent"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
[[package]]
name = "windows-targets"
version = "0.48.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
rpds_py-0.12.0/pyproject.toml 0100644 0001751 0000177 00000002572 14521272702 0014362 0 ustar 0000000 0000000 [build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
[project]
name = "rpds-py"
description = "Python bindings to Rust's persistent data structures (rpds)"
readme = "README.rst"
license = {text = "MIT"}
requires-python = ">=3.8"
keywords = ["data structures", "rust", "persistent"]
authors = [
{email = "Julian+rpds@GrayVines.com"},
{name = "Julian Berman"},
]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Rust",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
]
dynamic = ["version"]
[project.urls]
Homepage = "https://github.com/crate-py/rpds"
Issues = "https://github.com/crate-py/rpds/issues/"
Funding = "https://github.com/sponsors/Julian"
Source = "https://github.com/crate-py/rpds"
[tool.isort]
combine_as_imports = true
from_first = true
include_trailing_comma = true
multi_line_output = 3
known_first_party = ["rpds"]
[tool.maturin]
features = ["pyo3/extension-module"]
rpds_py-0.12.0/PKG-INFO 0000644 00000007243 00000000000 0007664 ustar Metadata-Version: 2.1
Name: rpds-py
Version: 0.12.0
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
License-File: LICENSE
Summary: Python bindings to Rust's persistent data structures (rpds)
Keywords: data structures,rust,persistent
Author: Julian Berman
Author-email: Julian+rpds@GrayVines.com
License: MIT
Requires-Python: >=3.8
Description-Content-Type: text/x-rst; charset=UTF-8
Project-URL: Homepage, https://github.com/crate-py/rpds
Project-URL: Issues, https://github.com/crate-py/rpds/issues/
Project-URL: Funding, https://github.com/sponsors/Julian
Project-URL: Source, https://github.com/crate-py/rpds
===========
``rpds.py``
===========
|PyPI| |Pythons| |CI|
.. |PyPI| image:: https://img.shields.io/pypi/v/rpds-py.svg
:alt: PyPI version
:target: https://pypi.org/project/rpds-py/
.. |Pythons| image:: https://img.shields.io/pypi/pyversions/rpds-py.svg
:alt: Supported Python versions
:target: https://pypi.org/project/rpds-py/
.. |CI| image:: https://github.com/crate-py/rpds/workflows/CI/badge.svg
:alt: Build status
:target: https://github.com/crate-py/rpds/actions?query=workflow%3ACI
Python bindings to the `Rust rpds crate `_ for persistent data structures.
What's here is quite minimal (in transparency, it was written initially to support replacing ``pyrsistent`` in the `referencing library `_).
If you see something missing (which is very likely), a PR is definitely welcome to add it.
Installation
------------
The distribution on PyPI is named ``rpds.py`` (equivalently ``rpds-py``), and thus can be installed via e.g.:
.. code:: sh
$ pip install rpds-py
Note that if you install ``rpds-py`` from source, you will need a Rust toolchain installed, as it is a build-time dependency.
An example of how to do so in a ``Dockerfile`` can be found `here `_.
If you believe you are on a common platform which should have wheels built (i.e. and not need to compile from source), feel free to file an issue or pull request modifying the GitHub action used here to build wheels via ``maturin``.
Usage
-----
Methods in general are named similarly to their ``rpds`` counterparts (rather than ``pyrsistent``\ 's conventions, though probably a full drop-in ``pyrsistent``\ -compatible wrapper module is a good addition at some point).
.. code:: python
>>> from rpds import HashTrieMap, HashTrieSet, List
>>> m = HashTrieMap({"foo": "bar", "baz": "quux"})
>>> m.insert("spam", 37) == HashTrieMap({"foo": "bar", "baz": "quux", "spam": 37})
True
>>> m.remove("foo") == HashTrieMap({"baz": "quux"})
True
>>> s = HashTrieSet({"foo", "bar", "baz", "quux"})
>>> s.insert("spam") == HashTrieSet({"foo", "bar", "baz", "quux", "spam"})
True
>>> s.remove("foo") == HashTrieSet({"bar", "baz", "quux"})
True
>>> L = List([1, 3, 5])
>>> L.push_front(-1) == List([-1, 1, 3, 5])
True
>>> L.rest == List([3, 5])
True