Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 0 additions & 27 deletions .circleci/config.yml

This file was deleted.

23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: CI

on: [push, pull_request]

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]

steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install uv
run: curl -LsSf https://astral.sh/uv/install.sh | sh
- name: Run tests
run: |
source ~/.cargo/env
make test
56 changes: 56 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
name: Upload Python Package

on:
release:
types: [published]

permissions:
contents: read

jobs:
release-build:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

# Installs the official uv GitHub Action
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true

- name: Build release distributions
run: uv build

- name: Upload distributions
uses: actions/upload-artifact@v4
with:
name: release-dists
path: dist/

pypi-publish:
runs-on: ubuntu-latest
needs: release-build
permissions:
# IMPORTANT: this permission is mandatory for Trusted Publishing (OIDC)
id-token: write

environment:
name: pypi
# OPTIONAL: uncomment and update to include your PyPI project URL in the deployment status:
# url: https://pypi.org/p/YOURPROJECT

steps:
- name: Retrieve release distributions
uses: actions/download-artifact@v4
with:
name: release-dists
path: dist/

# uv needs to be available in this job as well to run `uv publish`
- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Publish release distributions to PyPI
run: uv publish
60 changes: 0 additions & 60 deletions .pre-commit-config.yaml

This file was deleted.

21 changes: 11 additions & 10 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
.PHONY: dev
PHONY: dev
dev:
tox -e install-hooks
uvx prek install

.PHONY: lint
lint:
uvx prek --all-files

.PHONY: test
test:
tox -e unit
uv run pytest --doctest-modules --doctest-glob="*.md"
uv run pyright

.PHONY: release
release: clean
python setup.py sdist bdist_wheel
echo "Checking dist:"
twine check dist/*
# "Are you sure you want to publish the ^ release? Press any key to continue."
read
twine upload dist/*
uv build
uv publish

.PHONY: clean
clean:
rm -rf build dist .coverage .mypy_cache .pytest_cache __pycache__ .tox
rm -rf build dist .coverage .mypy_cache .pytest_cache __pycache__ .tox .venv .git/hooks/pre-commit
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@

## Prerequisites

The only requirement is using **Python 3.8+**. You can verify this by running:
The only requirement is using **Python 3.10+**. You can verify this by running:

```bash
$ python --version
Python 3.8.18
Python 3.10.18
```

## Installation
Expand All @@ -27,7 +27,7 @@ You can verify you're running the latest package version by running:
```python
>>> import disjoint_set
>>> disjoint_set.__version__
'0.8.0'
'0.9.0'

```

Expand Down
5 changes: 2 additions & 3 deletions disjoint_set/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from .main import DisjointSet
from .main import InvalidInitialMappingError
from .main import DisjointSet, InvalidInitialMappingError

name = "disjoint_set"
__all__ = ["DisjointSet", "InvalidInitialMappingError"]
__version__ = "0.8.0"
__version__ = "0.9.0"
37 changes: 23 additions & 14 deletions disjoint_set/main.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
from __future__ import annotations

import sys
from collections import defaultdict
from typing import Any
from typing import DefaultDict
from typing import Generic
from typing import Iterable
from typing import Iterator
from typing import TypeVar
from collections.abc import Iterable, Iterator
from typing import Any, Generic, TypeVar

if sys.version_info >= (3, 12):
from typing import override
else:
from typing_extensions import override

from disjoint_set.utils import IdentityDict

Expand All @@ -18,20 +20,20 @@ class InvalidInitialMappingError(RuntimeError):

def __init__(
self,
msg=(
msg: str = (
"The mapping passed during ther DisjointSet initialization must have been wrong. "
"Check that all keys are mapping to other keys and not some external values."
),
*args,
**kwargs,
*args: Any,
**kwargs: Any,
):
super().__init__(msg, *args, **kwargs)


class DisjointSet(Generic[T]):
"""A disjoint set data structure."""

def __init__(self, *args, **kwargs) -> None:
def __init__(self, *args: Any, **kwargs: Any) -> None:
"""
Disjoint set data structure.

Expand Down Expand Up @@ -65,7 +67,8 @@ def __bool__(self) -> bool:
def __getitem__(self, element: T) -> T:
return self.find(element)

def __eq__(self, other: Any) -> bool:
@override
def __eq__(self, other: object) -> bool:
"""
Return True if both DistjoinSet structures are equivalent.

Expand All @@ -76,8 +79,11 @@ def __eq__(self, other: Any) -> bool:
if not isinstance(other, DisjointSet):
return False

return {tuple(x) for x in self.itersets()} == {tuple(x) for x in other.itersets()}
return {tuple(x) for x in self.itersets()} == {
tuple(x) for x in other.itersets()
}

@override
def __repr__(self) -> str:
"""
Print self in a reproducible way.
Expand All @@ -88,6 +94,7 @@ def __repr__(self) -> str:
sets = {key: val for key, val in self}
return f"{self.__class__.__name__}({sets})"

@override
def __str__(self) -> str:
return "{classname}({values})".format(
classname=self.__class__.__name__,
Expand All @@ -102,7 +109,9 @@ def __iter__(self) -> Iterator[tuple[T, T]]:
except RuntimeError as e:
raise InvalidInitialMappingError() from e

def itersets(self, with_canonical_elements: bool = False) -> Iterator[set[T] | tuple[T, set[T]]]:
def itersets(
self, with_canonical_elements: bool = False
) -> Iterator[set[T] | tuple[T, set[T]]]:
"""
Yield sets of connected components.

Expand All @@ -114,7 +123,7 @@ def itersets(self, with_canonical_elements: bool = False) -> Iterator[set[T] | t
>>> list(ds.itersets(with_canonical_elements=True))
[(2, {1, 2})]
"""
element_classes: DefaultDict[T, set[T]] = defaultdict(set)
element_classes: defaultdict[T, set[T]] = defaultdict(set)
for element in self._data:
element_classes[self.find(element)].add(element)

Expand Down
7 changes: 3 additions & 4 deletions disjoint_set/utils.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
from typing import Dict
from typing import TypeVar
from typing import Any, TypeVar

T = TypeVar("T")


class IdentityDict(Dict[T, T]):
class IdentityDict(dict[T, T]):
"""A defaultdict implementation which places the requested key as its value in case it's missing."""

def __init__(self, *args, **kwargs):
def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)

def __missing__(self, key: T) -> T:
Expand Down
Loading