ucon 0.5.1__tar.gz → 0.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ucon-0.5.1 → ucon-0.6.0}/.github/workflows/publish.yaml +5 -5
- {ucon-0.5.1 → ucon-0.6.0}/.github/workflows/tests.yaml +4 -6
- ucon-0.6.0/Makefile +129 -0
- {ucon-0.5.1/ucon.egg-info → ucon-0.6.0}/PKG-INFO +88 -31
- ucon-0.5.1/PKG-INFO → ucon-0.6.0/README.md +75 -58
- {ucon-0.5.1 → ucon-0.6.0}/ROADMAP.md +51 -36
- ucon-0.5.1/docs/decisions/unity-distance-metric-for-nearest-scale.md → ucon-0.6.0/docs/decisions/001-unity-distance-metric-for-nearest-scale.md +35 -17
- ucon-0.6.0/docs/decisions/002-composite-units.md +116 -0
- ucon-0.6.0/docs/decisions/003-composable-unit-algebra.md +123 -0
- ucon-0.5.1/docs/decisions/unit-algebra-naming.md → ucon-0.6.0/docs/decisions/004-unit-algebra-naming.md +49 -48
- ucon-0.5.1/docs/decisions/pseudo-dimension-tuple-values.md → ucon-0.6.0/docs/decisions/005-pseudo-dimension-tuple-values.md +39 -75
- ucon-0.6.0/docs/decisions/006-pydantic-integration-pattern.md +92 -0
- ucon-0.6.0/docs/examples/basis-transform-fantasy-units.md +89 -0
- ucon-0.6.0/pyproject.toml +86 -0
- ucon-0.6.0/requirements.txt +1 -0
- {ucon-0.5.1 → ucon-0.6.0}/setup.py +9 -0
- ucon-0.6.0/tests/ucon/mcp/__init__.py +1 -0
- ucon-0.6.0/tests/ucon/mcp/test_server.py +351 -0
- ucon-0.6.0/tests/ucon/test_basis_transform.py +521 -0
- ucon-0.6.0/tests/ucon/test_graph_basis_transform.py +263 -0
- ucon-0.6.0/tests/ucon/test_pickle.py +197 -0
- ucon-0.6.0/tests/ucon/test_pydantic.py +350 -0
- ucon-0.6.0/tests/ucon/test_rebased_unit.py +184 -0
- ucon-0.6.0/tests/ucon/test_unit_parsing.py +290 -0
- ucon-0.6.0/tests/ucon/test_unit_system.py +174 -0
- ucon-0.6.0/tests/ucon/test_vector_fraction.py +185 -0
- {ucon-0.5.1 → ucon-0.6.0}/ucon/__init__.py +24 -3
- {ucon-0.5.1 → ucon-0.6.0}/ucon/algebra.py +36 -14
- {ucon-0.5.1 → ucon-0.6.0}/ucon/core.py +414 -2
- {ucon-0.5.1 → ucon-0.6.0}/ucon/graph.py +167 -10
- ucon-0.6.0/ucon/mcp/__init__.py +8 -0
- ucon-0.6.0/ucon/mcp/server.py +250 -0
- ucon-0.6.0/ucon/pydantic.py +199 -0
- ucon-0.6.0/ucon/units.py +434 -0
- ucon-0.5.1/README.md → ucon-0.6.0/ucon.egg-info/PKG-INFO +115 -21
- {ucon-0.5.1 → ucon-0.6.0}/ucon.egg-info/SOURCES.txt +26 -7
- ucon-0.6.0/ucon.egg-info/entry_points.txt +2 -0
- ucon-0.6.0/ucon.egg-info/requires.txt +11 -0
- ucon-0.6.0/ucon.egg-info/top_level.txt +1 -0
- ucon-0.6.0/uv.lock +1603 -0
- ucon-0.5.1/docs/decisions/composable-unit-algebra.md +0 -241
- ucon-0.5.1/docs/decisions/composite-units.md +0 -160
- ucon-0.5.1/noxfile.py +0 -132
- ucon-0.5.1/requirements.txt +0 -2
- ucon-0.5.1/ucon/units.py +0 -159
- ucon-0.5.1/ucon.egg-info/top_level.txt +0 -2
- {ucon-0.5.1 → ucon-0.6.0}/.gitignore +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/LICENSE +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/NOTICE +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/docs/explainers/exponent-scale-relationship.md +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/docs/explainers/type-operation-matrix.md +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/docs/explainers/why-algebraic-closure-matters.md +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/docs/explainers/why-type-safety-matters.md +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/docs/proposals/interface-unifying-the-value-layer.md +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/docs/proposals/project_unified-algebraic-core.md +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/docs/proposals/support-for-fractional-exponents.md +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/docs/proposals/unified-unit-presentation.md +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/setup.cfg +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/tests/__init__.py +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/__init__.py +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/conversion/__init__.py +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/conversion/test_graph.py +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/conversion/test_map.py +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/test_algebra.py +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/test_core.py +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/test_default_graph_conversions.py +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/test_dimensionless_units.py +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/test_quantity.py +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/test_uncertainty.py +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/tests/ucon/test_units.py +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/ucon/maps.py +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/ucon/quantity.py +0 -0
- {ucon-0.5.1 → ucon-0.6.0}/ucon.egg-info/dependency_links.txt +0 -0
|
@@ -16,16 +16,16 @@ jobs:
|
|
|
16
16
|
fetch-depth: 0 # needed for ancestry check
|
|
17
17
|
ref: ${{ github.ref_name }}
|
|
18
18
|
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v5
|
|
21
|
+
|
|
19
22
|
- name: Set up Python
|
|
20
23
|
uses: actions/setup-python@v5
|
|
21
24
|
with:
|
|
22
25
|
python-version: '3.14'
|
|
23
26
|
|
|
24
|
-
- name: Install dependencies
|
|
25
|
-
run: pip install -r requirements.txt
|
|
26
|
-
|
|
27
27
|
- name: Build distribution 📦
|
|
28
|
-
run:
|
|
28
|
+
run: LOCAL_VERSION_SCHEME=true make build
|
|
29
29
|
|
|
30
30
|
- name: Publish to Test PyPI
|
|
31
31
|
if: github.ref == 'refs/heads/main'
|
|
@@ -39,7 +39,7 @@ jobs:
|
|
|
39
39
|
- name: Verify tag is on mainline
|
|
40
40
|
if: startsWith(github.ref, 'refs/tags/')
|
|
41
41
|
run: |
|
|
42
|
-
git fetch origin main --
|
|
42
|
+
git fetch origin main --unshallow 2>/dev/null || git fetch origin main
|
|
43
43
|
TAG=${GITHUB_REF#refs/tags/}
|
|
44
44
|
TAG_COMMIT=$(git rev-list -n 1 "$TAG")
|
|
45
45
|
if ! git merge-base --is-ancestor "$TAG_COMMIT" origin/main; then
|
|
@@ -26,18 +26,16 @@ jobs:
|
|
|
26
26
|
- name: Checkout code
|
|
27
27
|
uses: actions/checkout@v5
|
|
28
28
|
|
|
29
|
+
- name: Install uv
|
|
30
|
+
uses: astral-sh/setup-uv@v5
|
|
31
|
+
|
|
29
32
|
- name: Set up Python ${{ matrix.python-version }}
|
|
30
33
|
uses: actions/setup-python@v6
|
|
31
34
|
with:
|
|
32
35
|
python-version: ${{ matrix.python-version }}
|
|
33
36
|
|
|
34
|
-
- name: Install dependencies
|
|
35
|
-
run: |
|
|
36
|
-
python -m pip install --upgrade pip
|
|
37
|
-
pip install -r requirements.txt
|
|
38
|
-
|
|
39
37
|
- name: Run tests
|
|
40
|
-
run:
|
|
38
|
+
run: make test PYTHON=${{ matrix.python-version }}
|
|
41
39
|
|
|
42
40
|
- name: Upload coverage to Codecov
|
|
43
41
|
if: success() && github.repository_owner == 'withtwoemms'
|
ucon-0.6.0/Makefile
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# © 2025 The Radiativity Company
|
|
2
|
+
# Licensed under the Apache License, Version 2.0
|
|
3
|
+
# See the LICENSE file for details.
|
|
4
|
+
|
|
5
|
+
# --- Configuration ---
|
|
6
|
+
PROJECTNAME := ucon
|
|
7
|
+
PYTHON ?= 3.12
|
|
8
|
+
SUPPORTED_PYTHONS := 3.7 3.8 3.9 3.10 3.11 3.12 3.13 3.14
|
|
9
|
+
UV_VENV ?= .${PROJECTNAME}-${PYTHON}
|
|
10
|
+
UV_INSTALLED := .uv-installed
|
|
11
|
+
DEPS_INSTALLED := ${UV_VENV}/.deps-installed
|
|
12
|
+
TESTDIR := tests/
|
|
13
|
+
TESTNAME ?=
|
|
14
|
+
COVERAGE ?= true
|
|
15
|
+
|
|
16
|
+
# --- Color Setup ---
|
|
17
|
+
GREEN := \033[0;32m
|
|
18
|
+
CYAN := \033[0;36m
|
|
19
|
+
YELLOW := \033[1;33m
|
|
20
|
+
RESET := \033[0m
|
|
21
|
+
|
|
22
|
+
# --- Help Command ---
|
|
23
|
+
.PHONY: help
|
|
24
|
+
help:
|
|
25
|
+
@echo "\n${YELLOW}ucon Development Commands${RESET}\n"
|
|
26
|
+
@echo " ${CYAN}install${RESET} - Install package with all extras"
|
|
27
|
+
@echo " ${CYAN}install-test${RESET} - Install with test dependencies only"
|
|
28
|
+
@echo " ${CYAN}test${RESET} - Run tests (PYTHON=X.Y for specific version)"
|
|
29
|
+
@echo " ${CYAN}test-all${RESET} - Run tests across all supported Python versions"
|
|
30
|
+
@echo " ${CYAN}coverage${RESET} - Generate coverage report"
|
|
31
|
+
@echo " ${CYAN}build${RESET} - Build source and wheel distributions"
|
|
32
|
+
@echo " ${CYAN}venv${RESET} - Create virtual environment"
|
|
33
|
+
@echo " ${CYAN}clean${RESET} - Remove build artifacts and caches"
|
|
34
|
+
@echo ""
|
|
35
|
+
@echo "${YELLOW}Variables:${RESET}\n"
|
|
36
|
+
@echo " PYTHON=${PYTHON} - Python version for test target"
|
|
37
|
+
@echo " UV_VENV=${UV_VENV} - Path to virtual environment"
|
|
38
|
+
@echo " TESTNAME= - Specific test to run (e.g., tests.ucon.test_core)"
|
|
39
|
+
@echo " COVERAGE=${COVERAGE} - Enable coverage (true/false)"
|
|
40
|
+
@echo ""
|
|
41
|
+
|
|
42
|
+
# --- uv Installation ---
|
|
43
|
+
${UV_INSTALLED}:
|
|
44
|
+
@command -v uv >/dev/null 2>&1 || { \
|
|
45
|
+
echo "${GREEN}Installing uv...${RESET}"; \
|
|
46
|
+
curl -LsSf https://astral.sh/uv/install.sh | sh; \
|
|
47
|
+
}
|
|
48
|
+
@touch ${UV_INSTALLED}
|
|
49
|
+
|
|
50
|
+
# --- Virtual Environment ---
|
|
51
|
+
${UV_VENV}: ${UV_INSTALLED}
|
|
52
|
+
@echo "${GREEN}Creating virtual environment at ${UV_VENV}...${RESET}"
|
|
53
|
+
@uv venv --python ${PYTHON} ${UV_VENV}
|
|
54
|
+
|
|
55
|
+
${DEPS_INSTALLED}: pyproject.toml uv.lock | ${UV_VENV}
|
|
56
|
+
@echo "${GREEN}Syncing dependencies into ${UV_VENV}...${RESET}"
|
|
57
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv sync --python ${PYTHON} --extra test --extra pydantic --extra mcp
|
|
58
|
+
@touch ${DEPS_INSTALLED}
|
|
59
|
+
|
|
60
|
+
.PHONY: venv
|
|
61
|
+
venv: ${DEPS_INSTALLED}
|
|
62
|
+
@echo "${CYAN}Virtual environment ready at ${UV_VENV}${RESET}"
|
|
63
|
+
@echo "${CYAN}Activate with:${RESET} source ${UV_VENV}/bin/activate"
|
|
64
|
+
|
|
65
|
+
# --- Installation ---
|
|
66
|
+
.PHONY: install-test
|
|
67
|
+
install-test: ${UV_VENV}
|
|
68
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv sync --python ${PYTHON} --extra test
|
|
69
|
+
|
|
70
|
+
.PHONY: install
|
|
71
|
+
install: ${UV_VENV}
|
|
72
|
+
@echo "${GREEN}Installing with all extras into ${UV_VENV}...${RESET}"
|
|
73
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv sync --python ${PYTHON} --extra test --extra pydantic --extra mcp
|
|
74
|
+
|
|
75
|
+
# --- Testing ---
|
|
76
|
+
.PHONY: test
|
|
77
|
+
test: ${DEPS_INSTALLED}
|
|
78
|
+
@echo "${GREEN}Running tests with Python ${PYTHON}...${RESET}"
|
|
79
|
+
ifeq ($(COVERAGE),true)
|
|
80
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} coverage run --source=ucon --branch \
|
|
81
|
+
--omit="**/tests/*,**/site-packages/*.py,setup.py" \
|
|
82
|
+
-m unittest $(if $(TESTNAME),$(TESTNAME),discover --start-directory ${TESTDIR} --top-level-directory .)
|
|
83
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} coverage report -m
|
|
84
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} coverage xml
|
|
85
|
+
else
|
|
86
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} -m unittest \
|
|
87
|
+
$(if $(TESTNAME),$(TESTNAME),discover --start-directory ${TESTDIR} --top-level-directory .)
|
|
88
|
+
endif
|
|
89
|
+
|
|
90
|
+
.PHONY: test-all
|
|
91
|
+
test-all: ${UV_INSTALLED}
|
|
92
|
+
@echo "${GREEN}Running tests across all supported Python versions...${RESET}"
|
|
93
|
+
@for pyver in $(SUPPORTED_PYTHONS); do \
|
|
94
|
+
echo "\n${CYAN}=== Python $$pyver ===${RESET}"; \
|
|
95
|
+
uv run --python $$pyver -m unittest discover \
|
|
96
|
+
--start-directory ${TESTDIR} --top-level-directory . \
|
|
97
|
+
|| echo "${YELLOW}Python $$pyver: FAILED or not available${RESET}"; \
|
|
98
|
+
done
|
|
99
|
+
|
|
100
|
+
# --- Coverage ---
|
|
101
|
+
.PHONY: coverage
|
|
102
|
+
coverage: ${DEPS_INSTALLED}
|
|
103
|
+
@echo "${GREEN}Generating coverage report...${RESET}"
|
|
104
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} coverage report -m
|
|
105
|
+
@UV_PROJECT_ENVIRONMENT=${UV_VENV} uv run --python ${PYTHON} coverage html
|
|
106
|
+
@echo "${CYAN}HTML report at htmlcov/index.html${RESET}"
|
|
107
|
+
|
|
108
|
+
# --- Building ---
|
|
109
|
+
.PHONY: build
|
|
110
|
+
build: ${UV_INSTALLED}
|
|
111
|
+
@echo "${GREEN}Building distributions...${RESET}"
|
|
112
|
+
@uv build
|
|
113
|
+
@echo "${CYAN}Distributions at dist/${RESET}"
|
|
114
|
+
|
|
115
|
+
# --- Cleaning ---
|
|
116
|
+
.PHONY: clean
|
|
117
|
+
clean:
|
|
118
|
+
@echo "${GREEN}Cleaning build artifacts...${RESET}"
|
|
119
|
+
@rm -rf dist/ build/ *.egg-info/
|
|
120
|
+
@rm -rf ${UV_VENV} ${DEPS_INSTALLED} ${UV_INSTALLED}
|
|
121
|
+
@rm -rf .uv_cache .pytest_cache htmlcov/
|
|
122
|
+
@rm -f coverage.xml .coverage
|
|
123
|
+
@find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
|
|
124
|
+
@echo "${CYAN}Clean complete.${RESET}"
|
|
125
|
+
|
|
126
|
+
.PHONY: clean-all
|
|
127
|
+
clean-all: clean
|
|
128
|
+
@echo "${YELLOW}Removing uv.lock...${RESET}"
|
|
129
|
+
@rm -f uv.lock
|
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ucon
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: A tool for dimensional analysis: a 'Unit CONverter'
|
|
5
5
|
Home-page: https://github.com/withtwoemms/ucon
|
|
6
6
|
Author: Emmanuel I. Obi
|
|
7
|
+
Author-email: "Emmanuel I. Obi" <withtwoemms@gmail.com>
|
|
7
8
|
Maintainer: Emmanuel I. Obi
|
|
8
|
-
Maintainer-email: withtwoemms@gmail.com
|
|
9
|
+
Maintainer-email: "Emmanuel I. Obi" <withtwoemms@gmail.com>
|
|
9
10
|
License: Apache-2.0
|
|
11
|
+
Project-URL: Homepage, https://github.com/withtwoemms/ucon
|
|
12
|
+
Project-URL: Repository, https://github.com/withtwoemms/ucon
|
|
10
13
|
Classifier: Development Status :: 4 - Beta
|
|
11
14
|
Classifier: Intended Audience :: Developers
|
|
12
15
|
Classifier: Intended Audience :: Education
|
|
13
16
|
Classifier: Intended Audience :: Science/Research
|
|
14
17
|
Classifier: Topic :: Software Development :: Build Tools
|
|
15
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
18
|
Classifier: Programming Language :: Python :: 3.7
|
|
17
19
|
Classifier: Programming Language :: Python :: 3.8
|
|
18
20
|
Classifier: Programming Language :: Python :: 3.9
|
|
@@ -21,19 +23,20 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
21
23
|
Classifier: Programming Language :: Python :: 3.12
|
|
22
24
|
Classifier: Programming Language :: Python :: 3.13
|
|
23
25
|
Classifier: Programming Language :: Python :: 3.14
|
|
26
|
+
Requires-Python: >=3.7
|
|
24
27
|
Description-Content-Type: text/markdown
|
|
25
28
|
License-File: LICENSE
|
|
26
29
|
License-File: NOTICE
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: coverage[toml]>=5.5; extra == "test"
|
|
32
|
+
Provides-Extra: pydantic
|
|
33
|
+
Requires-Dist: pydantic>=2.0; extra == "pydantic"
|
|
34
|
+
Provides-Extra: mcp
|
|
35
|
+
Requires-Dist: mcp>=1.0; python_version >= "3.10" and extra == "mcp"
|
|
27
36
|
Dynamic: author
|
|
28
|
-
Dynamic: classifier
|
|
29
|
-
Dynamic: description
|
|
30
|
-
Dynamic: description-content-type
|
|
31
37
|
Dynamic: home-page
|
|
32
|
-
Dynamic: license
|
|
33
38
|
Dynamic: license-file
|
|
34
39
|
Dynamic: maintainer
|
|
35
|
-
Dynamic: maintainer-email
|
|
36
|
-
Dynamic: summary
|
|
37
40
|
|
|
38
41
|
<table>
|
|
39
42
|
<tr>
|
|
@@ -68,6 +71,7 @@ It combines **units**, **scales**, and **dimensions** into a composable algebra
|
|
|
68
71
|
- Metric and binary prefixes (`kilo`, `kibi`, `micro`, `mebi`, etc.)
|
|
69
72
|
- Pseudo-dimensions for angles, solid angles, and ratios with semantic isolation
|
|
70
73
|
- Uncertainty propagation through arithmetic and conversions
|
|
74
|
+
- Pydantic v2 integration for API validation and JSON serialization
|
|
71
75
|
- A clean foundation for physics, chemistry, data modeling, and beyond
|
|
72
76
|
|
|
73
77
|
Think of it as **`decimal.Decimal` for the physical world** — precise, predictable, and type-safe.
|
|
@@ -92,7 +96,11 @@ To best answer this question, we turn to an age-old technique ([dimensional anal
|
|
|
92
96
|
| **`Ratio`** | `ucon.core` | Represents the division of two `Number` objects; captures relationships between quantities. | Expressing rates, densities, efficiencies (e.g., energy / time = power, length / time = velocity). |
|
|
93
97
|
| **`Map`** hierarchy | `ucon.maps` | Composable conversion morphisms: `LinearMap`, `AffineMap`, `ComposedMap`. | Defining conversion functions between units (e.g., meter→foot, celsius→kelvin). |
|
|
94
98
|
| **`ConversionGraph`** | `ucon.graph` | Registry of unit conversion edges with BFS path composition. | Converting between units via `Number.to(target)`; managing default and custom graphs. |
|
|
99
|
+
| **`UnitSystem`** | `ucon.core` | Named mapping from dimensions to base units (e.g., SI, Imperial). | Defining coherent unit systems; grouping base units by dimension. |
|
|
100
|
+
| **`BasisTransform`** | `ucon.core` | Matrix-based transformation between dimensional exponent spaces. | Converting between incompatible dimensional structures; exact arithmetic with `Fraction`. |
|
|
101
|
+
| **`RebasedUnit`** | `ucon.core` | A unit rebased to another system's dimension, preserving provenance. | Cross-basis conversions; tracking original unit through basis changes. |
|
|
95
102
|
| **`units` module** | `ucon.units` | Defines canonical unit instances (SI, imperial, information, and derived units). | Quick access to standard physical units (`units.meter`, `units.foot`, `units.byte`, etc.). |
|
|
103
|
+
| **`pydantic` module** | `ucon.pydantic` | Pydantic v2 integration with `Number` type for model validation and JSON serialization. | Using `Number` in Pydantic models; API request/response validation; JSON round-trip serialization. |
|
|
96
104
|
|
|
97
105
|
### Under the Hood
|
|
98
106
|
|
|
@@ -139,35 +147,40 @@ Simple:
|
|
|
139
147
|
pip install ucon
|
|
140
148
|
```
|
|
141
149
|
|
|
150
|
+
With Pydantic v2 support:
|
|
151
|
+
```bash
|
|
152
|
+
pip install ucon[pydantic]
|
|
153
|
+
```
|
|
154
|
+
|
|
142
155
|
## Usage
|
|
143
156
|
|
|
144
|
-
|
|
157
|
+
### Quantities and Arithmetic
|
|
158
|
+
|
|
159
|
+
Dimensional analysis like this:
|
|
145
160
|
```
|
|
146
161
|
2 mL bromine | 3.119 g bromine
|
|
147
162
|
--------------x----------------- #=> 6.238 g bromine
|
|
148
163
|
1 | 1 mL bromine
|
|
149
164
|
```
|
|
150
|
-
becomes straightforward
|
|
165
|
+
becomes straightforward:
|
|
151
166
|
```python
|
|
152
167
|
from ucon import Number, Scale, units
|
|
153
168
|
from ucon.quantity import Ratio
|
|
154
169
|
|
|
155
|
-
# Two milliliters of bromine
|
|
156
170
|
mL = Scale.milli * units.liter
|
|
157
171
|
two_mL_bromine = Number(quantity=2, unit=mL)
|
|
158
172
|
|
|
159
|
-
# Density of bromine: 3.119 g/mL
|
|
160
173
|
bromine_density = Ratio(
|
|
161
174
|
numerator=Number(unit=units.gram, quantity=3.119),
|
|
162
175
|
denominator=Number(unit=mL),
|
|
163
176
|
)
|
|
164
177
|
|
|
165
|
-
# Multiply to find mass
|
|
166
178
|
grams_bromine = bromine_density.evaluate() * two_mL_bromine
|
|
167
179
|
print(grams_bromine) # <6.238 g>
|
|
168
180
|
```
|
|
169
181
|
|
|
170
|
-
Scale
|
|
182
|
+
### Scale Prefixes
|
|
183
|
+
|
|
171
184
|
```python
|
|
172
185
|
km = Scale.kilo * units.meter # UnitProduct with kilo-scaled meter
|
|
173
186
|
mg = Scale.milli * units.gram # UnitProduct with milli-scaled gram
|
|
@@ -175,12 +188,12 @@ mg = Scale.milli * units.gram # UnitProduct with milli-scaled gram
|
|
|
175
188
|
print(km.shorthand) # 'km'
|
|
176
189
|
print(mg.shorthand) # 'mg'
|
|
177
190
|
|
|
178
|
-
# Scale arithmetic
|
|
179
191
|
print(km.fold_scale()) # 1000.0
|
|
180
192
|
print(mg.fold_scale()) # 0.001
|
|
181
193
|
```
|
|
182
194
|
|
|
183
|
-
|
|
195
|
+
### Callable Units and Conversion
|
|
196
|
+
|
|
184
197
|
```python
|
|
185
198
|
from ucon import units, Scale
|
|
186
199
|
|
|
@@ -199,16 +212,16 @@ distance_mi = distance.to(units.mile)
|
|
|
199
212
|
print(distance_mi) # <3.107... mi>
|
|
200
213
|
```
|
|
201
214
|
|
|
202
|
-
Dimensionless
|
|
215
|
+
### Dimensionless Units
|
|
216
|
+
|
|
217
|
+
Angles, solid angles, and ratios are semantically isolated:
|
|
203
218
|
```python
|
|
204
219
|
import math
|
|
205
220
|
from ucon import units
|
|
206
221
|
|
|
207
|
-
# Angle conversions
|
|
208
222
|
angle = units.radian(math.pi)
|
|
209
223
|
print(angle.to(units.degree)) # <180.0 deg>
|
|
210
224
|
|
|
211
|
-
# Ratio conversions
|
|
212
225
|
ratio = units.percent(50)
|
|
213
226
|
print(ratio.to(units.ppm)) # <500000.0 ppm>
|
|
214
227
|
|
|
@@ -216,25 +229,61 @@ print(ratio.to(units.ppm)) # <500000.0 ppm>
|
|
|
216
229
|
units.radian(1).to(units.percent) # raises ConversionNotFound
|
|
217
230
|
```
|
|
218
231
|
|
|
219
|
-
Uncertainty
|
|
232
|
+
### Uncertainty Propagation
|
|
233
|
+
|
|
220
234
|
```python
|
|
221
235
|
from ucon import units
|
|
222
236
|
|
|
223
|
-
# Measurements with uncertainty
|
|
224
237
|
length = units.meter(1.234, uncertainty=0.005)
|
|
225
238
|
width = units.meter(0.567, uncertainty=0.003)
|
|
226
239
|
|
|
227
240
|
print(length) # <1.234 ± 0.005 m>
|
|
228
241
|
|
|
229
|
-
#
|
|
242
|
+
# Propagates through arithmetic (quadrature)
|
|
230
243
|
area = length * width
|
|
231
244
|
print(area) # <0.699678 ± 0.00424... m²>
|
|
232
245
|
|
|
233
|
-
#
|
|
246
|
+
# Propagates through conversion
|
|
234
247
|
length_ft = length.to(units.foot)
|
|
235
248
|
print(length_ft) # <4.048... ± 0.0164... ft>
|
|
236
249
|
```
|
|
237
250
|
|
|
251
|
+
### Pydantic Integration
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
from pydantic import BaseModel
|
|
255
|
+
from ucon.pydantic import Number
|
|
256
|
+
from ucon import units
|
|
257
|
+
|
|
258
|
+
class Measurement(BaseModel):
|
|
259
|
+
value: Number
|
|
260
|
+
|
|
261
|
+
# From JSON/dict input
|
|
262
|
+
m = Measurement(value={"quantity": 5, "unit": "km"})
|
|
263
|
+
print(m.value) # <5 km>
|
|
264
|
+
|
|
265
|
+
# From Number instance
|
|
266
|
+
m2 = Measurement(value=units.meter(10))
|
|
267
|
+
|
|
268
|
+
# Serialize to JSON
|
|
269
|
+
print(m.model_dump_json())
|
|
270
|
+
# {"value": {"quantity": 5.0, "unit": "km", "uncertainty": null}}
|
|
271
|
+
|
|
272
|
+
# Supports both Unicode and ASCII unit notation
|
|
273
|
+
m3 = Measurement(value={"quantity": 9.8, "unit": "m/s^2"}) # ASCII
|
|
274
|
+
m4 = Measurement(value={"quantity": 9.8, "unit": "m/s²"}) # Unicode
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**Design notes:**
|
|
278
|
+
- **Serialization format**: Units serialize as human-readable shorthand strings (`"km"`, `"m/s^2"`) rather than structured dicts, aligning with how scientists express units.
|
|
279
|
+
- **Parsing priority**: When parsing `"kg"`, ucon returns `Scale.kilo * gram` rather than looking up a `kilogram` Unit, ensuring consistent round-trip serialization and avoiding redundant unit definitions.
|
|
280
|
+
|
|
281
|
+
### Custom Unit Systems
|
|
282
|
+
|
|
283
|
+
`BasisTransform` enables conversions between incompatible dimensional structures (e.g., fantasy game physics, CGS units, domain-specific systems).
|
|
284
|
+
|
|
285
|
+
See full example: [docs/examples/basis-transform-fantasy-units.md](./docs/examples/basis-transform-fantasy-units.md)
|
|
286
|
+
|
|
238
287
|
---
|
|
239
288
|
|
|
240
289
|
## Roadmap Highlights
|
|
@@ -245,8 +294,9 @@ print(length_ft) # <4.048... ± 0.0164... ft>
|
|
|
245
294
|
| **0.4.x** | Conversion System | `ConversionGraph`, `Number.to()`, callable units | ✅ Complete |
|
|
246
295
|
| **0.5.0** | Dimensionless Units | Pseudo-dimensions for angle, solid angle, ratio | ✅ Complete |
|
|
247
296
|
| **0.5.x** | Uncertainty | Propagation through arithmetic and conversions | ✅ Complete |
|
|
248
|
-
| **0.5.x** | Unit Systems | `
|
|
249
|
-
| **0.
|
|
297
|
+
| **0.5.x** | Unit Systems | `BasisTransform`, `UnitSystem`, cross-basis conversion | ✅ Complete |
|
|
298
|
+
| **0.6.x** | Pydantic Integration | Type-safe quantity validation, JSON serialization | ✅ Complete |
|
|
299
|
+
| **0.7.x** | NumPy Arrays | Vectorized conversion and arithmetic | ⏳ Planned |
|
|
250
300
|
|
|
251
301
|
See full roadmap: [ROADMAP.md](./ROADMAP.md)
|
|
252
302
|
|
|
@@ -255,14 +305,21 @@ See full roadmap: [ROADMAP.md](./ROADMAP.md)
|
|
|
255
305
|
## Contributing
|
|
256
306
|
|
|
257
307
|
Contributions, issues, and pull requests are welcome!
|
|
258
|
-
|
|
308
|
+
|
|
309
|
+
Set up your development environment:
|
|
310
|
+
```bash
|
|
311
|
+
make venv
|
|
312
|
+
source .ucon-3.12/bin/activate
|
|
259
313
|
```
|
|
260
|
-
|
|
314
|
+
|
|
315
|
+
Run the test suite before committing:
|
|
316
|
+
```bash
|
|
317
|
+
make test
|
|
261
318
|
```
|
|
262
|
-
Then run the full test suite (against all supported python versions) before committing:
|
|
263
319
|
|
|
320
|
+
Run tests across all supported Python versions:
|
|
264
321
|
```bash
|
|
265
|
-
|
|
322
|
+
make test-all
|
|
266
323
|
```
|
|
267
324
|
---
|
|
268
325
|
|
|
@@ -1,40 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: ucon
|
|
3
|
-
Version: 0.5.1
|
|
4
|
-
Summary: a tool for dimensional analysis: a "Unit CONverter"
|
|
5
|
-
Home-page: https://github.com/withtwoemms/ucon
|
|
6
|
-
Author: Emmanuel I. Obi
|
|
7
|
-
Maintainer: Emmanuel I. Obi
|
|
8
|
-
Maintainer-email: withtwoemms@gmail.com
|
|
9
|
-
License: Apache-2.0
|
|
10
|
-
Classifier: Development Status :: 4 - Beta
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: Intended Audience :: Education
|
|
13
|
-
Classifier: Intended Audience :: Science/Research
|
|
14
|
-
Classifier: Topic :: Software Development :: Build Tools
|
|
15
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
-
Description-Content-Type: text/markdown
|
|
25
|
-
License-File: LICENSE
|
|
26
|
-
License-File: NOTICE
|
|
27
|
-
Dynamic: author
|
|
28
|
-
Dynamic: classifier
|
|
29
|
-
Dynamic: description
|
|
30
|
-
Dynamic: description-content-type
|
|
31
|
-
Dynamic: home-page
|
|
32
|
-
Dynamic: license
|
|
33
|
-
Dynamic: license-file
|
|
34
|
-
Dynamic: maintainer
|
|
35
|
-
Dynamic: maintainer-email
|
|
36
|
-
Dynamic: summary
|
|
37
|
-
|
|
38
1
|
<table>
|
|
39
2
|
<tr>
|
|
40
3
|
<td width="200">
|
|
@@ -68,6 +31,7 @@ It combines **units**, **scales**, and **dimensions** into a composable algebra
|
|
|
68
31
|
- Metric and binary prefixes (`kilo`, `kibi`, `micro`, `mebi`, etc.)
|
|
69
32
|
- Pseudo-dimensions for angles, solid angles, and ratios with semantic isolation
|
|
70
33
|
- Uncertainty propagation through arithmetic and conversions
|
|
34
|
+
- Pydantic v2 integration for API validation and JSON serialization
|
|
71
35
|
- A clean foundation for physics, chemistry, data modeling, and beyond
|
|
72
36
|
|
|
73
37
|
Think of it as **`decimal.Decimal` for the physical world** — precise, predictable, and type-safe.
|
|
@@ -92,7 +56,11 @@ To best answer this question, we turn to an age-old technique ([dimensional anal
|
|
|
92
56
|
| **`Ratio`** | `ucon.core` | Represents the division of two `Number` objects; captures relationships between quantities. | Expressing rates, densities, efficiencies (e.g., energy / time = power, length / time = velocity). |
|
|
93
57
|
| **`Map`** hierarchy | `ucon.maps` | Composable conversion morphisms: `LinearMap`, `AffineMap`, `ComposedMap`. | Defining conversion functions between units (e.g., meter→foot, celsius→kelvin). |
|
|
94
58
|
| **`ConversionGraph`** | `ucon.graph` | Registry of unit conversion edges with BFS path composition. | Converting between units via `Number.to(target)`; managing default and custom graphs. |
|
|
59
|
+
| **`UnitSystem`** | `ucon.core` | Named mapping from dimensions to base units (e.g., SI, Imperial). | Defining coherent unit systems; grouping base units by dimension. |
|
|
60
|
+
| **`BasisTransform`** | `ucon.core` | Matrix-based transformation between dimensional exponent spaces. | Converting between incompatible dimensional structures; exact arithmetic with `Fraction`. |
|
|
61
|
+
| **`RebasedUnit`** | `ucon.core` | A unit rebased to another system's dimension, preserving provenance. | Cross-basis conversions; tracking original unit through basis changes. |
|
|
95
62
|
| **`units` module** | `ucon.units` | Defines canonical unit instances (SI, imperial, information, and derived units). | Quick access to standard physical units (`units.meter`, `units.foot`, `units.byte`, etc.). |
|
|
63
|
+
| **`pydantic` module** | `ucon.pydantic` | Pydantic v2 integration with `Number` type for model validation and JSON serialization. | Using `Number` in Pydantic models; API request/response validation; JSON round-trip serialization. |
|
|
96
64
|
|
|
97
65
|
### Under the Hood
|
|
98
66
|
|
|
@@ -139,35 +107,40 @@ Simple:
|
|
|
139
107
|
pip install ucon
|
|
140
108
|
```
|
|
141
109
|
|
|
110
|
+
With Pydantic v2 support:
|
|
111
|
+
```bash
|
|
112
|
+
pip install ucon[pydantic]
|
|
113
|
+
```
|
|
114
|
+
|
|
142
115
|
## Usage
|
|
143
116
|
|
|
144
|
-
|
|
117
|
+
### Quantities and Arithmetic
|
|
118
|
+
|
|
119
|
+
Dimensional analysis like this:
|
|
145
120
|
```
|
|
146
121
|
2 mL bromine | 3.119 g bromine
|
|
147
122
|
--------------x----------------- #=> 6.238 g bromine
|
|
148
123
|
1 | 1 mL bromine
|
|
149
124
|
```
|
|
150
|
-
becomes straightforward
|
|
125
|
+
becomes straightforward:
|
|
151
126
|
```python
|
|
152
127
|
from ucon import Number, Scale, units
|
|
153
128
|
from ucon.quantity import Ratio
|
|
154
129
|
|
|
155
|
-
# Two milliliters of bromine
|
|
156
130
|
mL = Scale.milli * units.liter
|
|
157
131
|
two_mL_bromine = Number(quantity=2, unit=mL)
|
|
158
132
|
|
|
159
|
-
# Density of bromine: 3.119 g/mL
|
|
160
133
|
bromine_density = Ratio(
|
|
161
134
|
numerator=Number(unit=units.gram, quantity=3.119),
|
|
162
135
|
denominator=Number(unit=mL),
|
|
163
136
|
)
|
|
164
137
|
|
|
165
|
-
# Multiply to find mass
|
|
166
138
|
grams_bromine = bromine_density.evaluate() * two_mL_bromine
|
|
167
139
|
print(grams_bromine) # <6.238 g>
|
|
168
140
|
```
|
|
169
141
|
|
|
170
|
-
Scale
|
|
142
|
+
### Scale Prefixes
|
|
143
|
+
|
|
171
144
|
```python
|
|
172
145
|
km = Scale.kilo * units.meter # UnitProduct with kilo-scaled meter
|
|
173
146
|
mg = Scale.milli * units.gram # UnitProduct with milli-scaled gram
|
|
@@ -175,12 +148,12 @@ mg = Scale.milli * units.gram # UnitProduct with milli-scaled gram
|
|
|
175
148
|
print(km.shorthand) # 'km'
|
|
176
149
|
print(mg.shorthand) # 'mg'
|
|
177
150
|
|
|
178
|
-
# Scale arithmetic
|
|
179
151
|
print(km.fold_scale()) # 1000.0
|
|
180
152
|
print(mg.fold_scale()) # 0.001
|
|
181
153
|
```
|
|
182
154
|
|
|
183
|
-
|
|
155
|
+
### Callable Units and Conversion
|
|
156
|
+
|
|
184
157
|
```python
|
|
185
158
|
from ucon import units, Scale
|
|
186
159
|
|
|
@@ -199,16 +172,16 @@ distance_mi = distance.to(units.mile)
|
|
|
199
172
|
print(distance_mi) # <3.107... mi>
|
|
200
173
|
```
|
|
201
174
|
|
|
202
|
-
Dimensionless
|
|
175
|
+
### Dimensionless Units
|
|
176
|
+
|
|
177
|
+
Angles, solid angles, and ratios are semantically isolated:
|
|
203
178
|
```python
|
|
204
179
|
import math
|
|
205
180
|
from ucon import units
|
|
206
181
|
|
|
207
|
-
# Angle conversions
|
|
208
182
|
angle = units.radian(math.pi)
|
|
209
183
|
print(angle.to(units.degree)) # <180.0 deg>
|
|
210
184
|
|
|
211
|
-
# Ratio conversions
|
|
212
185
|
ratio = units.percent(50)
|
|
213
186
|
print(ratio.to(units.ppm)) # <500000.0 ppm>
|
|
214
187
|
|
|
@@ -216,25 +189,61 @@ print(ratio.to(units.ppm)) # <500000.0 ppm>
|
|
|
216
189
|
units.radian(1).to(units.percent) # raises ConversionNotFound
|
|
217
190
|
```
|
|
218
191
|
|
|
219
|
-
Uncertainty
|
|
192
|
+
### Uncertainty Propagation
|
|
193
|
+
|
|
220
194
|
```python
|
|
221
195
|
from ucon import units
|
|
222
196
|
|
|
223
|
-
# Measurements with uncertainty
|
|
224
197
|
length = units.meter(1.234, uncertainty=0.005)
|
|
225
198
|
width = units.meter(0.567, uncertainty=0.003)
|
|
226
199
|
|
|
227
200
|
print(length) # <1.234 ± 0.005 m>
|
|
228
201
|
|
|
229
|
-
#
|
|
202
|
+
# Propagates through arithmetic (quadrature)
|
|
230
203
|
area = length * width
|
|
231
204
|
print(area) # <0.699678 ± 0.00424... m²>
|
|
232
205
|
|
|
233
|
-
#
|
|
206
|
+
# Propagates through conversion
|
|
234
207
|
length_ft = length.to(units.foot)
|
|
235
208
|
print(length_ft) # <4.048... ± 0.0164... ft>
|
|
236
209
|
```
|
|
237
210
|
|
|
211
|
+
### Pydantic Integration
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
from pydantic import BaseModel
|
|
215
|
+
from ucon.pydantic import Number
|
|
216
|
+
from ucon import units
|
|
217
|
+
|
|
218
|
+
class Measurement(BaseModel):
|
|
219
|
+
value: Number
|
|
220
|
+
|
|
221
|
+
# From JSON/dict input
|
|
222
|
+
m = Measurement(value={"quantity": 5, "unit": "km"})
|
|
223
|
+
print(m.value) # <5 km>
|
|
224
|
+
|
|
225
|
+
# From Number instance
|
|
226
|
+
m2 = Measurement(value=units.meter(10))
|
|
227
|
+
|
|
228
|
+
# Serialize to JSON
|
|
229
|
+
print(m.model_dump_json())
|
|
230
|
+
# {"value": {"quantity": 5.0, "unit": "km", "uncertainty": null}}
|
|
231
|
+
|
|
232
|
+
# Supports both Unicode and ASCII unit notation
|
|
233
|
+
m3 = Measurement(value={"quantity": 9.8, "unit": "m/s^2"}) # ASCII
|
|
234
|
+
m4 = Measurement(value={"quantity": 9.8, "unit": "m/s²"}) # Unicode
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Design notes:**
|
|
238
|
+
- **Serialization format**: Units serialize as human-readable shorthand strings (`"km"`, `"m/s^2"`) rather than structured dicts, aligning with how scientists express units.
|
|
239
|
+
- **Parsing priority**: When parsing `"kg"`, ucon returns `Scale.kilo * gram` rather than looking up a `kilogram` Unit, ensuring consistent round-trip serialization and avoiding redundant unit definitions.
|
|
240
|
+
|
|
241
|
+
### Custom Unit Systems
|
|
242
|
+
|
|
243
|
+
`BasisTransform` enables conversions between incompatible dimensional structures (e.g., fantasy game physics, CGS units, domain-specific systems).
|
|
244
|
+
|
|
245
|
+
See full example: [docs/examples/basis-transform-fantasy-units.md](./docs/examples/basis-transform-fantasy-units.md)
|
|
246
|
+
|
|
238
247
|
---
|
|
239
248
|
|
|
240
249
|
## Roadmap Highlights
|
|
@@ -245,8 +254,9 @@ print(length_ft) # <4.048... ± 0.0164... ft>
|
|
|
245
254
|
| **0.4.x** | Conversion System | `ConversionGraph`, `Number.to()`, callable units | ✅ Complete |
|
|
246
255
|
| **0.5.0** | Dimensionless Units | Pseudo-dimensions for angle, solid angle, ratio | ✅ Complete |
|
|
247
256
|
| **0.5.x** | Uncertainty | Propagation through arithmetic and conversions | ✅ Complete |
|
|
248
|
-
| **0.5.x** | Unit Systems | `
|
|
249
|
-
| **0.
|
|
257
|
+
| **0.5.x** | Unit Systems | `BasisTransform`, `UnitSystem`, cross-basis conversion | ✅ Complete |
|
|
258
|
+
| **0.6.x** | Pydantic Integration | Type-safe quantity validation, JSON serialization | ✅ Complete |
|
|
259
|
+
| **0.7.x** | NumPy Arrays | Vectorized conversion and arithmetic | ⏳ Planned |
|
|
250
260
|
|
|
251
261
|
See full roadmap: [ROADMAP.md](./ROADMAP.md)
|
|
252
262
|
|
|
@@ -255,14 +265,21 @@ See full roadmap: [ROADMAP.md](./ROADMAP.md)
|
|
|
255
265
|
## Contributing
|
|
256
266
|
|
|
257
267
|
Contributions, issues, and pull requests are welcome!
|
|
258
|
-
|
|
268
|
+
|
|
269
|
+
Set up your development environment:
|
|
270
|
+
```bash
|
|
271
|
+
make venv
|
|
272
|
+
source .ucon-3.12/bin/activate
|
|
259
273
|
```
|
|
260
|
-
|
|
274
|
+
|
|
275
|
+
Run the test suite before committing:
|
|
276
|
+
```bash
|
|
277
|
+
make test
|
|
261
278
|
```
|
|
262
|
-
Then run the full test suite (against all supported python versions) before committing:
|
|
263
279
|
|
|
280
|
+
Run tests across all supported Python versions:
|
|
264
281
|
```bash
|
|
265
|
-
|
|
282
|
+
make test-all
|
|
266
283
|
```
|
|
267
284
|
---
|
|
268
285
|
|