summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/pull_request_template.md2
-rw-r--r--.github/workflows/ci.yml82
-rw-r--r--.pre-commit-config.yaml32
-rw-r--r--CHANGES75
-rw-r--r--README.rst4
-rw-r--r--benchmarks/benchmarks/10_registry.py1
-rw-r--r--benchmarks/benchmarks/20_quantity.py1
-rw-r--r--benchmarks/benchmarks/30_numpy.py1
-rw-r--r--docs/_static/index_api.svg26
-rw-r--r--docs/_static/index_contribute.svg21
-rw-r--r--docs/_static/index_getting_started.svg18
-rw-r--r--docs/_static/index_user_guide.svg18
-rw-r--r--docs/_static/style.css45
-rw-r--r--docs/_themes/.gitignore3
-rw-r--r--docs/_themes/LICENSE37
-rw-r--r--docs/_themes/README31
-rw-r--r--docs/_themes/flask/layout.html30
-rw-r--r--docs/_themes/flask/relations.html20
-rw-r--r--docs/_themes/flask/static/flasky.css_t395
-rw-r--r--docs/_themes/flask/static/small_flask.css70
-rw-r--r--docs/_themes/flask/theme.conf10
-rw-r--r--docs/_themes/flask_theme_support.py89
-rw-r--r--docs/advanced/currencies.rst (renamed from docs/currencies.rst)0
-rw-r--r--docs/advanced/custom-registry-class.rst83
-rw-r--r--docs/advanced/defining.rst (renamed from docs/defining.rst)0
-rw-r--r--docs/advanced/index.rst17
-rw-r--r--docs/advanced/measurement.rst (renamed from docs/measurement.rst)0
-rw-r--r--docs/advanced/performance.rst (renamed from docs/performance.rst)60
-rw-r--r--docs/advanced/pitheorem.rst (renamed from docs/pitheorem.rst)0
-rw-r--r--docs/advanced/serialization.rst (renamed from docs/serialization.rst)2
-rw-r--r--docs/advanced/wrapping.rst (renamed from docs/wrapping.rst)4
-rw-r--r--docs/api/base.rst70
-rw-r--r--docs/api/facets.rst45
-rw-r--r--docs/api/index.rst10
-rw-r--r--docs/api/specific.rst44
-rw-r--r--docs/conf.py239
-rw-r--r--docs/dev/contributing.rst (renamed from docs/contributing.rst)18
-rw-r--r--docs/dev/pint-convert.rst (renamed from docs/pint-convert.rst)44
-rw-r--r--docs/developers_reference.rst75
-rw-r--r--docs/ecosystem.rst11
-rw-r--r--docs/getting.rst58
-rw-r--r--docs/getting/faq.rst (renamed from docs/faq.rst)0
-rw-r--r--docs/getting/index.rst51
-rw-r--r--docs/getting/overview.rst116
-rw-r--r--docs/getting/pint-in-your-projects.rst70
-rw-r--r--docs/getting/tutorial.rst (renamed from docs/tutorial.rst)52
-rw-r--r--docs/index.rst198
-rw-r--r--docs/user/angular_frequency.rst (renamed from docs/angular_frequency.rst)6
-rw-r--r--docs/user/contexts.rst (renamed from docs/contexts.rst)0
-rw-r--r--docs/user/defining-quantities.rst (renamed from docs/defining-quantities.rst)0
-rw-r--r--docs/user/formatting.rst (renamed from docs/formatting.rst)6
-rw-r--r--docs/user/index.rst20
-rw-r--r--docs/user/log_units.rst (renamed from docs/log_units.rst)0
-rw-r--r--docs/user/nonmult.rst (renamed from docs/nonmult.rst)0
-rw-r--r--docs/user/numpy.ipynb (renamed from docs/numpy.ipynb)301
-rw-r--r--docs/user/plotting.rst (renamed from docs/plotting.rst)25
-rw-r--r--docs/user/systems.rst (renamed from docs/systems.rst)4
-rw-r--r--pint/__init__.py2
-rw-r--r--pint/_typing.py4
-rw-r--r--pint/_vendor/flexparser.py1455
-rw-r--r--pint/compat.py75
-rw-r--r--pint/default_en.txt27
-rw-r--r--pint/definitions.py149
-rw-r--r--pint/delegates/__init__.py14
-rw-r--r--pint/delegates/base_defparser.py105
-rw-r--r--pint/delegates/txt_defparser/__init__.py14
-rw-r--r--pint/delegates/txt_defparser/block.py45
-rw-r--r--pint/delegates/txt_defparser/common.py57
-rw-r--r--pint/delegates/txt_defparser/context.py196
-rw-r--r--pint/delegates/txt_defparser/defaults.py77
-rw-r--r--pint/delegates/txt_defparser/defparser.py118
-rw-r--r--pint/delegates/txt_defparser/group.py106
-rw-r--r--pint/delegates/txt_defparser/plain.py283
-rw-r--r--pint/delegates/txt_defparser/system.py110
-rw-r--r--pint/errors.py196
-rw-r--r--pint/facets/__init__.py7
-rw-r--r--pint/facets/context/__init__.py2
-rw-r--r--pint/facets/context/definitions.py267
-rw-r--r--pint/facets/context/objects.py38
-rw-r--r--pint/facets/context/registry.py97
-rw-r--r--pint/facets/dask/__init__.py20
-rw-r--r--pint/facets/formatting/__init__.py2
-rw-r--r--pint/facets/formatting/objects.py4
-rw-r--r--pint/facets/formatting/registry.py1
-rw-r--r--pint/facets/group/__init__.py2
-rw-r--r--pint/facets/group/definitions.py104
-rw-r--r--pint/facets/group/objects.py27
-rw-r--r--pint/facets/group/registry.py46
-rw-r--r--pint/facets/measurement/__init__.py2
-rw-r--r--pint/facets/measurement/objects.py2
-rw-r--r--pint/facets/measurement/registry.py5
-rw-r--r--pint/facets/nonmultiplicative/__init__.py2
-rw-r--r--pint/facets/nonmultiplicative/registry.py66
-rw-r--r--pint/facets/numpy/__init__.py2
-rw-r--r--pint/facets/numpy/numpy_func.py41
-rw-r--r--pint/facets/numpy/quantity.py1
-rw-r--r--pint/facets/numpy/registry.py1
-rw-r--r--pint/facets/numpy/unit.py1
-rw-r--r--pint/facets/plain/__init__.py23
-rw-r--r--pint/facets/plain/definitions.py378
-rw-r--r--pint/facets/plain/quantity.py14
-rw-r--r--pint/facets/plain/registry.py346
-rw-r--r--pint/facets/plain/unit.py2
-rw-r--r--pint/facets/system/__init__.py2
-rw-r--r--pint/facets/system/definitions.py122
-rw-r--r--pint/facets/system/objects.py13
-rw-r--r--pint/facets/system/registry.py31
-rw-r--r--pint/formatting.py24
-rw-r--r--pint/matplotlib.py3
-rw-r--r--pint/parser.py374
-rwxr-xr-xpint/pint_convert.py (renamed from pint/pint-convert)7
-rw-r--r--pint/registry.py1
-rw-r--r--pint/registry_helpers.py9
-rw-r--r--pint/testsuite/baseline/test_basic_plot.pngbin17483 -> 17415 bytes
-rw-r--r--pint/testsuite/baseline/test_plot_with_non_default_format.pngbin0 -> 16617 bytes
-rw-r--r--pint/testsuite/baseline/test_plot_with_set_units.pngbin18145 -> 18176 bytes
-rw-r--r--pint/testsuite/helpers.py6
-rw-r--r--pint/testsuite/test_compat_downcast.py2
-rw-r--r--pint/testsuite/test_contexts.py19
-rw-r--r--pint/testsuite/test_dask.py2
-rw-r--r--pint/testsuite/test_definitions.py7
-rw-r--r--pint/testsuite/test_diskcache.py10
-rw-r--r--pint/testsuite/test_errors.py40
-rw-r--r--pint/testsuite/test_formatting.py15
-rw-r--r--pint/testsuite/test_issues.py82
-rw-r--r--pint/testsuite/test_log_units.py1
-rw-r--r--pint/testsuite/test_matplotlib.py18
-rw-r--r--pint/testsuite/test_measurement.py2
-rw-r--r--pint/testsuite/test_non_int.py516
-rw-r--r--pint/testsuite/test_numpy.py63
-rw-r--r--pint/testsuite/test_pint_eval.py74
-rw-r--r--pint/testsuite/test_pitheorem.py1
-rw-r--r--pint/testsuite/test_quantity.py21
-rw-r--r--pint/testsuite/test_umath.py68
-rw-r--r--pint/testsuite/test_unit.py70
-rw-r--r--pint/testsuite/test_util.py4
-rw-r--r--pint/util.py103
-rw-r--r--pyproject.toml86
-rw-r--r--requirements_docs.txt3
-rw-r--r--setup.cfg73
-rw-r--r--setup.py5
-rw-r--r--version.py2
142 files changed, 5641 insertions, 3414 deletions
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 8ee5e75..b012d24 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,5 +1,5 @@
- [ ] Closes # (insert issue number)
-- [ ] Executed ``pre-commit run --all-files`` with no errors
+- [ ] Executed `pre-commit run --all-files` with no errors
- [ ] The change is fully covered by automated unit tests
- [ ] Documented in docs/ as appropriate
- [ ] Added an entry to the CHANGES file
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 60ac53b..96601c2 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,13 +7,13 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: [3.8, 3.9, "3.10"]
+ python-version: [3.8, 3.9, "3.10", "3.11"]
numpy: [null, "numpy>=1.19,<2.0.0"]
uncertainties: [null, "uncertainties==3.1.6", "uncertainties>=3.1.6,<4.0.0"]
extras: [null]
include:
- python-version: 3.8 # Minimal versions
- numpy: numpy==1.19.5
+ numpy: "numpy"
extras: matplotlib==2.2.5
- python-version: 3.8
numpy: "numpy"
@@ -64,7 +64,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt install -y graphviz
- pip install pytest pytest-cov pytest-subtests
+ pip install pytest pytest-cov pytest-subtests packaging
pip install .
- name: Install pytest-mpl
@@ -92,18 +92,8 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: [3.8, 3.9, "3.10"]
+ python-version: [3.8, 3.9, "3.10", "3.11"]
numpy: [ "numpy>=1.19,<2.0.0" ]
- # uncertainties: [null, "uncertainties==3.1.6", "uncertainties>=3.1.6,<4.0.0"]
- # extras: [null]
- # include:
- # - python-version: 3.8 # Minimal versions
- # numpy: numpy==1.19.5
- # extras: matplotlib==2.2.5
- # - python-version: 3.8
- # numpy: "numpy"
- # uncertainties: "uncertainties"
- # extras: "sparse xarray netCDF4 dask[complete] graphviz babel==2.8"
runs-on: windows-latest
env:
@@ -149,7 +139,7 @@ jobs:
- name: Install dependencies
run: |
# sudo apt install -y graphviz
- pip install pytest pytest-cov pytest-subtests
+ pip install pytest pytest-cov pytest-subtests packaging
pip install .
# - name: Install pytest-mpl
@@ -159,6 +149,68 @@ jobs:
- name: Run tests
run: pytest ${env:TEST_OPTS}
+ test-macos:
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: [3.8, 3.9, "3.10", "3.11"]
+ numpy: [null, "numpy>=1.19,<2.0.0" ]
+ runs-on: macos-latest
+
+ env:
+ TEST_OPTS: "-rfsxEX -s --cov=pint --cov-config=.coveragerc"
+
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ fetch-depth: 100
+
+ - name: Get tags
+ run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
+
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: Get pip cache dir
+ id: pip-cache
+ run: echo "::set-output name=dir::$(pip cache dir)"
+
+ - name: Setup caching
+ uses: actions/cache@v2
+ with:
+ path: ${{ steps.pip-cache.outputs.dir }}
+ key: pip-${{ matrix.python-version }}
+ restore-keys: |
+ pip-${{ matrix.python-version }}
+
+ - name: Install numpy
+ if: ${{ matrix.numpy != null }}
+ run: pip install "${{matrix.numpy}}"
+
+ - name: Install dependencies
+ run: |
+ pip install pytest pytest-cov pytest-subtests packaging
+ pip install .
+
+ - name: Run Tests
+ run: |
+ pytest $TEST_OPTS
+
+ - name: Coverage report
+ run: coverage report -m
+
+ - name: Coveralls Parallel
+ env:
+ COVERALLS_FLAG_NAME: ${{ matrix.test-number }}
+ COVERALLS_PARALLEL: true
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ COVERALLS_SERVICE_NAME: github
+ run: |
+ pip install coveralls
+ coveralls
+
coveralls:
needs: test-linux
runs-on: ubuntu-latest
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 0dc6294..a4a3f4a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,20 +1,30 @@
exclude: '^pint/_vendor'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.3.0
+ rev: v4.4.0
hooks:
- - id: trailing-whitespace
- - id: end-of-file-fixer
- - id: check-yaml
+ - id: check-yaml
+ - id: end-of-file-fixer
+ - id: trailing-whitespace
- repo: https://github.com/psf/black
- rev: 22.6.0
+ rev: 23.1.0
hooks:
- id: black
-- repo: https://github.com/pycqa/isort
- rev: 5.10.1
+ - id: black-jupyter
+- repo: https://github.com/charliermarsh/ruff-pre-commit
+ rev: 'v0.0.240'
hooks:
- - id: isort
-- repo: https://gitlab.com/pycqa/flake8
- rev: 3.9.2
+ - id: ruff
+ args: ["--fix"]
+- repo: https://github.com/executablebooks/mdformat
+ rev: 0.7.16
hooks:
- - id: flake8
+ - id: mdformat
+ additional_dependencies:
+ - mdformat-gfm # GitHub-flavored Markdown
+ - mdformat-black
+- repo: https://github.com/kynan/nbstripout
+ rev: 0.6.1
+ hooks:
+ - id: nbstripout
+ args: [--extra-keys=metadata.kernelspec metadata.language_info.version]
diff --git a/CHANGES b/CHANGES
index 9826e33..1942230 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,16 +1,87 @@
Pint Changelog
==============
-0.20 (unreleased)
-------------------
+0.21 (unreleased)
+-----------------
+
+- Add PEP621/631 support.
+ (Issue #1647)
+- Exposed matplotlib unit formatter (PR #1703)
+- Fix error when when re-registering a formatter.
+ (PR #1629)
+- Add new SI prefixes: ronna-, ronto-, quetta-, quecto-.
+ (PR #1652)
+- Fix unit check with `atol` using `np.allclose` & `np.isclose`.
+ (Issue #1658)
+- Implementation for numpy.positive added for Quantity.
+ (PR #1663)
+- Changed frequency to angular frequency in the docs.
+ (PR #1668)
+- Remove deprecated `alen` numpy function
+ (PR #1678)
+- Updated URLs for log and offset unit errors.
+ (PR #1727)
+- Patched TYPE_CHECKING import regression.
+ (PR #1686)
+- Parse '°' along with previous text, rather than adding a space,
+ allowing, eg 'Δ°C' as a unit.
+ (PR #1729)
+- Improved escaping of special characters for LaTeX format
+ (PR #1712)
+- Avoid addition of spurious trailing zeros when converting units and non-int-type is
+ Decimal (PR #1625).
+- Implementation for numpy.delete added for Quantity.
+ (PR #1669)
+- Fixed Quantity type returned from `__dask_postcompute__`.
+ (PR #1722)
+- Added Townsend unit
+ (PR #1738)
+
+
+### Breaking Changes
+
+- Support percent and ppm units. Support the `%` symbol.
+ (Issue #1277)
+- Fix error when parsing subtraction operator followed by white space.
+ (PR #1701)
+- Removed Td as an alias for denier (within the Textile group)
+
+0.20.1 (2022-10-27)
+-------------------
+
+- Simplify registry subclassing.
+ (Issue #1631)
+- Restore intersphinx cross reference functionality.
+ (Issue #1637)
+- Use a newer version of flexparser that can deal with
+ imports in linked/temporary folders.
+ (Issue #1634)
+
+
+0.20 (2022-10-25)
+-----------------
- Reorganized code into facets.
Each facet encapsulate a Pint functionality.
(See #1466, #1479)
+- The definition parser is now completely appart, making it easy to try other formats.
+ (See #1595)
+- Extra requires for optional packages are now explicit in setup.cfg
+ (See #1627)
- Parse both Greek mu and micro Unicode points without error.
(Issue #1030, #574)
- Added angular frequency documentation page.
- Move ASV benchmarks to dedicated folder. (Issue #1542)
+- Implement `numpy.broadcast_arrays` (#1607)
+- An ndim attribute has been added to Quantity and DataFrame has been added to upcast
+types for pint-pandas compatibility. (#1596)
+- Fix a recursion error that would be raised when passing quantities to `cond` and `x`.
+ (Issue #1510, #1530)
+- Update test_non_int tests for pytest.
+- Create NaN-value quantities of appropriate non-int-type (Issue #1570).
+- New documentation format and organization!
+- Better support for pandas and dask.
+
- Add Quantity.to_preferred
0.19.2 (2022-04-23)
diff --git a/README.rst b/README.rst
index 86c8f77..32879d9 100644
--- a/README.rst
+++ b/README.rst
@@ -153,7 +153,7 @@ see CHANGES_
.. _`NumPy`: http://www.numpy.org/
.. _`PEP 3101`: https://www.python.org/dev/peps/pep-3101/
.. _`Babel`: http://babel.pocoo.org/
-.. _`Pandas Extension Types`: https://pandas.pydata.org/pandas-docs/stable/extending.html#extension-types
-.. _`pint-pandas Jupyter notebook`: https://github.com/hgrecco/pint-pandas/blob/master/notebooks/pandas_support.ipynb
+.. _`Pandas Extension Types`: https://pandas.pydata.org/pandas-docs/stable/development/extending.html#extension-types
+.. _`pint-pandas Jupyter notebook`: https://github.com/hgrecco/pint-pandas/blob/master/notebooks/pint-pandas.ipynb
.. _`AUTHORS`: https://github.com/hgrecco/pint/blob/master/AUTHORS
.. _`CHANGES`: https://github.com/hgrecco/pint/blob/master/CHANGES
diff --git a/benchmarks/benchmarks/10_registry.py b/benchmarks/benchmarks/10_registry.py
index 1019eb5..41da67b 100644
--- a/benchmarks/benchmarks/10_registry.py
+++ b/benchmarks/benchmarks/10_registry.py
@@ -15,7 +15,6 @@ data = {}
def setup(*args):
-
global ureg, data
data["int"] = 1
diff --git a/benchmarks/benchmarks/20_quantity.py b/benchmarks/benchmarks/20_quantity.py
index 5f6dd41..c0174ef 100644
--- a/benchmarks/benchmarks/20_quantity.py
+++ b/benchmarks/benchmarks/20_quantity.py
@@ -20,7 +20,6 @@ data = {}
def setup(*args):
-
global ureg, data
data["int"] = 1
diff --git a/benchmarks/benchmarks/30_numpy.py b/benchmarks/benchmarks/30_numpy.py
index ec83833..15ae66c 100644
--- a/benchmarks/benchmarks/30_numpy.py
+++ b/benchmarks/benchmarks/30_numpy.py
@@ -29,7 +29,6 @@ def float_range(n):
def setup(*args):
-
global ureg, data
short = list(float_range(3))
mid = list(float_range(1_000))
diff --git a/docs/_static/index_api.svg b/docs/_static/index_api.svg
new file mode 100644
index 0000000..ceb5be6
--- /dev/null
+++ b/docs/_static/index_api.svg
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="99.058548mm" height="89.967583mm" viewBox="0 0 99.058554 89.967582" version="1.1" id="svg1040" inkscape:version="0.92.4 (f8dce91, 2019-08-02)" sodipodi:docname="index_api.svg">
+ <defs id="defs1034"/>
+ <sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.35" inkscape:cx="533.74914" inkscape:cy="10.90433" inkscape:document-units="mm" inkscape:current-layer="layer1" showgrid="false" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" inkscape:window-width="930" inkscape:window-height="472" inkscape:window-x="2349" inkscape:window-y="267" inkscape:window-maximized="0"/>
+ <metadata id="metadata1037">
+ <rdf:RDF>
+ <cc:Work rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ <dc:title/>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(195.19933,-1.0492759)">
+ <g id="g1008" transform="matrix(1.094977,0,0,1.094977,-521.5523,-198.34055)">
+ <path inkscape:connector-curvature="0" id="path899" d="M 324.96812,187.09499 H 303.0455 v 72.1639 h 22.67969" style="fill:none;stroke:#5a5a5a;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
+ <path inkscape:connector-curvature="0" id="path899-3" d="m 361.58921,187.09499 h 21.92262 v 72.1639 h -22.67969" style="fill:none;stroke:#5a5a5a;stroke-width:10;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
+ <g transform="translate(415.87139,46.162126)" id="g944">
+ <circle style="fill:#5a5a5a;fill-opacity:1;stroke:#5a5a5a;stroke-width:4.53704548;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="path918" cx="-84.40152" cy="189.84375" r="2.2293637"/>
+ <circle style="fill:#5a5a5a;fill-opacity:1;stroke:#5a5a5a;stroke-width:4.53704548;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="path918-5" cx="-72.949402" cy="189.84375" r="2.2293637"/>
+ <circle style="fill:#5a5a5a;fill-opacity:1;stroke:#5a5a5a;stroke-width:4.53704548;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" id="path918-6" cx="-61.497284" cy="189.84375" r="2.2293637"/>
+ </g>
+ </g>
+ </g>
+</svg>
diff --git a/docs/_static/index_contribute.svg b/docs/_static/index_contribute.svg
new file mode 100644
index 0000000..24a29f3
--- /dev/null
+++ b/docs/_static/index_contribute.svg
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="89.624855mm" height="89.96759mm" viewBox="0 0 89.62486 89.96759" version="1.1" id="svg1040" inkscape:version="0.92.4 (f8dce91, 2019-08-02)" sodipodi:docname="index_contribute.svg">
+ <defs id="defs1034"/>
+ <sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.35" inkscape:cx="683.11893" inkscape:cy="-59.078181" inkscape:document-units="mm" inkscape:current-layer="layer1" showgrid="false" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" inkscape:window-width="930" inkscape:window-height="472" inkscape:window-x="2349" inkscape:window-y="267" inkscape:window-maximized="0"/>
+ <metadata id="metadata1037">
+ <rdf:RDF>
+ <cc:Work rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ <dc:title/>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(234.72009,17.466935)">
+ <g id="g875" transform="matrix(0.99300176,0,0,0.99300176,-133.24106,-172.58804)">
+ <path sodipodi:nodetypes="ccc" inkscape:connector-curvature="0" id="path869" d="m -97.139881,161.26069 47.247024,40.25446 -47.247024,40.25446" style="fill:none;stroke:#5a5a5a;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
+ <path inkscape:connector-curvature="0" id="path871" d="m -49.514879,241.81547 h 32.505951" style="fill:none;stroke:#5a5a5a;stroke-width:10;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"/>
+ </g>
+ </g>
+</svg>
diff --git a/docs/_static/index_getting_started.svg b/docs/_static/index_getting_started.svg
new file mode 100644
index 0000000..2afbbe1
--- /dev/null
+++ b/docs/_static/index_getting_started.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="101.09389mm" height="89.96759mm" viewBox="0 0 101.09389 89.96759" version="1.1" id="svg1040" inkscape:version="0.92.4 (f8dce91, 2019-08-02)" sodipodi:docname="index_getting_started.svg">
+ <defs id="defs1034"/>
+ <sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.35" inkscape:cx="-93.242129" inkscape:cy="-189.9825" inkscape:document-units="mm" inkscape:current-layer="layer1" showgrid="false" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" inkscape:window-width="1875" inkscape:window-height="1056" inkscape:window-x="1965" inkscape:window-y="0" inkscape:window-maximized="1"/>
+ <metadata id="metadata1037">
+ <rdf:RDF>
+ <cc:Work rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ <dc:title/>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(2.9219487,-8.5995374)">
+ <path style="fill:#5a5a5a;fill-opacity:1;stroke-width:0.20233451" d="M 37.270955,98.335591 C 33.358064,97.07991 31.237736,92.52319 32.964256,89.08022 c 0.18139,-0.361738 4.757999,-5.096629 10.17021,-10.521968 l 9.84041,-9.864254 -4.03738,-4.041175 -4.037391,-4.041172 -4.96415,4.916665 c -3.61569,3.581096 -5.238959,5.04997 -5.975818,5.407377 l -1.011682,0.490718 H 17.267525 1.5866055 L 0.65034544,70.96512 C -2.2506745,69.535833 -3.5952145,66.18561 -2.5925745,62.884631 c 0.53525,-1.762217 1.61699004,-3.050074 3.22528014,-3.839847 l 1.15623996,-0.56778 13.2591094,-0.05613 13.259111,-0.05613 11.5262,-11.527539 11.526199,-11.527528 H 40.622647 c -12.145542,0 -12.189222,-0.0046 -13.752801,-1.445851 -2.229871,-2.055423 -2.162799,-5.970551 0.135998,-7.938238 1.475193,-1.262712 1.111351,-1.238469 18.588522,-1.238469 12.899229,0 16.035311,0.05193 16.692589,0.276494 0.641832,0.219264 2.590731,2.051402 9.416301,8.852134 l 8.606941,8.575638 h 6.848168 c 4.837422,0 7.092281,0.07311 7.679571,0.249094 0.48064,0.144008 1.22985,0.634863 1.77578,1.163429 2.383085,2.307333 1.968685,6.539886 -0.804989,8.221882 -0.571871,0.346781 -1.38284,0.687226 -1.80217,0.756523 -0.41933,0.06928 -4.2741,0.127016 -8.56615,0.128238 -6.56998,0.0016 -7.977492,-0.04901 -8.902732,-0.321921 -0.975569,-0.287742 -1.400468,-0.622236 -3.783999,-2.978832 l -2.685021,-2.654679 -5.05411,5.051071 -5.0541,5.051081 3.926292,3.947202 c 2.365399,2.378001 4.114289,4.309171 4.399158,4.857713 0.39266,0.75606 0.47311,1.219412 0.474321,2.731516 0.003,3.083647 0.620779,2.331942 -13.598011,16.531349 -10.273768,10.259761 -12.679778,12.563171 -13.500979,12.92519 -1.267042,0.55857 -3.156169,0.681342 -4.390271,0.285321 z m 40.130741,-65.45839 c -2.212909,-0.579748 -3.782711,-1.498393 -5.51275,-3.226063 -2.522111,-2.518633 -3.633121,-5.181304 -3.633121,-8.707194 0,-3.530699 1.11238,-6.197124 3.631161,-8.704043 4.866751,-4.8438383 12.324781,-4.8550953 17.211791,-0.026 3.908758,3.862461 4.818578,9.377999 2.372188,14.380771 -0.846209,1.730481 -3.39493,4.326384 -5.143839,5.239072 -2.69708,1.407492 -6.042829,1.798628 -8.92543,1.043434 z" id="path1000" inkscape:connector-curvature="0"/>
+ </g>
+</svg>
diff --git a/docs/_static/index_user_guide.svg b/docs/_static/index_user_guide.svg
new file mode 100644
index 0000000..d70b3d7
--- /dev/null
+++ b/docs/_static/index_user_guide.svg
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" width="123.72241mm" height="89.96759mm" viewBox="0 0 123.72242 89.96759" version="1.1" id="svg1040" inkscape:version="0.92.4 (f8dce91, 2019-08-02)" sodipodi:docname="index_userguide.svg">
+ <defs id="defs1034"/>
+ <sodipodi:namedview id="base" pagecolor="#ffffff" bordercolor="#666666" borderopacity="1.0" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:zoom="0.35" inkscape:cx="332.26618" inkscape:cy="83.744004" inkscape:document-units="mm" inkscape:current-layer="layer1" showgrid="false" fit-margin-top="0" fit-margin-left="0" fit-margin-right="0" fit-margin-bottom="0" inkscape:window-width="930" inkscape:window-height="472" inkscape:window-x="2349" inkscape:window-y="267" inkscape:window-maximized="0"/>
+ <metadata id="metadata1037">
+ <rdf:RDF>
+ <cc:Work rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
+ <dc:title/>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g inkscape:label="Layer 1" inkscape:groupmode="layer" id="layer1" transform="translate(141.8903,-20.32143)">
+ <path style="fill:#5a5a5a;fill-opacity:1;stroke-width:0.20483544" d="m -139.53374,110.1657 c -0.80428,-0.24884 -1.71513,-1.11296 -2.07107,-1.96486 -0.23905,-0.57214 -0.28453,-6.28104 -0.28453,-35.720988 0,-38.274546 -0.079,-35.840728 1.19849,-36.91568 0.58869,-0.495345 4.63766,-2.187548 8.47998,-3.544073 l 1.58749,-0.560453 v -3.309822 c 0,-3.025538 0.0396,-3.388179 0.46086,-4.222122 0.68808,-1.362003 1.38671,-1.714455 4.60319,-2.322195 4.12797,-0.779966 5.13304,-0.912766 8.81544,-1.16476 11.80964,-0.808168 22.80911,2.509277 30.965439,9.3392 1.750401,1.465747 3.840861,3.5635 5.0903,5.108065 l 0.659122,0.814805 0.659109,-0.814805 c 1.249431,-1.544565 3.33988,-3.642318 5.09029,-5.108065 8.156331,-6.829923 19.155791,-10.147368 30.965441,-9.3392 3.682389,0.251994 4.68748,0.384794 8.81544,1.16476 3.21647,0.60774 3.91511,0.960192 4.60318,2.322195 0.4213,0.833943 0.46087,1.196584 0.46087,4.222122 v 3.309822 l 1.58748,0.560453 c 4.10165,1.448077 7.98852,3.072753 8.5259,3.563743 1.22643,1.120567 1.15258,-1.245868 1.15258,36.927177 0,34.567591 -0.005,35.083151 -0.40663,35.903991 -0.22365,0.45804 -0.73729,1.05665 -1.14143,1.33024 -1.22281,0.82783 -2.17721,0.70485 -5.86813,-0.7561 -9.19595,-3.63998 -18.956011,-6.38443 -26.791332,-7.53353 -3.02827,-0.44412 -9.26189,-0.61543 -11.77821,-0.3237 -5.19357,0.60212 -8.736108,2.05527 -11.700039,4.79936 -0.684501,0.63371 -1.466141,1.23646 -1.736979,1.33942 -0.63859,0.2428 -4.236521,0.2428 -4.875112,0 -0.27083,-0.10296 -1.05247,-0.70571 -1.73696,-1.33942 -2.96395,-2.74409 -6.50648,-4.19724 -11.700058,-4.79936 -2.516312,-0.29173 -8.749941,-0.12042 -11.778201,0.3237 -7.78194,1.14127 -17.39965,3.83907 -26.73341,7.49883 -3.38325,1.32658 -4.15525,1.50926 -5.11851,1.21125 z m 4.2107,-5.34052 c 5.86759,-2.29858 14.40398,-4.922695 20.2018,-6.210065 6.31584,-1.402418 8.5236,-1.646248 14.91592,-1.647338 4.68699,-7.94e-4 6.013661,0.0632 7.257809,0.3497 0.837332,0.19286 1.561052,0.312028 1.60828,0.264819 0.147111,-0.147119 -1.803289,-1.307431 -4.154879,-2.471801 -8.12511,-4.023029 -18.27311,-4.986568 -29.0861,-2.761718 -1.09536,0.22538 -2.32708,0.40827 -2.73715,0.406418 -1.12787,-0.005 -2.3054,-0.76382 -2.84516,-1.8332 l -0.46086,-0.913098 V 62.99179 35.97471 l -0.56331,0.138329 c -0.30981,0.07608 -1.89985,0.665075 -3.5334,1.308881 -2.27551,0.896801 -2.96414,1.252878 -2.94452,1.522563 0.014,0.193604 0.0372,15.284513 0.0512,33.535345 0.014,18.250839 0.0538,33.183322 0.0884,33.183322 0.0346,0 1.02543,-0.3771 2.20198,-0.83801 z m 113.006991,-32.697216 -0.0518,-33.535203 -3.17495,-1.272156 c -1.74623,-0.699685 -3.33627,-1.278755 -3.53341,-1.286819 -0.33966,-0.01389 -0.35847,1.401778 -0.35847,26.980216 v 26.994863 l -0.46087,0.913112 c -0.53976,1.06939 -1.71729,1.828088 -2.84515,1.833189 -0.41008,0.0021 -1.6418,-0.181031 -2.73716,-0.406421 -11.888201,-2.446089 -22.84337,-1.046438 -31.491022,4.02332 -1.68175,0.985941 -2.216748,1.467501 -1.36534,1.228942 1.575181,-0.441362 4.990592,-0.73864 8.524862,-0.742011 5.954408,-0.005 11.43046,0.791951 19.10874,2.78333 3.9516,1.024874 12.1555,3.687454 15.6699,5.085704 1.23926,0.49306 2.36869,0.90517 2.50985,0.9158 0.20489,0.0155 0.2462,-6.745894 0.20483,-33.515866 z m -59.76135,-2.233777 V 40.065438 l -0.95972,-1.357442 c -1.380522,-1.952627 -5.376262,-5.847994 -7.64336,-7.45136 -3.778692,-2.672401 -9.063392,-4.943324 -13.672511,-5.875304 -3.19731,-0.646503 -5.23069,-0.833103 -9.05886,-0.831312 -4.37716,0.0021 -7.70223,0.349169 -11.83461,1.235469 l -1.07538,0.230645 v 31.242342 c 0,26.565778 0.0426,31.226011 0.28429,31.133261 0.15637,-0.06 1.42379,-0.297169 2.81648,-0.527026 12.37657,-2.042634 23.21658,-0.346861 32.521639,5.087596 2.10018,1.226558 5.20202,3.618878 6.880942,5.30692 0.788609,0.792909 1.502978,1.446609 1.587468,1.452679 0.0845,0.006 0.153622,-13.411893 0.153622,-29.817719 z m 5.80221,28.3766 c 6.21476,-6.141601 15.08488,-10.061509 25.025529,-11.05933 4.262419,-0.427849 11.579921,-0.0054 16.017661,0.924912 0.75932,0.15916 1.45259,0.244888 1.54058,0.190498 0.088,-0.05434 0.16003,-14.060382 0.16003,-31.124436 V 26.176883 l -0.52136,-0.198219 c -0.66893,-0.254325 -4.77649,-0.95482 -7.159981,-1.221048 -2.41372,-0.269605 -8.559851,-0.266589 -10.759229,0.0052 -6.458111,0.798299 -12.584091,3.083792 -17.405651,6.49374 -2.267091,1.603366 -6.262831,5.498733 -7.64336,7.45136 l -0.959721,1.357438 v 29.828747 c 0,16.405812 0.0532,29.828746 0.11802,29.828746 0.065,0 0.77928,-0.65347 1.587482,-1.452149 z" id="path845" inkscape:connector-curvature="0" sodipodi:nodetypes="csscccscsssscsssssscscsccsccsccscsscccccccscccccccccsccscscscccscccsccssccsscccscccccsccccsccscsccsscc"/>
+ </g>
+</svg>
diff --git a/docs/_static/style.css b/docs/_static/style.css
new file mode 100644
index 0000000..b2bc297
--- /dev/null
+++ b/docs/_static/style.css
@@ -0,0 +1,45 @@
+@import url('https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,700;0,900;1,400;1,700;1,900&family=Open+Sans:ital,wght@0,400;0,600;1,400;1,600&display=swap');
+
+body {
+ font-family: 'Open Sans', sans-serif;
+}
+
+h1 {
+ font-family: "Lato", sans-serif;
+}
+
+pre, code {
+ font-size: 100%;
+ line-height: 155%;
+}
+
+/* Main page overview cards */
+
+.sd-card {
+ border-radius: 0;
+ padding: 30px 10px 20px 10px;
+ margin: 10px 0px;
+}
+
+.sd-card .sd-card-header {
+ text-align: center;
+}
+
+.sd-card .sd-card-header .sd-card-text {
+ margin: 0px;
+}
+
+.sd-card .sd-card-img-top {
+ height: 52px;
+ width: 52px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.sd-card .sd-card-header {
+ border: none;
+ color: #150458 !important;
+ font-size: var(--pst-font-size-h5);
+ font-weight: bold;
+ padding: 2.5rem 0rem 0.5rem 0rem;
+}
diff --git a/docs/_themes/.gitignore b/docs/_themes/.gitignore
deleted file mode 100644
index 66b6e4c..0000000
--- a/docs/_themes/.gitignore
+++ /dev/null
@@ -1,3 +0,0 @@
-*.pyc
-*.pyo
-.DS_Store
diff --git a/docs/_themes/LICENSE b/docs/_themes/LICENSE
deleted file mode 100644
index 8daab7e..0000000
--- a/docs/_themes/LICENSE
+++ /dev/null
@@ -1,37 +0,0 @@
-Copyright (c) 2010 by Armin Ronacher.
-
-Some rights reserved.
-
-Redistribution and use in source and binary forms of the theme, with or
-without modification, are permitted provided that the following conditions
-are met:
-
-* Redistributions of source code must retain the above copyright
- notice, this list of conditions and the following disclaimer.
-
-* Redistributions in binary form must reproduce the above
- copyright notice, this list of conditions and the following
- disclaimer in the documentation and/or other materials provided
- with the distribution.
-
-* The names of the contributors may not be used to endorse or
- promote products derived from this software without specific
- prior written permission.
-
-We kindly ask you to only use these themes in an unmodified manner just
-for Flask and Flask-related products, not for unrelated projects. If you
-like the visual style and want to use it for your own projects, please
-consider making some larger changes to the themes (such as changing
-font faces, sizes, colors or margins).
-
-THIS THEME IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
-AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
-IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
-ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
-LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
-CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
-SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
-INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
-CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-ARISING IN ANY WAY OUT OF THE USE OF THIS THEME, EVEN IF ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
diff --git a/docs/_themes/README b/docs/_themes/README
deleted file mode 100644
index b3292bd..0000000
--- a/docs/_themes/README
+++ /dev/null
@@ -1,31 +0,0 @@
-Flask Sphinx Styles
-===================
-
-This repository contains sphinx styles for Flask and Flask related
-projects. To use this style in your Sphinx documentation, follow
-this guide:
-
-1. put this folder as _themes into your docs folder. Alternatively
- you can also use git submodules to check out the contents there.
-2. add this to your conf.py:
-
- sys.path.append(os.path.abspath('_themes'))
- html_theme_path = ['_themes']
- html_theme = 'flask'
-
-The following themes exist:
-
-- 'flask' - the standard flask documentation theme for large
- projects
-- 'flask_small' - small one-page theme. Intended to be used by
- very small addon libraries for flask.
-
-The following options exist for the flask_small theme:
-
- [options]
- index_logo = '' filename of a picture in _static
- to be used as replacement for the
- h1 in the index.rst file.
- index_logo_height = 120px height of the index logo
- github_fork = '' repository name on github for the
- "fork me" badge
diff --git a/docs/_themes/flask/layout.html b/docs/_themes/flask/layout.html
deleted file mode 100644
index 4c79a9a..0000000
--- a/docs/_themes/flask/layout.html
+++ /dev/null
@@ -1,30 +0,0 @@
-{%- extends "basic/layout.html" %}
-{%- block extrahead %}
- {{ super() }}
- {% if theme_touch_icon %}
- <link rel="apple-touch-icon" href="{{ pathto('_static/' ~ theme_touch_icon, 1) }}" />
- {% endif %}
- <link media="only screen and (max-device-width: 480px)" href="{{
- pathto('_static/small_flask.css', 1) }}" type= "text/css" rel="stylesheet" />
-{% endblock %}
-{%- block relbar2 %}
- {% if theme_github_fork %}
- <a href="http://github.com/{{ theme_github_fork }}"><img style="position: fixed; top: 0; right: 0; border: 0;"
- src="http://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png" alt="Fork me on GitHub" /></a>
- {% endif %}
-{% endblock %}
-{% block header %}
- {{ super() }}
- {% if pagename == 'index' %}
- <div>
- {% endif %}
-{% endblock %}
-{%- block footer %}
- <div class="footer">
- &copy; Copyright {{ copyright }}. Pint {{ version }}.
- Created using <a href="http://sphinx.pocoo.org/">Sphinx</a>.
- </div>
- {% if pagename == 'index' %}
- </div>
- {% endif %}
-{%- endblock %}
diff --git a/docs/_themes/flask/relations.html b/docs/_themes/flask/relations.html
deleted file mode 100644
index ed433fa..0000000
--- a/docs/_themes/flask/relations.html
+++ /dev/null
@@ -1,20 +0,0 @@
-
-<h3>Related Topics</h3>
-<ul>
-<li><a href="{{ pathto(master_doc) }}">Documentation overview</a></li>
-{%- for parent in parents %}
-<li><a href="{{ parent.link|e }}">{{ parent.title }}</a><li>
-</ul>
-{%- endfor %}
-{%- if prev %}
- <p>
- <b>Previous</b><br/>
- <a href="{{ prev.link|e }}" title="{{ _('previous chapter') }}">{{ prev.title }}</a>
- </p>
-{%- endif %}
-{%- if next %}
- <p>
- <b>Next</b><br/>
- <a href="{{ next.link|e }}" title="{{ _('next chapter') }}">{{ next.title }}</a>
- </p>
-{%- endif %}
diff --git a/docs/_themes/flask/static/flasky.css_t b/docs/_themes/flask/static/flasky.css_t
deleted file mode 100644
index 4f78308..0000000
--- a/docs/_themes/flask/static/flasky.css_t
+++ /dev/null
@@ -1,395 +0,0 @@
-/*
- * flasky.css_t
- * ~~~~~~~~~~~~
- *
- * :copyright: Copyright 2010 by Armin Ronacher.
- * :license: Flask Design License, see LICENSE for details.
- */
-
-{% set page_width = '940px' %}
-{% set sidebar_width = '220px' %}
-
-@import url("basic.css");
-
-/* -- page layout ----------------------------------------------------------- */
-
-body {
- font-family: 'Georgia', serif;
- font-size: 17px;
- background-color: white;
- color: #000;
- margin: 0;
- padding: 0;
-}
-
-div.document {
- width: {{ page_width }};
- margin: 30px auto 0 auto;
-}
-
-div.documentwrapper {
- float: left;
- width: 100%;
-}
-
-div.bodywrapper {
- margin: 0 0 0 {{ sidebar_width }};
-}
-
-div.sphinxsidebar {
- width: {{ sidebar_width }};
-}
-
-hr {
- border: 1px solid #B1B4B6;
-}
-
-div.body {
- background-color: #ffffff;
- color: #3E4349;
- padding: 0 30px 0 30px;
-}
-
-img.floatingflask {
- padding: 0 0 10px 10px;
- float: right;
-}
-
-div.footer {
- width: {{ page_width }};
- margin: 20px auto 30px auto;
- font-size: 14px;
- color: #888;
- text-align: right;
-}
-
-div.footer a {
- color: #888;
-}
-
-div.related {
- display: none;
-}
-
-div.sphinxsidebar a {
- color: #444;
- text-decoration: none;
- border-bottom: 1px dotted #999;
-}
-
-div.sphinxsidebar a:hover {
- border-bottom: 1px solid #999;
-}
-
-div.sphinxsidebar {
- font-size: 14px;
- line-height: 1.5;
-}
-
-div.sphinxsidebarwrapper {
- padding: 18px 10px;
-}
-
-div.sphinxsidebarwrapper p.logo {
- padding: 0 0 20px 0;
- margin: 0;
- text-align: center;
-}
-
-div.sphinxsidebar h3,
-div.sphinxsidebar h4 {
- font-family: 'Garamond', 'Georgia', serif;
- color: #444;
- font-size: 24px;
- font-weight: normal;
- margin: 0 0 5px 0;
- padding: 0;
-}
-
-div.sphinxsidebar h4 {
- font-size: 20px;
-}
-
-div.sphinxsidebar h3 a {
- color: #444;
-}
-
-div.sphinxsidebar p.logo a,
-div.sphinxsidebar h3 a,
-div.sphinxsidebar p.logo a:hover,
-div.sphinxsidebar h3 a:hover {
- border: none;
-}
-
-div.sphinxsidebar p {
- color: #555;
- margin: 10px 0;
-}
-
-div.sphinxsidebar ul {
- margin: 10px 0;
- padding: 0;
- color: #000;
-}
-
-div.sphinxsidebar input {
- border: 1px solid #ccc;
- font-family: 'Georgia', serif;
- font-size: 1em;
-}
-
-/* -- body styles ----------------------------------------------------------- */
-
-a {
- color: #004B6B;
- text-decoration: underline;
-}
-
-a:hover {
- color: #6D4100;
- text-decoration: underline;
-}
-
-div.body h1,
-div.body h2,
-div.body h3,
-div.body h4,
-div.body h5,
-div.body h6 {
- font-family: 'Garamond', 'Georgia', serif;
- font-weight: normal;
- margin: 30px 0px 10px 0px;
- padding: 0;
-}
-
-{% if theme_index_logo %}
-div.indexwrapper h1 {
- text-indent: -999999px;
- background: url({{ theme_index_logo }}) no-repeat center center;
- height: {{ theme_index_logo_height }};
-}
-{% endif %}
-
-div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; }
-div.body h2 { font-size: 180%; }
-div.body h3 { font-size: 150%; }
-div.body h4 { font-size: 130%; }
-div.body h5 { font-size: 100%; }
-div.body h6 { font-size: 100%; }
-
-a.headerlink {
- color: #ddd;
- padding: 0 4px;
- text-decoration: none;
-}
-
-a.headerlink:hover {
- color: #444;
- background: #eaeaea;
-}
-
-div.body p, div.body dd, div.body li {
- line-height: 1.4em;
-}
-
-div.admonition {
- background: #fafafa;
- margin: 20px -30px;
- padding: 10px 30px;
- border-top: 1px solid #ccc;
- border-bottom: 1px solid #ccc;
-}
-
-div.admonition tt.xref, div.admonition a tt {
- border-bottom: 1px solid #fafafa;
-}
-
-dd div.admonition {
- margin-left: -60px;
- padding-left: 60px;
-}
-
-div.admonition p.admonition-title {
- font-family: 'Garamond', 'Georgia', serif;
- font-weight: normal;
- font-size: 24px;
- margin: 0 0 10px 0;
- padding: 0;
- line-height: 1;
-}
-
-div.admonition p.last {
- margin-bottom: 0;
-}
-
-div.highlight {
- background-color: white;
-}
-
-dt:target, .highlight {
- background: #FAF3E8;
-}
-
-div.note {
- background-color: #eee;
- border: 1px solid #ccc;
-}
-
-div.seealso {
- background-color: #ffc;
- border: 1px solid #ff6;
-}
-
-div.topic {
- background-color: #eee;
-}
-
-p.admonition-title {
- display: inline;
-}
-
-p.admonition-title:after {
- content: ":";
-}
-
-pre, tt {
- font-family: 'Consolas', 'Menlo', 'Deja Vu Sans Mono', 'Bitstream Vera Sans Mono', monospace;
- font-size: 0.9em;
-}
-
-img.screenshot {
-}
-
-tt.descname, tt.descclassname {
- font-size: 0.95em;
-}
-
-tt.descname {
- padding-right: 0.08em;
-}
-
-img.screenshot {
- -moz-box-shadow: 2px 2px 4px #eee;
- -webkit-box-shadow: 2px 2px 4px #eee;
- box-shadow: 2px 2px 4px #eee;
-}
-
-table.docutils {
- border: 1px solid #888;
- -moz-box-shadow: 2px 2px 4px #eee;
- -webkit-box-shadow: 2px 2px 4px #eee;
- box-shadow: 2px 2px 4px #eee;
-}
-
-table.docutils td, table.docutils th {
- border: 1px solid #888;
- padding: 0.25em 0.7em;
-}
-
-table.field-list, table.footnote {
- border: none;
- -moz-box-shadow: none;
- -webkit-box-shadow: none;
- box-shadow: none;
-}
-
-table.footnote {
- margin: 15px 0;
- width: 100%;
- border: 1px solid #eee;
- background: #fdfdfd;
- font-size: 0.9em;
-}
-
-table.footnote + table.footnote {
- margin-top: -15px;
- border-top: none;
-}
-
-table.field-list th {
- padding: 0 0.8em 0 0;
-}
-
-table.field-list td {
- padding: 0;
-}
-
-table.footnote td.label {
- width: 0px;
- padding: 0.3em 0 0.3em 0.5em;
-}
-
-table.footnote td {
- padding: 0.3em 0.5em;
-}
-
-dl {
- margin: 0;
- padding: 0;
-}
-
-dl dd {
- margin-left: 30px;
-}
-
-blockquote {
- margin: 0 0 0 30px;
- padding: 0;
-}
-
-ul, ol {
- margin: 10px 0 10px 30px;
- padding: 0;
-}
-
-pre {
- background: #eee;
- padding: 7px 30px;
- margin: 15px -30px;
- line-height: 1.3em;
-}
-
-dl pre, blockquote pre, li pre {
- margin-left: -60px;
- padding-left: 60px;
-}
-
-dl dl pre {
- margin-left: -90px;
- padding-left: 90px;
-}
-
-tt {
- background-color: #ecf0f3;
- color: #222;
- /* padding: 1px 2px; */
-}
-
-tt.xref, a tt {
- background-color: #FBFBFB;
- border-bottom: 1px solid white;
-}
-
-a.reference {
- text-decoration: none;
- border-bottom: 1px dotted #004B6B;
-}
-
-a.reference:hover {
- border-bottom: 1px solid #6D4100;
-}
-
-a.footnote-reference {
- text-decoration: none;
- font-size: 0.7em;
- vertical-align: top;
- border-bottom: 1px dotted #004B6B;
-}
-
-a.footnote-reference:hover {
- border-bottom: 1px solid #6D4100;
-}
-
-a:hover tt {
- background: #EEE;
-}
diff --git a/docs/_themes/flask/static/small_flask.css b/docs/_themes/flask/static/small_flask.css
deleted file mode 100644
index 1c6df30..0000000
--- a/docs/_themes/flask/static/small_flask.css
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * small_flask.css_t
- * ~~~~~~~~~~~~~~~~~
- *
- * :copyright: Copyright 2010 by Armin Ronacher.
- * :license: Flask Design License, see LICENSE for details.
- */
-
-body {
- margin: 0;
- padding: 20px 30px;
-}
-
-div.documentwrapper {
- float: none;
- background: white;
-}
-
-div.sphinxsidebar {
- display: block;
- float: none;
- width: 102.5%;
- margin: 50px -30px -20px -30px;
- padding: 10px 20px;
- background: #333;
- color: white;
-}
-
-div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p,
-div.sphinxsidebar h3 a {
- color: white;
-}
-
-div.sphinxsidebar a {
- color: #aaa;
-}
-
-div.sphinxsidebar p.logo {
- display: none;
-}
-
-div.document {
- width: 100%;
- margin: 0;
-}
-
-div.related {
- display: block;
- margin: 0;
- padding: 10px 0 20px 0;
-}
-
-div.related ul,
-div.related ul li {
- margin: 0;
- padding: 0;
-}
-
-div.footer {
- display: none;
-}
-
-div.bodywrapper {
- margin: 0;
-}
-
-div.body {
- min-height: 0;
- padding: 0;
-}
diff --git a/docs/_themes/flask/theme.conf b/docs/_themes/flask/theme.conf
deleted file mode 100644
index 0b3d313..0000000
--- a/docs/_themes/flask/theme.conf
+++ /dev/null
@@ -1,10 +0,0 @@
-[theme]
-inherit = basic
-stylesheet = flasky.css
-pygments_style = flask_theme_support.FlaskyStyle
-
-[options]
-index_logo = ''
-index_logo_height = 120px
-touch_icon =
-github_fork = hgrecco/pint
diff --git a/docs/_themes/flask_theme_support.py b/docs/_themes/flask_theme_support.py
deleted file mode 100644
index 64e2499..0000000
--- a/docs/_themes/flask_theme_support.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# flasky extensions. flasky pygments style based on tango style
-from pygments.style import Style
-from pygments.token import (
- Comment,
- Error,
- Generic,
- Keyword,
- Literal,
- Name,
- Number,
- Operator,
- Other,
- Punctuation,
- String,
- Whitespace,
-)
-
-
-class FlaskyStyle(Style):
- background_color = "#f8f8f8"
- default_style = ""
-
- styles = {
- # No corresponding class for the following:
- # Text: "", # class: ''
- Whitespace: "underline #f8f8f8", # class: 'w'
- Error: "#a40000 border:#ef2929", # class: 'err'
- Other: "#000000", # class 'x'
- Comment: "italic #8f5902", # class: 'c'
- Comment.Preproc: "noitalic", # class: 'cp'
- Keyword: "bold #004461", # class: 'k'
- Keyword.Constant: "bold #004461", # class: 'kc'
- Keyword.Declaration: "bold #004461", # class: 'kd'
- Keyword.Namespace: "bold #004461", # class: 'kn'
- Keyword.Pseudo: "bold #004461", # class: 'kp'
- Keyword.Reserved: "bold #004461", # class: 'kr'
- Keyword.Type: "bold #004461", # class: 'kt'
- Operator: "#582800", # class: 'o'
- Operator.Word: "bold #004461", # class: 'ow' - like keywords
- Punctuation: "bold #000000", # class: 'p'
- # because special names such as Name.Class, Name.Function, etc.
- # are not recognized as such later in the parsing, we choose them
- # to look the same as ordinary variables.
- Name: "#000000", # class: 'n'
- Name.Attribute: "#c4a000", # class: 'na' - to be revised
- Name.Builtin: "#004461", # class: 'nb'
- Name.Builtin.Pseudo: "#3465a4", # class: 'bp'
- Name.Class: "#000000", # class: 'nc' - to be revised
- Name.Constant: "#000000", # class: 'no' - to be revised
- Name.Decorator: "#888", # class: 'nd' - to be revised
- Name.Entity: "#ce5c00", # class: 'ni'
- Name.Exception: "bold #cc0000", # class: 'ne'
- Name.Function: "#000000", # class: 'nf'
- Name.Property: "#000000", # class: 'py'
- Name.Label: "#f57900", # class: 'nl'
- Name.Namespace: "#000000", # class: 'nn' - to be revised
- Name.Other: "#000000", # class: 'nx'
- Name.Tag: "bold #004461", # class: 'nt' - like a keyword
- Name.Variable: "#000000", # class: 'nv' - to be revised
- Name.Variable.Class: "#000000", # class: 'vc' - to be revised
- Name.Variable.Global: "#000000", # class: 'vg' - to be revised
- Name.Variable.Instance: "#000000", # class: 'vi' - to be revised
- Number: "#990000", # class: 'm'
- Literal: "#000000", # class: 'l'
- Literal.Date: "#000000", # class: 'ld'
- String: "#4e9a06", # class: 's'
- String.Backtick: "#4e9a06", # class: 'sb'
- String.Char: "#4e9a06", # class: 'sc'
- String.Doc: "italic #8f5902", # class: 'sd' - like a comment
- String.Double: "#4e9a06", # class: 's2'
- String.Escape: "#4e9a06", # class: 'se'
- String.Heredoc: "#4e9a06", # class: 'sh'
- String.Interpol: "#4e9a06", # class: 'si'
- String.Other: "#4e9a06", # class: 'sx'
- String.Regex: "#4e9a06", # class: 'sr'
- String.Single: "#4e9a06", # class: 's1'
- String.Symbol: "#4e9a06", # class: 'ss'
- Generic: "#000000", # class: 'g'
- Generic.Deleted: "#a40000", # class: 'gd'
- Generic.Emph: "italic #000000", # class: 'ge'
- Generic.Error: "#ef2929", # class: 'gr'
- Generic.Heading: "bold #000080", # class: 'gh'
- Generic.Inserted: "#00A000", # class: 'gi'
- Generic.Output: "#888", # class: 'go'
- Generic.Prompt: "#745334", # class: 'gp'
- Generic.Strong: "bold #000000", # class: 'gs'
- Generic.Subheading: "bold #800080", # class: 'gu'
- Generic.Traceback: "bold #a40000", # class: 'gt'
- }
diff --git a/docs/currencies.rst b/docs/advanced/currencies.rst
index 26b66b5..26b66b5 100644
--- a/docs/currencies.rst
+++ b/docs/advanced/currencies.rst
diff --git a/docs/advanced/custom-registry-class.rst b/docs/advanced/custom-registry-class.rst
new file mode 100644
index 0000000..31f3d76
--- /dev/null
+++ b/docs/advanced/custom-registry-class.rst
@@ -0,0 +1,83 @@
+.. _custom_registry_class:
+
+Custom registry class
+=====================
+
+Pay as you go
+-------------
+
+Pint registry functionality is divided into facets. The default
+UnitRegistry inherits from all of them, providing a full fledged
+and feature rich registry. However, in certain cases you might want
+to have a simpler and light registry. Just pick what you need
+and create your own.
+
+- FormattingRegistry: adds the capability to format quantities and units into string.
+- SystemRegistry: adds the capability to work with system of units.
+- GroupRegistry: adds the capability to group units.
+- MeasurementRegistry: adds the capability to handle measurements (quantities with uncertainties).
+- NumpyRegistry: adds the capability to interoperate with NumPy.
+- DaskRegistry: adds the capability to interoperate with Dask.
+- ContextRegistry: the capability to contexts: predefined conversions
+ between incompatible dimensions.
+- NonMultiplicativeRegistry: adds the capability to handle nonmultiplicative units (offset, logarithmic).
+- PlainRegistry: base implementation for registry, units and quantities.
+
+The only required one is `PlainRegistry`, the rest are completely
+optional.
+
+For example:
+
+.. doctest::
+
+ >>> import pint
+ >>> class MyRegistry(pint.facets.NonMultiplicativeRegistry, pint.facets.PlainRegistry):
+ ... pass
+
+
+Subclassing
+-----------
+
+If you want to add the default registry class some specific functionality,
+you can subclass it:
+
+.. doctest::
+
+ >>> import pint
+ >>> class MyRegistry(pint.UnitRegistry):
+ ...
+ ... def my_specific_function(self):
+ ... """Do something
+ ... """
+
+
+If you want to create your own Quantity class, you must tell
+your registry about it:
+
+.. doctest::
+
+ >>> import pint
+ >>> class MyQuantity:
+ ...
+ ... # Notice that subclassing pint.Quantity
+ ... # is not necessary.
+ ... # Pint will inspect the Registry class and create
+ ... # a Quantity class that contains all the
+ ... # required parents.
+ ...
+ ... def to_my_desired_format(self):
+ ... """Do something else
+ ... """
+ >>>
+ >>> class MyRegistry(pint.UnitRegistry):
+ ...
+ ... _quantity_class = MyQuantity
+ ...
+ ... # The same you can be done with
+ ... # _unit_class
+ ... # _measurement_class
+
+
+While these examples demonstrate how to add functionality to the default
+registry class, you can actually subclass just the PlainRegistry or any
+combination of facets.
diff --git a/docs/defining.rst b/docs/advanced/defining.rst
index 43344bc..43344bc 100644
--- a/docs/defining.rst
+++ b/docs/advanced/defining.rst
diff --git a/docs/advanced/index.rst b/docs/advanced/index.rst
new file mode 100644
index 0000000..e013321
--- /dev/null
+++ b/docs/advanced/index.rst
@@ -0,0 +1,17 @@
+Advanced Guides
+===============
+
+Pint contains some useful and fun feature. You will find them here.
+
+.. toctree::
+ :maxdepth: 2
+ :hidden:
+
+ performance
+ serialization
+ defining
+ wrapping
+ measurement
+ pitheorem
+ currencies
+ custom-registry-class
diff --git a/docs/measurement.rst b/docs/advanced/measurement.rst
index a49c821..a49c821 100644
--- a/docs/measurement.rst
+++ b/docs/advanced/measurement.rst
diff --git a/docs/performance.rst b/docs/advanced/performance.rst
index 1dbd27d..d7b8a0c 100644
--- a/docs/performance.rst
+++ b/docs/advanced/performance.rst
@@ -11,51 +11,53 @@ Pint can impose a significant performance overhead on computationally-intensive
Use magnitudes when possible
----------------------------
-It's significantly faster to perform mathematical operations on magnitudes (even though your'e still using pint to retrieve them from a quantity object).
+It's significantly faster to perform mathematical operations on magnitudes (even though you're still using pint to retrieve them from a quantity object).
-.. doctest::
+.. ipython::
+ :verbatim:
- In [1]: from pint import UnitRegistry
+ In [1]: from pint import UnitRegistry
- In [2]: ureg = UnitRegistry()
+ In [2]: ureg = UnitRegistry()
- In [3]: q1 =ureg('1m')
+ In [3]: q1 = ureg('1m')
- In [5]: q2=ureg('2m')
+ In [5]: q2 = ureg('2m')
- In [6]: %timeit (q1-q2)
- 100000 loops, best of 3: 7.9 µs per loop
+ In [6]: %timeit (q1 - q2)
+ 8.24 us +- 44.5 ns per loop (mean +- std. dev. of 7 runs, 100,000 loops each)
- In [7]: %timeit (q1.magnitude-q2.magnitude)
- 1000000 loops, best of 3: 356 ns per loop
+ In [7]: %timeit (q1.magnitude - q2.magnitude)
+ 214 ns +- 2.39 ns per loop (mean +- std. dev. of 7 runs, 1,000,000 loops each)
This is especially important when using pint Quantities in conjunction with an iterative solver, such as the `brentq method`_ from scipy:
-.. doctest::
+.. ipython::
+ :verbatim:
In [1]: from scipy.optimize import brentq
In [2]: def foobar_with_quantity(x):
- # find the value of x that equals q2
-
- # assign x the same units as q2
- qx = ureg(str(x)+str(q2.units))
-
- # compare the two quantities, then take their magnitude because
- # brentq requires a dimensionless return type
- return (qx - q2).magnitude
+ ...: # find the value of x that equals q2
+ ...:
+ ...: # assign x the same units as q2
+ ...: qx = ureg(str(x)+str(q2.units))
+ ...:
+ ...: # compare the two quantities, then take their magnitude because
+ ...: # brentq requires a dimensionless return type
+ ...: return (qx - q2).magnitude
In [3]: def foobar_with_magnitude(x):
- # find the value of x that equals q2
-
- # don't bother converting x to a quantity, just compare it with q2's magnitude
- return x - q2.magnitude
+ ...: # find the value of x that equals q2
+ ...:
+ ...: # don't bother converting x to a quantity, just compare it with q2's magnitude
+ ...: return x - q2.magnitude
In [4]: %timeit brentq(foobar_with_quantity,0,q2.magnitude)
- 1000 loops, best of 3: 310 µs per loop
+ 286 us +- 9.05 us per loop (mean +- std. dev. of 7 runs, 1,000 loops each)
In [5]: %timeit brentq(foobar_with_magnitude,0,q2.magnitude)
- 1000000 loops, best of 3: 1.63 µs per loop
+ 1.14 us +- 21.3 ns per loop (mean +- std. dev. of 7 runs, 1,000,000 loops each)
Bear in mind that altering computations like this **loses the benefits of automatic unit conversion**, so use with care.
@@ -63,7 +65,7 @@ A safer method: wrapping
------------------------
A better way to use magnitudes is to use pint's wraps decorator (See :ref:`wrapping`). By decorating a function with wraps, you pass only the magnitude of an argument to the function body according to units you specify. As such this method is safer in that you are sure the magnitude is supplied in the correct units.
-.. doctest::
+.. ipython::
In [1]: import pint
@@ -72,11 +74,11 @@ A better way to use magnitudes is to use pint's wraps decorator (See :ref:`wrapp
In [3]: import numpy as np
In [4]: def f(x, y):
- return (x - y) / (x + y) * np.log(x/y)
+ ...: return (x - y) / (x + y) * np.log(x/y)
In [5]: @ureg.wraps(None, ('meter', 'meter'))
- def g(x, y):
- return (x - y) / (x + y) * np.log(x/y)
+ ...: def g(x, y):
+ ...: return (x - y) / (x + y) * np.log(x/y)
In [6]: a = 1 * ureg.meter
diff --git a/docs/pitheorem.rst b/docs/advanced/pitheorem.rst
index cd37165..cd37165 100644
--- a/docs/pitheorem.rst
+++ b/docs/advanced/pitheorem.rst
diff --git a/docs/serialization.rst b/docs/advanced/serialization.rst
index 01fed50..581bc9d 100644
--- a/docs/serialization.rst
+++ b/docs/advanced/serialization.rst
@@ -1,5 +1,3 @@
-.. _serialization:
-
Serialization
=============
diff --git a/docs/wrapping.rst b/docs/advanced/wrapping.rst
index d04b0ba..21d8177 100644
--- a/docs/wrapping.rst
+++ b/docs/advanced/wrapping.rst
@@ -145,7 +145,7 @@ the extra outputs. For example, given the NREL SOLPOS calculator that outputs
solar zenith, azimuth and air mass, the following wrapper assumes no units for
airmass
-.. doctest::
+.. code-block:: python
@ureg.wraps(('deg', 'deg'), ('deg', 'deg', 'millibar', 'degC'))
def solar_position(lat, lon, press, tamb, timestamp):
@@ -232,7 +232,7 @@ To avoid the conversion of an argument or return value, use None
Checking dimensionality
-=======================
+-----------------------
When you want pint quantities to be used as inputs to your functions, pint provides a wrapper to ensure units are of
correct type - or more precisely, they match the expected dimensionality of the physical quantity.
diff --git a/docs/api/base.rst b/docs/api/base.rst
new file mode 100644
index 0000000..3e859fa
--- /dev/null
+++ b/docs/api/base.rst
@@ -0,0 +1,70 @@
+
+Base API
+========
+
+.. currentmodule:: pint
+
+
+These are the classes, exceptions and functions that you will most likely use.
+
+
+Most important classes
+----------------------
+
+.. autoclass:: UnitRegistry
+ :members:
+ :exclude-members: Quantity, Unit, Measurement, Group, Context, System
+
+.. autoclass:: Quantity
+ :members:
+ :inherited-members:
+
+.. autoclass:: Unit
+ :members:
+ :inherited-members:
+
+.. autoclass:: Measurement
+ :members:
+ :inherited-members:
+
+Exceptions and warnings
+-----------------------
+
+.. autoexception:: PintError
+ :members:
+
+.. autoexception:: DefinitionSyntaxError
+ :members:
+
+.. autoexception:: LogarithmicUnitCalculusError
+ :members:
+
+.. autoexception:: DimensionalityError
+ :members:
+
+.. autoexception:: OffsetUnitCalculusError
+ :members:
+
+.. autoexception:: RedefinitionError
+ :members:
+
+.. autoexception:: UndefinedUnitError
+ :members:
+
+.. autoexception:: UnitStrippedWarning
+ :members:
+
+
+Sharing registry among packages
+-------------------------------
+
+.. autofunction:: get_application_registry
+.. autofunction:: set_application_registry
+
+Other functions
+---------------
+
+.. autofunction:: formatter
+.. autofunction:: register_unit_format
+.. autofunction:: pi_theorem
+.. autoclass:: Context
diff --git a/docs/api/facets.rst b/docs/api/facets.rst
new file mode 100644
index 0000000..f4b6a54
--- /dev/null
+++ b/docs/api/facets.rst
@@ -0,0 +1,45 @@
+API facets reference
+====================
+
+Registry functionality is divided into facet. Each provides classes and functions
+specific to a particular purpose. They expose at least a Registry, and in certain
+cases also a Quantity, Unit and other objects.
+
+The default UnitRegistry inherits from all of them.
+
+
+.. automodule:: pint.facets.plain
+ :members:
+ :exclude-members: Quantity, Unit, Measurement, Group, Context, System
+
+.. automodule:: pint.facets.nonmultiplicative
+ :members:
+ :exclude-members: Quantity, Unit, Measurement, Group, Context, System
+
+.. automodule:: pint.facets.formatting
+ :members:
+ :exclude-members: Quantity, Unit, Measurement, Group, Context, System
+
+.. automodule:: pint.facets.numpy
+ :members:
+ :exclude-members: Quantity, Unit, Measurement, Group, Context, System
+
+.. automodule:: pint.facets.dask
+ :members:
+ :exclude-members: Quantity, Unit, Measurement, Group, Context, System
+
+.. automodule:: pint.facets.measurement
+ :members:
+ :exclude-members: Quantity, Unit, Measurement, Group, Context, System
+
+.. automodule:: pint.facets.group
+ :members:
+ :exclude-members: Quantity, Unit, Measurement, Group, Context, System
+
+.. automodule:: pint.facets.system
+ :members:
+ :exclude-members: Quantity, Unit, Measurement, Group, Context, System
+
+.. automodule:: pint.facets.context
+ :members:
+ :exclude-members: Quantity, Unit, Measurement, Group, Context, System
diff --git a/docs/api/index.rst b/docs/api/index.rst
new file mode 100644
index 0000000..212a02d
--- /dev/null
+++ b/docs/api/index.rst
@@ -0,0 +1,10 @@
+API reference
+==============
+
+.. toctree::
+ :maxdepth: 1
+ :hidden:
+
+ base
+ specific
+ facets
diff --git a/docs/api/specific.rst b/docs/api/specific.rst
new file mode 100644
index 0000000..f0343c1
--- /dev/null
+++ b/docs/api/specific.rst
@@ -0,0 +1,44 @@
+Specific API
+============
+
+These are the classes, exceptions and functions that you will use if you need
+to dig deeper into Pint or develop Pint.
+
+
+.. automodule:: pint.babel_names
+ :members:
+
+.. automodule:: pint.compat
+ :members:
+
+.. automodule:: pint.converters
+ :members:
+
+.. automodule:: pint.definitions
+ :members:
+
+.. automodule:: pint.errors
+ :members:
+
+.. automodule:: pint.formatting
+ :members:
+
+.. automodule:: pint.matplotlib
+ :members:
+
+.. automodule:: pint.pint_eval
+ :members:
+
+.. automodule:: pint.registry
+ :members:
+ :exclude-members: Quantity, Unit, Measurement, Group, Context, System, UnitRegistry
+
+.. automodule:: pint.registry_helpers
+ :members:
+
+.. automodule:: pint.testing
+ :members:
+
+.. automodule:: pint.util
+ :members:
+ :exclude-members: Unit
diff --git a/docs/conf.py b/docs/conf.py
index 4f0dfa6..ee74481 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -12,12 +12,7 @@
# serve to show the default.
import datetime
-
-try:
- from importlib.metadata import version
-except ImportError:
- # Backport for Python < 3.8
- from importlib_metadata import version
+from importlib.metadata import version
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
@@ -33,6 +28,7 @@ except ImportError:
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
"sphinx.ext.autodoc",
+ "sphinx.ext.autosummary",
"sphinx.ext.autosectionlabel",
"sphinx.ext.doctest",
"sphinx.ext.intersphinx",
@@ -42,8 +38,10 @@ extensions = [
"sphinx.ext.mathjax",
"matplotlib.sphinxext.plot_directive",
"nbsphinx",
+ "sphinx_copybutton",
"IPython.sphinxext.ipython_directive",
"IPython.sphinxext.ipython_console_highlighting",
+ "sphinx_design",
]
# Add any paths that contain templates here, relative to this directory.
@@ -75,136 +73,78 @@ except Exception: # pragma: no cover
release = version
this_year = datetime.date.today().year
-copyright = "%s, %s" % (this_year, author)
-
-# The language for content autogenerated by Sphinx. Refer to documentation
-# for a list of supported languages.
-# language = None
-
-# There are two options for replacing |today|: either, you set today to some
-# non-false value, then it is used:
-# today = ''
-# Else, today_fmt is used as the format for a strftime call.
-# today_fmt = '%B %d, %Y'
+copyright = f"2012-{this_year}, Pint Developers"
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
exclude_patterns = ["_build"]
-# The reST default role (used for this markup: `text`) to use for all documents.
-# default_role = None
-
-# If true, '()' will be appended to :func: etc. cross-reference text.
-# add_function_parentheses = True
-
-# If true, the current module name will be prepended to all description
-# unit titles (such as .. function::).
-# add_module_names = True
-
-# If true, sectionauthor and moduleauthor directives will be shown in the
-# output. They are ignored by default.
-# show_authors = False
+# Napoleon configurations
-# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = "sphinx"
-
-# A list of ignored prefixes for module index sorting.
-# modindex_common_prefix = []
-
-# -- Options for extensions ----------------------------------------------------
-# napoleon
+napoleon_google_docstring = False
+napoleon_numpy_docstring = True
+napoleon_use_param = False
+napoleon_use_rtype = False
napoleon_preprocess_types = True
+napoleon_type_aliases = {
+ # general terms
+ "sequence": ":term:`sequence`",
+ "iterable": ":term:`iterable`",
+ "callable": ":py:func:`callable`",
+ "dict_like": ":term:`dict-like <mapping>`",
+ "path-like": ":term:`path-like <path-like object>`",
+ "mapping": ":term:`mapping`",
+ "file-like": ":term:`file-like <file-like object>`",
+ # stdlib type aliases
+ "timedelta": "~datetime.timedelta",
+ "datetime": "~datetime.datetime",
+ "string": ":class:`string <str>`",
+ "Path": "~pathlib.Path",
+ # numpy terms
+ "array_like": ":term:`array_like`",
+ "array-like": ":term:`array-like <array_like>`",
+ "scalar": ":term:`scalar`",
+ "array": ":term:`array`",
+ "hashable": ":term:`hashable <name>`",
+ # objects without namespace: pint
+ "Quantity": "~pint.Quantity",
+ "Unit": "~pint.Unit",
+ "UnitsContainer": "~pint.UnitsContainer",
+ # objects without namespace: numpy
+ "ndarray": "~numpy.ndarray",
+ "MaskedArray": "~numpy.ma.MaskedArray",
+ "dtype": "~numpy.dtype",
+}
-# -- Options for HTML output ---------------------------------------------------
-
-# The theme to use for HTML and HTML Help pages. See the documentation for
-# a list of builtin themes.
-# html_theme = 'default'
-html_theme = "flask"
-
-# Theme options are theme-specific and customize the look and feel of a theme
-# further. For a list of options available for each theme, see the
-# documentation.
-# html_theme_options = {}
-
-# Add any paths that contain custom themes here, relative to this directory.
-# html_theme_path = []
-html_theme_path = ["_themes"]
-
-# The name for this set of Sphinx documents. If None, it defaults to
-# "<project> v<release> documentation".
-# html_title = None
-
-# A shorter title for the navigation bar. Default is the same as html_title.
-# html_short_title = None
+html_theme = "sphinx_book_theme"
-# The name of an image file (relative to this directory) to place at the top
-# of the sidebar.
-# html_logo = None
+html_theme_options = {
+ "repository_url": "https://github.com/hgrecco/pint",
+ "repository_branch": "master",
+ "use_repository_button": True,
+ "use_issues_button": True,
+ "extra_navbar": "",
+ "navbar_footer_text": "",
+}
-# The name of an image file (within the static path) to use as favicon of the
-# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
-# pixels large.
-# html_favicon = None
+html_logo = "_static/logo-full.jpg"
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]
-
-# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
-# using the given strftime format.
-# html_last_updated_fmt = '%b %d, %Y'
-
-# If true, SmartyPants will be used to convert quotes and dashes to
-# typographically correct entities.
-# html_use_smartypants = True
+html_css_files = ["style.css"]
# Custom sidebar templates, maps document names to template names.
# html_sidebars = {}
-html_sidebars = {
- "index": ["sidebarintro.html", "sourcelink.html", "searchbox.html"],
- "**": [
- "sidebarlogo.html",
- "localtoc.html",
- "relations.html",
- "sourcelink.html",
- "searchbox.html",
- ],
-}
-
-# Additional templates that should be rendered to pages, maps page names to
-# template names.
-# html_additional_pages = {}
-
-# If false, no module index is generated.
-# html_domain_indices = True
-
-# If false, no index is generated.
-# html_use_index = True
-
-# If true, the index is split into individual pages for each letter.
-# html_split_index = False
-
-# If true, links to the reST sources are added to the pages.
-# html_show_sourcelink = True
-
-# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
-# html_show_sphinx = True
-
-# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
-# html_show_copyright = True
-
-# If true, an OpenSearch description file will be output, and all pages will
-# contain a <link> tag referring to it. The value of this option must be the
-# plain URL from which the finished HTML is served.
-# html_use_opensearch = ''
-
-# This is the file name suffix for HTML files (e.g. ".xhtml").
-# html_file_suffix = None
-
-# Output file plain name for HTML help builder.
+# html_sidebars = {
+# "index": ["sidebarintro.html", "sourcelink.html", "searchbox.html"],
+# "**": [
+# "sidebarlogo.html",
+# "localtoc.html",
+# "relations.html",
+# "sourcelink.html",
+# "searchbox.html",
+# ],
+# }
+
+# Output file base name for HTML help builder.
htmlhelp_basename = "pintdoc"
@@ -225,26 +165,6 @@ latex_documents = [
("index", "pint.tex", "pint Documentation", "Hernan E. Grecco", "manual")
]
-# The name of an image file (relative to this directory) to place at the top of
-# the title page.
-# latex_logo = None
-
-# For "manual" documents, if this is true, then toplevel headings are parts,
-# not chapters.
-# latex_use_parts = False
-
-# If true, show page references after internal links.
-# latex_show_pagerefs = False
-
-# If true, show URL addresses after external links.
-# latex_show_urls = False
-
-# Documents to append as an appendix to all manuals.
-# latex_appendices = []
-
-# If false, no module index is generated.
-# latex_domain_indices = True
-
# -- Options for manual page output --------------------------------------------
@@ -291,45 +211,12 @@ epub_author = author
epub_publisher = author
epub_copyright = copyright
-# The language of the text. It defaults to the language option
-# or en if the language is not set.
-# epub_language = ''
-
-# The scheme of the identifier. Typical schemes are ISBN or URL.
-# epub_scheme = ''
-
-# The unique identifier of the text. This can be a ISBN number
-# or the project homepage.
-# epub_identifier = ''
-
-# A unique identification for the text.
-# epub_uid = ''
-
-# A tuple containing the cover image and cover page html template filenames.
-# epub_cover = ()
-
-# HTML files that should be inserted before the pages created by sphinx.
-# The format is a list of tuples containing the path and title.
-# epub_pre_files = []
-
-# HTML files shat should be inserted after the pages created by sphinx.
-# The format is a list of tuples containing the path and title.
-# epub_post_files = []
-
-# A list of files that should not be packed into the epub file.
-# epub_exclude_files = []
-
-# The depth of the table of contents in toc.ncx.
-# epub_tocdepth = 3
-
-# Allow duplicate toc entries.
-# epub_tocdup = True
-
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
"python": ("https://docs.python.org/3/", None),
"numpy": ("https://numpy.org/doc/stable", None),
+ "scipy": ("https://docs.scipy.org/doc/scipy", None),
"matplotlib": ("https://matplotlib.org/stable/", None),
"dask": ("https://docs.dask.org/en/latest", None),
"sparse": ("https://sparse.pydata.org/en/latest/", None),
diff --git a/docs/contributing.rst b/docs/dev/contributing.rst
index ad2914f..c63381b 100644
--- a/docs/contributing.rst
+++ b/docs/dev/contributing.rst
@@ -6,7 +6,7 @@ Contributing to Pint
Pint uses (and thanks):
- github_ to host the code
-- travis_ to test all commits and PRs.
+- `github actions`_ to test all commits and PRs.
- coveralls_ to monitor coverage test coverage
- readthedocs_ to host the documentation.
- `bors-ng`_ as a merge bot and therefore every PR is tested before merging.
@@ -57,7 +57,7 @@ environment on Linux or OSX with the following commands::
$ cd pint
$ python -m virtualenv venv
$ source venv/bin/activate
- $ pip install -e .
+ $ pip install -e '.[test]'
$ pip install -r requirements_docs.txt
$ pip install pre-commit # This step and the next are optional but recommended.
$ pre-commit install
@@ -107,15 +107,16 @@ Extension Packages
------------------
Pint naturally integrates with other libraries in the scientific Python ecosystem, and
-a small number of
-`extension/compatibility packages<numpy.html#Compatibility-Packages>`_ have arisen to
-aid in compatibility between certain packages. Pint's rule of thumb for integration
+a small _`ecosystem` have arisen to aid in compatibility between certain packages
+allowing to build an
+
+Pint's rule of thumb for integration
features that work best as an extension package versus direct inclusion in Pint is:
* Extension (separate packages)
- * Duck array types that wrap Pint (come above Pint in
- `the type casting hierarchy<numpy.html#Technical-Commentary>`_)
+ * Duck array types that wrap Pint (come above Pint
+ in :ref:`the type casting hierarchy <_numpy#technical-commentary>`
* Uses features independent/on top of the libraries
@@ -134,7 +135,7 @@ features that work best as an extension package versus direct inclusion in Pint
.. _`issue tracker`: https://github.com/hgrecco/pint/issues
.. _`bors-ng`: https://github.com/bors-ng/bors-ng
.. _`github docs`: https://help.github.com/articles/closing-issues-via-commit-messages/
-.. _travis: https://travis-ci.com/
+.. _`github actions`: https://docs.github.com/en/actions
.. _coveralls: https://coveralls.io/
.. _readthedocs: https://readthedocs.org/
.. _pre-commit: https://pre-commit.com/
@@ -143,3 +144,4 @@ features that work best as an extension package versus direct inclusion in Pint
.. _flake8: https://flake8.pycqa.org/en/latest/
.. _pytest: https://docs.pytest.org/en/stable/
.. _sphinx: https://www.sphinx-doc.org/en/master/
+.. _`extension/compatibility packages`:
diff --git a/docs/pint-convert.rst b/docs/dev/pint-convert.rst
index 8d548f8..dbb0804 100644
--- a/docs/pint-convert.rst
+++ b/docs/dev/pint-convert.rst
@@ -6,38 +6,52 @@ Command-line script
The script `pint-convert` allows a quick conversion to a target system or
between arbitrary compatible units.
-By default, `pint-convert` converts to SI units::
+By default, `pint-convert` converts to SI units:
+
+.. code-block:: console
$ pint-convert 225lb
225 pound = 102.05828325 kg
-use the `--sys` argument to change it::
+use the `--sys` argument to change it:
+
+.. code-block:: console
$ pint-convert --sys US 102kg
102 kilogram = 224.871507429 lb
-or specify directly the target units::
+or specify directly the target units:
+
+.. code-block:: console
$ pint-convert 102kg lb
102 kilogram = 224.871507429 lb
-The input quantity can contain expressions::
+The input quantity can contain expressions:
+
+.. code-block:: console
$ pint-convert 7ft+2in
7.166666666666667 foot = 2.1844 m
-in some cases parentheses and quotes may be needed::
+in some cases parentheses and quotes may be needed:
+
+.. code-block:: console
$ pint-convert "225lb/(7ft+2in)"
31.3953488372093 pound / foot = 46.7214261353 kg/m
-If a number is omitted, 1 is assumed::
+If a number is omitted, 1 is assumed:
+
+.. code-block:: console
$ pint-convert km mi
1 kilometer = 0.621371192237 mi
The default precision is 12 significant figures, it can be changed with `-p`,
-but note that the accuracy may be affected by floating-point errors::
+but note that the accuracy may be affected by floating-point errors:
+
+.. code-block:: console
$ pint-convert -p 3 mi
1 mile = 1.61e+03 m
@@ -46,7 +60,9 @@ but note that the accuracy may be affected by floating-point errors::
1 light_year = 9460730472580.80078125 km
Some contexts are automatically enabled, allowing conversion between not fully
-compatible units::
+compatible units:
+
+.. code-block:: console
$ pint-convert 540nm
540 nanometer = 5.4e-07 m
@@ -59,7 +75,9 @@ compatible units::
With the `uncertainties` package, the experimental uncertainty in the physical
constants is considered, and the result is given in compact notation, with the
-uncertainty in the last figures in parentheses::
+uncertainty in the last figures in parentheses:
+
+.. code-block:: console
$ pint-convert Eh eV
1 hartree = 27.21138624599(5) eV
@@ -73,13 +91,17 @@ and the maximum number of uncertainty digits (`-u`, 2 by default)::
$ pint-convert -p 20 -u 4 Eh eV
1 hartree = 27.21138624598847(5207) eV
-The uncertainty can be disabled with `-U`)::
+The uncertainty can be disabled with `-U`):
+
+.. code-block:: console
$ pint-convert -p 20 -U Eh eV
1 hartree = 27.211386245988471444 eV
Correlations between experimental constants are also known, and taken into
-account. Use `-C` to disable it::
+account. Use `-C` to disable it:
+
+.. code-block:: console
$ pint-convert --sys atomic m_p
1 proton_mass = 1836.15267344(11) m_e
diff --git a/docs/developers_reference.rst b/docs/developers_reference.rst
deleted file mode 100644
index 19ce3cb..0000000
--- a/docs/developers_reference.rst
+++ /dev/null
@@ -1,75 +0,0 @@
-===================
-Developer reference
-===================
-
-All Modules
-===========
-
-.. automodule:: pint
- :members:
-
-.. automodule:: pint.babel_names
- :members:
-
-.. automodule:: pint.compat
- :members:
-
-.. automodule:: pint.converters
- :members:
-
-.. automodule:: pint.definitions
- :members:
-
-.. automodule:: pint.errors
- :members:
-
-.. automodule:: pint.formatting
- :members:
-
-.. automodule:: pint.matplotlib
- :members:
-
-.. automodule:: pint.pint_eval
- :members:
-
-.. automodule:: pint.registry
- :members:
-
-.. automodule:: pint.registry_helpers
- :members:
-
-.. automodule:: pint.testing
- :members:
-
-.. automodule:: pint.util
- :members:
-
-Facets
-------
-
-.. automodule:: pint.facets.plain
- :members:
-
-.. automodule:: pint.facets.nonmultiplicative
- :members:
-
-.. automodule:: pint.facets.formatting
- :members:
-
-.. automodule:: pint.facets.numpy
- :members:
-
-.. automodule:: pint.facets.dask
- :members:
-
-.. automodule:: pint.facets.measurement
- :members:
-
-.. automodule:: pint.facets.group
- :members:
-
-.. automodule:: pint.facets.system
- :members:
-
-.. automodule:: pint.facets.context
- :members:
diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst
new file mode 100644
index 0000000..7610fd0
--- /dev/null
+++ b/docs/ecosystem.rst
@@ -0,0 +1,11 @@
+Ecosystem
+=========
+
+Here is a list of known projects, packages and integrations using pint.
+
+
+Pint integrations:
+------------------
+
+- `pint-pandas <https://github.com/hgrecco/pint-pandas>`_ Pandas integration
+- `pint-xarray <https://github.com/xarray-contrib/pint-xarray>`_ Xarray integration
diff --git a/docs/getting.rst b/docs/getting.rst
deleted file mode 100644
index 0afcea3..0000000
--- a/docs/getting.rst
+++ /dev/null
@@ -1,58 +0,0 @@
-.. _getting:
-
-Installation
-============
-
-Pint has no dependencies except Python_ itself. In runs on Python 3.8+.
-
-You can install it (or upgrade to the latest version) using pip_::
-
- $ pip install -U pint
-
-That's all! You can check that Pint is correctly installed by starting up python, and importing Pint:
-
-.. code-block:: python
-
- >>> import pint
- >>> pint.__version__ # doctest: +SKIP
-
-Or running the test suite:
-
-.. code-block:: python
-
- >>> pint.test()
-
-.. note:: If you have an old system installation of Python and you don't want to
- mess with it, you can try `Anaconda CE`_. It is a free Python distribution by
- Continuum Analytics that includes many scientific packages. To install pint
- from the conda-forge channel instead of through pip use::
-
- $ conda install -c conda-forge pint
-
-
-Getting the code
-----------------
-
-You can also get the code from PyPI_ or GitHub_. You can either clone the public repository::
-
- $ git clone git://github.com/hgrecco/pint.git
-
-Download the tarball::
-
- $ curl -OL https://github.com/hgrecco/pint/tarball/master
-
-Or, download the zipball::
-
- $ curl -OL https://github.com/hgrecco/pint/zipball/master
-
-Once you have a copy of the source, you can embed it in your Python package, or install it into your site-packages easily::
-
- $ python setup.py install
-
-
-.. _easy_install: http://pypi.python.org/pypi/setuptools
-.. _Python: http://www.python.org/
-.. _pip: http://www.pip-installer.org/
-.. _`Anaconda CE`: https://store.continuum.io/cshop/anaconda
-.. _PyPI: https://pypi.python.org/pypi/Pint/
-.. _GitHub: https://github.com/hgrecco/pint
diff --git a/docs/faq.rst b/docs/getting/faq.rst
index 75d7db7..75d7db7 100644
--- a/docs/faq.rst
+++ b/docs/getting/faq.rst
diff --git a/docs/getting/index.rst b/docs/getting/index.rst
new file mode 100644
index 0000000..9907aeb
--- /dev/null
+++ b/docs/getting/index.rst
@@ -0,0 +1,51 @@
+Getting Started
+===============
+
+The getting started guide aims to get you using pint productively as quickly as possible.
+
+
+
+Installation
+------------
+
+Pint has no dependencies except Python itself. In runs on Python 3.8+.
+
+.. grid:: 2
+
+ .. grid-item-card:: Prefer pip?
+
+ **pint** can be installed via pip from `PyPI <https://pypi.org/project/pint>`__.
+
+ ++++++++++++++++++++++
+
+ .. code-block:: bash
+
+ pip install pint
+
+ .. grid-item-card:: Working with conda?
+
+ **pint** is part of the `Conda-Forge <https://conda-forge.org//>`__
+ channel and can be installed with Anaconda or Miniconda:
+
+ ++++++++++++++++++++++
+
+ .. code-block:: bash
+
+ conda install -c conda-forge pint
+
+
+That's all! You can check that Pint is correctly installed by starting up python, and importing Pint:
+
+.. code-block:: python
+
+ >>> import pint
+ >>> pint.__version__ # doctest: +SKIP
+
+.. toctree::
+ :maxdepth: 2
+ :hidden:
+
+ overview
+ tutorial
+ pint-in-your-projects
+ faq
diff --git a/docs/getting/overview.rst b/docs/getting/overview.rst
new file mode 100644
index 0000000..cd639aa
--- /dev/null
+++ b/docs/getting/overview.rst
@@ -0,0 +1,116 @@
+What is Pint ?
+==============
+
+.. .. image:: _static/logo-full.jpg
+.. :alt: Pint: **physical quantities**
+.. :class: float-right
+
+Pint is a Python package to define, operate and manipulate **physical quantities**:
+the product of a numerical value and a unit of measurement. It allows
+arithmetic operations between them and conversions from and to different units.
+
+It is distributed with a `comprehensive list of physical units, prefixes and constants`_.
+Due to its modular design, you can extend (or even rewrite!) the complete list
+without changing the source code. It supports a lot of numpy mathematical
+operations **without monkey patching or wrapping numpy**.
+
+It has a complete test coverage. It runs in Python 3.8+ with no other
+dependencies. It is licensed under a `BSD 3-clause style license`_.
+
+It is extremely easy and natural to use:
+
+.. code-block:: python
+
+ >>> import pint
+ >>> ureg = pint.UnitRegistry()
+ >>> 3 * ureg.meter + 4 * ureg.cm
+ <Quantity(3.04, 'meter')>
+
+and you can make good use of numpy if you want:
+
+.. code-block:: python
+
+ >>> import numpy as np
+ >>> [3, 4] * ureg.meter + [4, 3] * ureg.cm
+ <Quantity([ 3.04 4.03], 'meter')>
+ >>> np.sum(_)
+ <Quantity(7.07, 'meter')>
+
+See the :ref:`Tutorial` for more help getting started.
+
+
+
+
+Design principles
+-----------------
+
+Although there are already a few very good Python packages to handle physical
+quantities, no one was really fitting my needs. Like most developers, I
+programmed Pint to scratch my own itches.
+
+**Unit parsing**: prefixed and pluralized forms of units are recognized without
+explicitly defining them. In other words: as the prefix *kilo* and the unit
+*meter* are defined, Pint understands *kilometers*. This results in a much
+shorter and maintainable unit definition list as compared to other packages.
+
+**Standalone unit definitions**: units definitions are loaded from a text file
+which is simple and easy to edit. Adding and changing units and their
+definitions does not involve changing the code.
+
+**Advanced string formatting**: a quantity can be formatted into string using
+`PEP 3101`_ syntax. Extended conversion flags are given to provide symbolic,
+LaTeX and pretty formatting. Unit name translation is available if Babel_ is
+installed.
+
+**Free to choose the numerical type**: You can use any numerical type
+(``fraction``, ``float``, ``decimal``, ``numpy.ndarray``, etc). NumPy_ is not
+required, but is supported.
+
+**Awesome NumPy integration**: When you choose to use a NumPy_ ndarray, its methods and
+ufuncs are supported including automatic conversion of units. For example
+``numpy.arccos(q)`` will require a dimensionless ``q`` and the units of the output
+quantity will be radian.
+
+**Uncertainties integration**: transparently handles calculations with
+quantities with uncertainties (like 3.14±0.01) meter via the `uncertainties
+package`_.
+
+**Handle temperature**: conversion between units with different reference
+points, like positions on a map or absolute temperature scales.
+
+**Dependency free**: it depends only on Python and its standard library. It interacts with other packages
+like numpy and uncertainties if they are installed
+
+**Pandas integration**: The `pint-pandas`_ package makes it possible to use Pint with Pandas.
+Operations on DataFrames and between columns are units aware, providing even more convenience for users
+of Pandas DataFrames. For full details, see the `pint-pandas Jupyter notebook`_.
+
+
+When you choose to use a NumPy_ ndarray, its methods and
+ufuncs are supported including automatic conversion of units. For example
+``numpy.arccos(q)`` will require a dimensionless ``q`` and the units
+of the output quantity will be radian.
+
+One last thing
+--------------
+
+
+ The MCO MIB has determined that the root cause for the loss of the MCO spacecraft was the failure to use metric units in the coding of a ground software file, “Small Forces,” used in trajectory models. Specifically, thruster performance data in English units instead of metric units was used in the software application code titled SM_FORCES (small forces). The output from the SM_FORCES application code as required by a MSOP Project Software Interface Specification (SIS) was to be in metric units of Newtonseconds (N-s). Instead, the data was reported in English units of pound-seconds (lbf-s). The Angular Momentum Desaturation (AMD) file contained the output data from the SM_FORCES software. The SIS, which was not followed, defines both the format and units of the AMD file generated by ground-based computers. Subsequent processing of the data from AMD file by the navigation software algorithm therefore, underestimated the effect on the spacecraft trajectory by a factor of 4.45, which is the required conversion factor from force in pounds to Newtons. An erroneous trajectory was computed using this incorrect data.
+
+ `Mars Climate Orbiter Mishap Investigation Phase I Report`
+ `PDF <https://llis.nasa.gov/llis_lib/pdf/1009464main1_0641-mr.pdf>`_
+
+
+License
+-------
+
+.. literalinclude:: ../../LICENSE
+
+.. _`comprehensive list of physical units, prefixes and constants`: https://github.com/hgrecco/pint/blob/master/pint/default_en.txt
+.. _`uncertainties package`: https://pythonhosted.org/uncertainties/
+.. _`NumPy`: http://www.numpy.org/
+.. _`PEP 3101`: https://www.python.org/dev/peps/pep-3101/
+.. _`Babel`: http://babel.pocoo.org/
+.. _`pint-pandas`: https://github.com/hgrecco/pint-pandas
+.. _`pint-pandas Jupyter notebook`: https://github.com/hgrecco/pint-pandas/blob/master/notebooks/pint-pandas.ipynb
+.. _`BSD 3-clause style license`: https://github.com/hgrecco/pint/blob/master/LICENSE
diff --git a/docs/getting/pint-in-your-projects.rst b/docs/getting/pint-in-your-projects.rst
new file mode 100644
index 0000000..37803d9
--- /dev/null
+++ b/docs/getting/pint-in-your-projects.rst
@@ -0,0 +1,70 @@
+.. _pint_in_your_projects:
+
+Using Pint in your projects
+===========================
+
+Having a shared registry
+------------------------
+
+If you use Pint in multiple modules within your Python package, you normally
+want to avoid creating multiple instances of the unit registry.
+The best way to do this is by instantiating the registry in a single place. For
+example, you can add the following code to your package ``__init__.py``
+
+.. doctest::
+
+ >>> from pint import UnitRegistry
+ >>> ureg = UnitRegistry()
+ >>> Q_ = ureg.Quantity
+
+
+Then in ``yourmodule.py`` the code would be
+
+.. code-block:: python
+
+ from . import ureg, Q_
+
+ length = 10 * ureg.meter
+ my_speed = Q_(20, 'm/s')
+
+If you are pickling and unpickling Quantities within your project, you should
+also define the registry as the application registry
+
+.. code-block:: python
+
+ from pint import UnitRegistry, set_application_registry
+ ureg = UnitRegistry()
+ set_application_registry(ureg)
+
+
+.. warning:: There are no global units in Pint. All units belong to a registry and
+ you can have multiple registries instantiated at the same time. However, you
+ are not supposed to operate between quantities that belong to different registries.
+ Never do things like this:
+
+.. doctest::
+
+ >>> q1 = 10 * UnitRegistry().meter
+ >>> q2 = 10 * UnitRegistry().meter
+ >>> q1 + q2
+ Traceback (most recent call last):
+ ...
+ ValueError: Cannot operate with Quantity and Quantity of different registries.
+ >>> id(q1._REGISTRY) == id(q2._REGISTRY)
+ False
+
+
+Keeping up to date with Pint development
+----------------------------------------
+
+While we work hard to avoid breaking code using Pint, sometimes it
+happens. To help you track how Pint is evolving it is recommended
+that you run a daily or weekly job against pint master branch.
+
+For example, this is how xarray_ is doing it:
+
+If a new version of Pint breaks your code, please open an issue_ to
+let us know.
+
+.. _xarray: https://github.com/pydata/xarray/blob/main/.github/workflows/upstream-dev-ci.yaml
+.. _issue: https://github.com/hgrecco/pint/issues
diff --git a/docs/tutorial.rst b/docs/getting/tutorial.rst
index 3b3fe98..76ba30d 100644
--- a/docs/tutorial.rst
+++ b/docs/getting/tutorial.rst
@@ -85,7 +85,7 @@ Pint will complain if you try to use a unit which is not in the registry:
You can add your own units to the existing registry, or build your own list.
See the page on :ref:`defining` for more information on that.
-See `String parsing`_ and :doc:`defining-quantities` for more ways of defining
+See `String parsing`_ and :ref:`Defining Quantities` for more ways of defining
a ``Quantity()`` object.
``Quantity()`` objects also work well with NumPy arrays, which you can
@@ -423,56 +423,6 @@ and by doing that, string formatting is now localized:
'1.3 mètre par seconde²'
-Using Pint in your projects
----------------------------
-
-If you use Pint in multiple modules within your Python package, you normally
-want to avoid creating multiple instances of the unit registry.
-The best way to do this is by instantiating the registry in a single place. For
-example, you can add the following code to your package ``__init__.py``
-
-.. code-block:: python
-
- from pint import UnitRegistry
- ureg = UnitRegistry()
- Q_ = ureg.Quantity
-
-
-Then in ``yourmodule.py`` the code would be
-
-.. code-block:: python
-
- from . import ureg, Q_
-
- length = 10 * ureg.meter
- my_speed = Q_(20, 'm/s')
-
-If you are pickling and unpickling Quantities within your project, you should
-also define the registry as the application registry
-
-.. code-block:: python
-
- from pint import UnitRegistry, set_application_registry
- ureg = UnitRegistry()
- set_application_registry(ureg)
-
-
-.. warning:: There are no global units in Pint. All units belong to a registry and
- you can have multiple registries instantiated at the same time. However, you
- are not supposed to operate between quantities that belong to different registries.
- Never do things like this:
-
-.. doctest::
-
- >>> q1 = 10 * UnitRegistry().meter
- >>> q2 = 10 * UnitRegistry().meter
- >>> q1 + q2
- Traceback (most recent call last):
- ...
- ValueError: Cannot operate with Quantity and Quantity of different registries.
- >>> id(q1._REGISTRY) == id(q2._REGISTRY)
- False
-
.. _`default list of units`: https://github.com/hgrecco/pint/blob/master/pint/default_en.txt
.. _`Babel`: http://babel.pocoo.org/
diff --git a/docs/index.rst b/docs/index.rst
index a0aa972..8c60992 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -3,168 +3,86 @@
Pint: makes units easy
======================
-.. image:: _static/logo-full.jpg
- :alt: Pint: **physical quantities**
- :class: floatingflask
+**Useful links**:
+`Code Repository <https://github.com/hgrecco/pint>`__ |
+`Issues <https://github.com/hgrecco/pint/issues>`__ |
+`Discussions <https://github.com/hgrecco/pint/discussions>`__
-Pint is a Python package to define, operate and manipulate **physical quantities**:
-the product of a numerical value and a unit of measurement. It allows
-arithmetic operations between them and conversions from and to different units.
-It is distributed with a `comprehensive list of physical units, prefixes and constants`_.
-Due to its modular design, you can extend (or even rewrite!) the complete list
-without changing the source code. It supports a lot of numpy mathematical
-operations **without monkey patching or wrapping numpy**.
-It has a complete test coverage. It runs in Python 3.8+ with no other
-dependencies. It is licensed under a `BSD 3-clause style license`_.
+.. grid:: 1 1 2 2
+ :gutter: 2
-It is extremely easy and natural to use:
+ .. grid-item-card::
+ :img-top: _static/index_getting_started.svg
+ :link: getting/index
+ :link-type: doc
-.. code-block:: python
+ Getting Started
+ ^^^^^^^^^^^^^^^
- >>> import pint
- >>> ureg = pint.UnitRegistry()
- >>> 3 * ureg.meter + 4 * ureg.cm
- <Quantity(3.04, 'meter')>
+ New to Pint? Check out the getting started guides. They contain an
+ introduction to Pint's main concepts and links to additional tutorials.
-and you can make good use of numpy if you want:
+ .. grid-item-card::
+ :img-top: _static/index_user_guide.svg
+ :link: user/index
+ :link-type: doc
-.. code-block:: python
+ User Guide
+ ^^^^^^^^^^
- >>> import numpy as np
- >>> [3, 4] * ureg.meter + [4, 3] * ureg.cm
- <Quantity([ 3.04 4.03], 'meter')>
- >>> np.sum(_)
- <Quantity(7.07, 'meter')>
+ The user guide provides in-depth information on the
+ key concepts of Pint with useful background information and explanation.
-See the :ref:`Tutorial` for more help getting started.
+ .. grid-item-card::
+ :img-top: _static/index_api.svg
+ :link: api/base
+ :link-type: doc
-Quick Installation
-------------------
+ API reference
+ ^^^^^^^^^^^^^
-To install Pint, simply:
+ The reference guide contains a detailed description of the pint API.
+ The reference describes how the methods work and which parameters can
+ be used. It assumes that you have an understanding of the key concepts.
-.. code-block:: bash
+ .. grid-item-card::
+ :img-top: _static/index_contribute.svg
+ :link: dev/contributing
+ :link-type: doc
- $ pip install pint
+ Developer guide
+ ^^^^^^^^^^^^^^^
-or utilizing conda, with the conda-forge channel:
+ Saw a typo in the documentation? Want to improve existing functionalities?
+ The contributing guidelines will guide you through the process of improving
+ Pint.
-.. code-block:: bash
-
- $ conda install -c conda-forge pint
-
-and then simply enjoy it!
-
-(See :ref:`Installation <getting>` for more detail.)
-
-
-Design principles
------------------
-
-Although there are already a few very good Python packages to handle physical
-quantities, no one was really fitting my needs. Like most developers, I
-programmed Pint to scratch my own itches.
-
-**Unit parsing**: prefixed and pluralized forms of units are recognized without
-explicitly defining them. In other words: as the prefix *kilo* and the unit
-*meter* are defined, Pint understands *kilometers*. This results in a much
-shorter and maintainable unit definition list as compared to other packages.
-
-**Standalone unit definitions**: units definitions are loaded from a text file
-which is simple and easy to edit. Adding and changing units and their
-definitions does not involve changing the code.
-
-**Advanced string formatting**: a quantity can be formatted into string using
-`PEP 3101`_ syntax. Extended conversion flags are given to provide symbolic,
-LaTeX and pretty formatting. Unit name translation is available if Babel_ is
-installed.
-
-**Free to choose the numerical type**: You can use any numerical type
-(``fraction``, ``float``, ``decimal``, ``numpy.ndarray``, etc). NumPy_ is not
-required, but is supported.
-
-**Awesome NumPy integration**: When you choose to use a NumPy_ ndarray, its methods and
-ufuncs are supported including automatic conversion of units. For example
-``numpy.arccos(q)`` will require a dimensionless ``q`` and the units of the output
-quantity will be radian.
-
-**Uncertainties integration**: transparently handles calculations with
-quantities with uncertainties (like 3.14±0.01) meter via the `uncertainties
-package`_.
-
-**Handle temperature**: conversion between units with different reference
-points, like positions on a map or absolute temperature scales.
-
-**Dependency free**: it depends only on Python and its standard library. It interacts with other packages
-like numpy and uncertainties if they are installed
-
-**Pandas integration**: The `pint-pandas`_ package makes it possible to use Pint with Pandas.
-Operations on DataFrames and between columns are units aware, providing even more convenience for users
-of Pandas DataFrames. For full details, see the `pint-pandas Jupyter notebook`_.
-
-
-When you choose to use a NumPy_ ndarray, its methods and
-ufuncs are supported including automatic conversion of units. For example
-``numpy.arccos(q)`` will require a dimensionless ``q`` and the units
-of the output quantity will be radian.
-
-
-User Guide
-----------
.. toctree::
- :maxdepth: 1
+ :maxdepth: 2
+ :hidden:
+ :caption: For users
- getting
- tutorial
- defining-quantities
- formatting
- numpy
- nonmult
- log_units
- angular_frequency
- wrapping
- plotting
- serialization
- pitheorem
- contexts
- measurement
- defining
- performance
- systems
- currencies
- pint-convert
-
-More information
-----------------
+ Getting started <getting/index>
+ User Guide <user/index>
+ Advanced topics <advanced/index>
+ ecosystem
+ API Reference <api/index>
.. toctree::
:maxdepth: 1
+ :hidden:
+ :caption: For developers
- developers_reference
- contributing
- faq
-
-
-One last thing
---------------
-
-.. epigraph::
-
- The MCO MIB has determined that the root cause for the loss of the MCO spacecraft was the failure to use metric units in the coding of a ground software file, “Small Forces,” used in trajectory models. Specifically, thruster performance data in English units instead of metric units was used in the software application code titled SM_FORCES (small forces). The output from the SM_FORCES application code as required by a MSOP Project Software Interface Specification (SIS) was to be in metric units of Newtonseconds (N-s). Instead, the data was reported in English units of pound-seconds (lbf-s). The Angular Momentum Desaturation (AMD) file contained the output data from the SM_FORCES software. The SIS, which was not followed, defines both the format and units of the AMD file generated by ground-based computers. Subsequent processing of the data from AMD file by the navigation software algorithm therefore, underestimated the effect on the spacecraft trajectory by a factor of 4.45, which is the required conversion factor from force in pounds to Newtons. An erroneous trajectory was computed using this incorrect data.
-
- `Mars Climate Orbiter Mishap Investigation Phase I Report`
- `PDF <https://llis.nasa.gov/llis_lib/pdf/1009464main1_0641-mr.pdf>`_
-
+ dev/contributing
+ dev/pint-convert
+.. toctree::
+ :maxdepth: 1
+ :hidden:
+ :caption: Community
-.. _`comprehensive list of physical units, prefixes and constants`: https://github.com/hgrecco/pint/blob/master/pint/default_en.txt
-.. _`uncertainties package`: https://pythonhosted.org/uncertainties/
-.. _`NumPy`: http://www.numpy.org/
-.. _`PEP 3101`: https://www.python.org/dev/peps/pep-3101/
-.. _`Babel`: http://babel.pocoo.org/
-.. _`pint-pandas`: https://github.com/hgrecco/pint-pandas
-.. _`pint-pandas Jupyter notebook`: https://github.com/hgrecco/pint-pandas/blob/master/notebooks/pint-pandas.ipynb
-.. _`BSD 3-clause style license`: https://github.com/hgrecco/pint/blob/master/LICENSE
+ GitHub repository <https://github.com/hgrecco/pint>
+ StackOverflow <https://stackoverflow.com/questions/tagged/pint>
diff --git a/docs/angular_frequency.rst b/docs/user/angular_frequency.rst
index 5636135..0bafd05 100644
--- a/docs/angular_frequency.rst
+++ b/docs/user/angular_frequency.rst
@@ -12,11 +12,11 @@ By default, pint treats angle quantities as `dimensionless`, so allows conversio
>> from pint import UnitRegistry
>>> ureg = UnitRegistry()
- >>> frequency = ureg('60rpm')
- >>> frequency.to('Hz')
+ >>> angular_frequency = ureg('60rpm')
+ >>> angular_frequency.to('Hz')
<Quantity(6.28318531, 'hertz')>
-pint follows the convetions of SI. The SI BIPM Brochure (Bureau International des Poids et Mesures) states:
+pint follows the conventions of SI. The SI BIPM Brochure (Bureau International des Poids et Mesures) states:
.. note::
diff --git a/docs/contexts.rst b/docs/user/contexts.rst
index 4077503..4077503 100644
--- a/docs/contexts.rst
+++ b/docs/user/contexts.rst
diff --git a/docs/defining-quantities.rst b/docs/user/defining-quantities.rst
index ec57454..ec57454 100644
--- a/docs/defining-quantities.rst
+++ b/docs/user/defining-quantities.rst
diff --git a/docs/formatting.rst b/docs/user/formatting.rst
index 3ed3540..7b0f15b 100644
--- a/docs/formatting.rst
+++ b/docs/user/formatting.rst
@@ -1,4 +1,3 @@
-.. _formatting:
.. currentmodule:: pint
@@ -8,8 +7,9 @@
import pint
-String formatting
-=================
+String formatting specification
+===============================
+
The conversion of :py:class:`Unit` and :py:class:`Quantity` objects to strings (e.g.
through the :py:class:`str` builtin or f-strings) can be customized using :ref:`format
specifications <formatspec>`. The basic format is:
diff --git a/docs/user/index.rst b/docs/user/index.rst
new file mode 100644
index 0000000..8fd9c35
--- /dev/null
+++ b/docs/user/index.rst
@@ -0,0 +1,20 @@
+User Guide
+==========
+
+In this user guide, you will find detailed descriptions and
+examples that describe many common tasks that you can accomplish with pint.
+
+
+.. toctree::
+ :maxdepth: 2
+ :hidden:
+
+ defining-quantities
+ formatting
+ nonmult
+ log_units
+ angular_frequency
+ contexts
+ systems
+ numpy
+ plotting
diff --git a/docs/log_units.rst b/docs/user/log_units.rst
index 03e0079..03e0079 100644
--- a/docs/log_units.rst
+++ b/docs/user/log_units.rst
diff --git a/docs/nonmult.rst b/docs/user/nonmult.rst
index a649d2a..a649d2a 100644
--- a/docs/nonmult.rst
+++ b/docs/user/nonmult.rst
diff --git a/docs/numpy.ipynb b/docs/user/numpy.ipynb
index 59def65..5491001 100644
--- a/docs/numpy.ipynb
+++ b/docs/user/numpy.ipynb
@@ -2,7 +2,11 @@
"cells": [
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"NumPy Support\n",
"=============\n",
@@ -21,7 +25,11 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
"# Import NumPy\n",
@@ -29,11 +37,13 @@
"\n",
"# Import Pint\n",
"import pint\n",
+ "\n",
"ureg = pint.UnitRegistry()\n",
"Q_ = ureg.Quantity\n",
"\n",
"# Silence NEP 18 warning\n",
"import warnings\n",
+ "\n",
"with warnings.catch_warnings():\n",
" warnings.simplefilter(\"ignore\")\n",
" Q_([])"
@@ -41,7 +51,11 @@
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"and then we create a quantity the standard way"
]
@@ -49,26 +63,38 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
- "legs1 = Q_(np.asarray([3., 4.]), 'meter')\n",
+ "legs1 = Q_(np.asarray([3.0, 4.0]), \"meter\")\n",
"print(legs1)"
]
},
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
- "legs1 = [3., 4.] * ureg.meter\n",
+ "legs1 = [3.0, 4.0] * ureg.meter\n",
"print(legs1)"
]
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"All usual Pint methods can be used with this quantity. For example:"
]
@@ -76,16 +102,24 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
- "print(legs1.to('kilometer'))"
+ "print(legs1.to(\"kilometer\"))"
]
},
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
"print(legs1.dimensionality)"
@@ -94,18 +128,26 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
"try:\n",
- " legs1.to('joule')\n",
+ " legs1.to(\"joule\")\n",
"except pint.DimensionalityError as exc:\n",
" print(exc)"
]
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"NumPy functions are supported by Pint. For example if we define:"
]
@@ -113,16 +155,24 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
- "legs2 = [400., 300.] * ureg.centimeter\n",
+ "legs2 = [400.0, 300.0] * ureg.centimeter\n",
"print(legs2)"
]
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"we can calculate the hypotenuse of the right triangles with legs1 and legs2."
]
@@ -130,7 +180,11 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
"hyps = np.hypot(legs1, legs2)\n",
@@ -139,7 +193,11 @@
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"Notice that before the `np.hypot` was used, the numerical value of legs2 was\n",
"internally converted to the units of legs1 as expected.\n",
@@ -151,16 +209,24 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
- "angles = np.arccos(legs2/hyps)\n",
+ "angles = np.arccos(legs2 / hyps)\n",
"print(angles)"
]
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"You can convert the result to degrees using usual unit conversion:"
]
@@ -168,15 +234,23 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
- "print(angles.to('degree'))"
+ "print(angles.to(\"degree\"))"
]
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"Applying a function that expects angles to a quantity with a different dimensionality\n",
"results in an error:"
@@ -185,7 +259,11 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
"try:\n",
@@ -196,7 +274,11 @@
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"Function/Method Support\n",
"-----------------------\n",
@@ -214,16 +296,25 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
"from pint.facets.numpy.numpy_func import HANDLED_FUNCTIONS\n",
+ "\n",
"print(sorted(list(HANDLED_FUNCTIONS)))"
]
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"And the following [NumPy ndarray methods](http://docs.scipy.org/doc/numpy/reference/arrays.ndarray.html#array-methods):\n",
"\n",
@@ -235,7 +326,11 @@
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"Array Type Support\n",
"------------------\n",
@@ -273,38 +368,46 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
"from graphviz import Digraph\n",
"\n",
- "g = Digraph(graph_attr={'size': '8,5'}, node_attr={'fontname': 'courier'})\n",
- "g.edge('Dask array', 'NumPy ndarray')\n",
- "g.edge('Dask array', 'CuPy ndarray')\n",
- "g.edge('Dask array', 'Sparse COO')\n",
- "g.edge('Dask array', 'NumPy masked array', style='dashed')\n",
- "g.edge('CuPy ndarray', 'NumPy ndarray')\n",
- "g.edge('Sparse COO', 'NumPy ndarray')\n",
- "g.edge('NumPy masked array', 'NumPy ndarray')\n",
- "g.edge('Jax array', 'NumPy ndarray')\n",
- "g.edge('Pint Quantity', 'Dask array', style='dashed')\n",
- "g.edge('Pint Quantity', 'NumPy ndarray')\n",
- "g.edge('Pint Quantity', 'CuPy ndarray', style='dashed')\n",
- "g.edge('Pint Quantity', 'Sparse COO')\n",
- "g.edge('Pint Quantity', 'NumPy masked array', style='dashed')\n",
- "g.edge('xarray Dataset/DataArray/Variable', 'Dask array')\n",
- "g.edge('xarray Dataset/DataArray/Variable', 'CuPy ndarray', style='dashed')\n",
- "g.edge('xarray Dataset/DataArray/Variable', 'Sparse COO')\n",
- "g.edge('xarray Dataset/DataArray/Variable', 'NumPy ndarray')\n",
- "g.edge('xarray Dataset/DataArray/Variable', 'NumPy masked array', style='dashed')\n",
- "g.edge('xarray Dataset/DataArray/Variable', 'Pint Quantity')\n",
- "g.edge('xarray Dataset/DataArray/Variable', 'Jax array', style='dashed')\n",
+ "g = Digraph(graph_attr={\"size\": \"8,5\"}, node_attr={\"fontname\": \"courier\"})\n",
+ "g.edge(\"Dask array\", \"NumPy ndarray\")\n",
+ "g.edge(\"Dask array\", \"CuPy ndarray\")\n",
+ "g.edge(\"Dask array\", \"Sparse COO\")\n",
+ "g.edge(\"Dask array\", \"NumPy masked array\", style=\"dashed\")\n",
+ "g.edge(\"CuPy ndarray\", \"NumPy ndarray\")\n",
+ "g.edge(\"Sparse COO\", \"NumPy ndarray\")\n",
+ "g.edge(\"NumPy masked array\", \"NumPy ndarray\")\n",
+ "g.edge(\"Jax array\", \"NumPy ndarray\")\n",
+ "g.edge(\"Pint Quantity\", \"Dask array\", style=\"dashed\")\n",
+ "g.edge(\"Pint Quantity\", \"NumPy ndarray\")\n",
+ "g.edge(\"Pint Quantity\", \"CuPy ndarray\", style=\"dashed\")\n",
+ "g.edge(\"Pint Quantity\", \"Sparse COO\")\n",
+ "g.edge(\"Pint Quantity\", \"NumPy masked array\", style=\"dashed\")\n",
+ "g.edge(\"xarray Dataset/DataArray/Variable\", \"Dask array\")\n",
+ "g.edge(\"xarray Dataset/DataArray/Variable\", \"CuPy ndarray\", style=\"dashed\")\n",
+ "g.edge(\"xarray Dataset/DataArray/Variable\", \"Sparse COO\")\n",
+ "g.edge(\"xarray Dataset/DataArray/Variable\", \"NumPy ndarray\")\n",
+ "g.edge(\"xarray Dataset/DataArray/Variable\", \"NumPy masked array\", style=\"dashed\")\n",
+ "g.edge(\"xarray Dataset/DataArray/Variable\", \"Pint Quantity\")\n",
+ "g.edge(\"xarray Dataset/DataArray/Variable\", \"Jax array\", style=\"dashed\")\n",
"g"
]
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"### Examples\n",
"\n",
@@ -314,16 +417,20 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
"import xarray as xr\n",
"\n",
"# Load tutorial data\n",
- "air = xr.tutorial.load_dataset('air_temperature')['air'][0]\n",
+ "air = xr.tutorial.load_dataset(\"air_temperature\")[\"air\"][0]\n",
"\n",
"# Convert to Quantity\n",
- "air.data = Q_(air.data, air.attrs.pop('units', ''))\n",
+ "air.data = Q_(air.data, air.attrs.pop(\"units\", \"\"))\n",
"\n",
"print(air)\n",
"print()\n",
@@ -332,7 +439,11 @@
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"**Pint Quantity wrapping Sparse COO**"
]
@@ -340,7 +451,11 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
"from sparse import COO\n",
@@ -360,7 +475,11 @@
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"**Pint Quantity wrapping NumPy Masked Array**"
]
@@ -368,13 +487,17 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
"m = np.ma.masked_array([2, 3, 5, 7], mask=[False, True, False, True])\n",
"\n",
"# Must create using Quantity class\n",
- "print(repr(ureg.Quantity(m, 'm')))\n",
+ "print(repr(ureg.Quantity(m, \"m\")))\n",
"print()\n",
"\n",
"# DO NOT create using multiplication until\n",
@@ -385,7 +508,11 @@
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"**Pint Quantity wrapping Dask Array**"
]
@@ -393,7 +520,11 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
"import dask.array as da\n",
@@ -415,7 +546,11 @@
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"**xarray wrapping Pint Quantity wrapping Dask array wrapping Sparse COO**"
]
@@ -423,7 +558,11 @@
{
"cell_type": "code",
"execution_count": null,
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%%\n"
+ }
+ },
"outputs": [],
"source": [
"import dask.array as da\n",
@@ -432,14 +571,14 @@
"x[x < 0.95] = 0\n",
"\n",
"data = xr.DataArray(\n",
- " Q_(x.map_blocks(COO), 'm'),\n",
- " dims=('z', 'y', 'x'),\n",
+ " Q_(x.map_blocks(COO), \"m\"),\n",
+ " dims=(\"z\", \"y\", \"x\"),\n",
" coords={\n",
- " 'z': np.arange(100),\n",
- " 'y': np.arange(100) - 50,\n",
- " 'x': np.arange(100) * 1.5 - 20\n",
+ " \"z\": np.arange(100),\n",
+ " \"y\": np.arange(100) - 50,\n",
+ " \"x\": np.arange(100) * 1.5 - 20,\n",
" },\n",
- " name='test'\n",
+ " name=\"test\",\n",
")\n",
"\n",
"print(data)\n",
@@ -449,7 +588,11 @@
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"### Compatibility Packages\n",
"\n",
@@ -463,7 +606,11 @@
},
{
"cell_type": "markdown",
- "metadata": {},
+ "metadata": {
+ "pycharm": {
+ "name": "#%% md\n"
+ }
+ },
"source": [
"## Additional Comments\n",
"\n",
@@ -483,11 +630,6 @@
}
],
"metadata": {
- "kernelspec": {
- "display_name": "Python 3",
- "language": "python",
- "name": "python3"
- },
"language_info": {
"codemirror_mode": {
"name": "ipython",
@@ -497,8 +639,7 @@
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
- "pygments_lexer": "ipython3",
- "version": "3.8.2"
+ "pygments_lexer": "ipython3"
}
},
"nbformat": 4,
diff --git a/docs/plotting.rst b/docs/user/plotting.rst
index a008d45..3c3fc39 100644
--- a/docs/plotting.rst
+++ b/docs/user/plotting.rst
@@ -70,6 +70,31 @@ This also allows controlling the actual plotting units for the x and y axes:
ax.axhline(26400 * ureg.feet, color='tab:red')
ax.axvline(120 * ureg.minutes, color='tab:green')
+Users have the possibility to change the format of the units on the plot:
+
+.. plot::
+ :include-source: true
+
+ import matplotlib.pyplot as plt
+ import numpy as np
+ import pint
+
+ ureg = pint.UnitRegistry()
+ ureg.setup_matplotlib(True)
+
+ ureg.mpl_formatter = "{:~P}"
+
+ y = np.linspace(0, 30) * ureg.miles
+ x = np.linspace(0, 5) * ureg.hours
+
+ fig, ax = plt.subplots()
+ ax.yaxis.set_units(ureg.inches)
+ ax.xaxis.set_units(ureg.seconds)
+
+ ax.plot(x, y, 'tab:blue')
+ ax.axhline(26400 * ureg.feet, color='tab:red')
+ ax.axvline(120 * ureg.minutes, color='tab:green')
+
For more information, visit the Matplotlib_ home page.
.. _Matplotlib: https://matplotlib.org
diff --git a/docs/systems.rst b/docs/user/systems.rst
index d4a175d..5a1c27b 100644
--- a/docs/systems.rst
+++ b/docs/user/systems.rst
@@ -1,7 +1,7 @@
.. _systems:
-Different Unit Systems (and default units)
-==========================================
+Dealing with unit systems
+=========================
Pint Unit Registry has the concept of system, which is a group of units
diff --git a/pint/__init__.py b/pint/__init__.py
index b30409a..ee80048 100644
--- a/pint/__init__.py
+++ b/pint/__init__.py
@@ -65,7 +65,7 @@ def _unpickle(cls, *args):
object of type cls
"""
- from pint.facets.plain import UnitsContainer
+ from pint.util import UnitsContainer
for arg in args:
# Prefixed units are defined within the registry
diff --git a/pint/_typing.py b/pint/_typing.py
index cfb803b..64c3a2b 100644
--- a/pint/_typing.py
+++ b/pint/_typing.py
@@ -3,7 +3,9 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any, Callable, Tuple, TypeVar, Union
if TYPE_CHECKING:
- from .facets.plain import Quantity, Unit, UnitsContainer
+ from .facets.plain import PlainQuantity as Quantity
+ from .facets.plain import PlainUnit as Unit
+ from .util import UnitsContainer
UnitLike = Union[str, "UnitsContainer", "Unit"]
diff --git a/pint/_vendor/flexparser.py b/pint/_vendor/flexparser.py
new file mode 100644
index 0000000..8945b6e
--- /dev/null
+++ b/pint/_vendor/flexparser.py
@@ -0,0 +1,1455 @@
+"""
+ flexparser.flexparser
+ ~~~~~~~~~~~~~~~~~~~~~
+
+ Classes and functions to create parsers.
+
+ The idea is quite simple. You write a class for every type of content
+ (called here ``ParsedStatement``) you need to parse. Each class should
+ have a ``from_string`` constructor. We used extensively the ``typing``
+ module to make the output structure easy to use and less error prone.
+
+ For more information, take a look at https://github.com/hgrecco/flexparser
+
+ :copyright: 2022 by flexparser Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+import collections
+import dataclasses
+import enum
+import functools
+import hashlib
+import hmac
+import inspect
+import logging
+import pathlib
+import re
+import sys
+import typing as ty
+from collections.abc import Iterator
+from dataclasses import dataclass
+from functools import cached_property
+from importlib import resources
+from typing import Optional, Tuple, Type
+
+_LOGGER = logging.getLogger("flexparser")
+
+_SENTINEL = object()
+
+
+################
+# Exceptions
+################
+
+
+@dataclass(frozen=True)
+class Statement:
+ """Base class for parsed elements within a source file."""
+
+ start_line: int = dataclasses.field(init=False, default=None)
+ start_col: int = dataclasses.field(init=False, default=None)
+
+ end_line: int = dataclasses.field(init=False, default=None)
+ end_col: int = dataclasses.field(init=False, default=None)
+
+ raw: str = dataclasses.field(init=False, default=None)
+
+ @classmethod
+ def from_statement(cls, statement: Statement):
+ out = cls()
+ out.set_position(*statement.get_position())
+ out.set_raw(statement.raw)
+ return out
+
+ @classmethod
+ def from_statement_iterator_element(cls, values: ty.Tuple[int, int, int, int, str]):
+ out = cls()
+ out.set_position(*values[:-1])
+ out.set_raw(values[-1])
+ return out
+
+ @property
+ def format_position(self):
+ if self.start_line is None:
+ return "N/A"
+ return "%d,%d-%d,%d" % self.get_position()
+
+ @property
+ def raw_strip(self):
+ return self.raw.strip()
+
+ def get_position(self):
+ return self.start_line, self.start_col, self.end_line, self.end_col
+
+ def set_position(self, start_line, start_col, end_line, end_col):
+ object.__setattr__(self, "start_line", start_line)
+ object.__setattr__(self, "start_col", start_col)
+ object.__setattr__(self, "end_line", end_line)
+ object.__setattr__(self, "end_col", end_col)
+ return self
+
+ def set_raw(self, raw):
+ object.__setattr__(self, "raw", raw)
+ return self
+
+ def set_simple_position(self, line, col, width):
+ return self.set_position(line, col, line, col + width)
+
+
+@dataclass(frozen=True)
+class ParsingError(Statement, Exception):
+ """Base class for all parsing exceptions in this package."""
+
+ def __str__(self):
+ return Statement.__str__(self)
+
+
+@dataclass(frozen=True)
+class UnknownStatement(ParsingError):
+ """A string statement could not bee parsed."""
+
+ def __str__(self):
+ return f"Could not parse '{self.raw}' ({self.format_position})"
+
+
+@dataclass(frozen=True)
+class UnhandledParsingError(ParsingError):
+ """Base class for all parsing exceptions in this package."""
+
+ ex: Exception
+
+ def __str__(self):
+ return f"Unhandled exception while parsing '{self.raw}' ({self.format_position}): {self.ex}"
+
+
+@dataclass(frozen=True)
+class UnexpectedEOF(ParsingError):
+ """End of file was found within an open block."""
+
+
+#############################
+# Useful methods and classes
+#############################
+
+
+@dataclass(frozen=True)
+class Hash:
+ algorithm_name: str
+ hexdigest: str
+
+ def __eq__(self, other: Hash):
+ return (
+ isinstance(other, Hash)
+ and self.algorithm_name != ""
+ and self.algorithm_name == other.algorithm_name
+ and hmac.compare_digest(self.hexdigest, other.hexdigest)
+ )
+
+ @classmethod
+ def from_bytes(cls, algorithm, b: bytes):
+ hasher = algorithm(b)
+ return cls(hasher.name, hasher.hexdigest())
+
+ @classmethod
+ def from_file_pointer(cls, algorithm, fp: ty.BinaryIO):
+ return cls.from_bytes(algorithm, fp.read())
+
+ @classmethod
+ def nullhash(cls):
+ return cls("", "")
+
+
+def _yield_types(
+ obj, valid_subclasses=(object,), recurse_origin=(tuple, list, ty.Union)
+):
+ """Recursively transverse type annotation if the
+ origin is any of the types in `recurse_origin`
+ and yield those type which are subclasses of `valid_subclasses`.
+
+ """
+ if ty.get_origin(obj) in recurse_origin:
+ for el in ty.get_args(obj):
+ yield from _yield_types(el, valid_subclasses, recurse_origin)
+ else:
+ if inspect.isclass(obj) and issubclass(obj, valid_subclasses):
+ yield obj
+
+
+class classproperty: # noqa N801
+ """Decorator for a class property
+
+ In Python 3.9+ can be replaced by
+
+ @classmethod
+ @property
+ def myprop(self):
+ return 42
+
+ """
+
+ def __init__(self, fget):
+ self.fget = fget
+
+ def __get__(self, owner_self, owner_cls):
+ return self.fget(owner_cls)
+
+
+def is_relative_to(self, *other):
+ """Return True if the path is relative to another path or False.
+
+ In Python 3.9+ can be replaced by
+
+ path.is_relative_to(other)
+ """
+ try:
+ self.relative_to(*other)
+ return True
+ except ValueError:
+ return False
+
+
+class DelimiterInclude(enum.IntEnum):
+ """Specifies how to deal with delimiters while parsing."""
+
+ #: Split at delimiter, not including in any string
+ SPLIT = enum.auto()
+
+ #: Split after, keeping the delimiter with previous string.
+ SPLIT_AFTER = enum.auto()
+
+ #: Split before, keeping the delimiter with next string.
+ SPLIT_BEFORE = enum.auto()
+
+ #: Do not split at delimiter.
+ DO_NOT_SPLIT = enum.auto()
+
+
+class DelimiterAction(enum.IntEnum):
+ """Specifies how to deal with delimiters while parsing."""
+
+ #: Continue parsing normally.
+ CONTINUE = enum.auto()
+
+ #: Capture everything til end of line as a whole.
+ CAPTURE_NEXT_TIL_EOL = enum.auto()
+
+ #: Stop parsing line and move to next.
+ STOP_PARSING_LINE = enum.auto()
+
+ #: Stop parsing content.
+ STOP_PARSING = enum.auto()
+
+
+DO_NOT_SPLIT_EOL = {
+ "\r\n": (DelimiterInclude.DO_NOT_SPLIT, DelimiterAction.CONTINUE),
+ "\n": (DelimiterInclude.DO_NOT_SPLIT, DelimiterAction.CONTINUE),
+ "\r": (DelimiterInclude.DO_NOT_SPLIT, DelimiterAction.CONTINUE),
+}
+
+SPLIT_EOL = {
+ "\r\n": (DelimiterInclude.SPLIT, DelimiterAction.CONTINUE),
+ "\n": (DelimiterInclude.SPLIT, DelimiterAction.CONTINUE),
+ "\r": (DelimiterInclude.SPLIT, DelimiterAction.CONTINUE),
+}
+
+_EOLs_set = set(DO_NOT_SPLIT_EOL.keys())
+
+
+@functools.lru_cache
+def _build_delimiter_pattern(delimiters: ty.Tuple[str, ...]) -> re.Pattern:
+ """Compile a tuple of delimiters into a regex expression with a capture group
+ around the delimiter.
+ """
+ return re.compile("|".join(f"({re.escape(el)})" for el in delimiters))
+
+
+############
+# Iterators
+############
+
+DelimiterDictT = ty.Dict[str, ty.Tuple[DelimiterInclude, DelimiterAction]]
+
+
+class Spliter:
+ """Content iterator splitting according to given delimiters.
+
+ The pattern can be changed dynamically sending a new pattern to the generator,
+ see DelimiterInclude and DelimiterAction for more information.
+
+ The current scanning position can be changed at any time.
+
+ Parameters
+ ----------
+ content : str
+ delimiters : ty.Dict[str, ty.Tuple[DelimiterInclude, DelimiterAction]]
+
+ Yields
+ ------
+ start_line : int
+ line number of the start of the content (zero-based numbering).
+ start_col : int
+ column number of the start of the content (zero-based numbering).
+ end_line : int
+ line number of the end of the content (zero-based numbering).
+ end_col : int
+ column number of the end of the content (zero-based numbering).
+ part : str
+ part of the text between delimiters.
+ """
+
+ _pattern: ty.Optional[re.Pattern]
+ _delimiters: DelimiterDictT
+
+ __stop_searching_in_line = False
+
+ __pending = ""
+ __first_line_col = None
+
+ __lines = ()
+ __lineno = 0
+ __colno = 0
+
+ def __init__(self, content: str, delimiters: DelimiterDictT):
+ self.set_delimiters(delimiters)
+ self.__lines = content.splitlines(keepends=True)
+
+ def set_position(self, lineno: int, colno: int):
+ self.__lineno, self.__colno = lineno, colno
+
+ def set_delimiters(self, delimiters: DelimiterDictT):
+ for k, v in delimiters.items():
+ if v == (DelimiterInclude.DO_NOT_SPLIT, DelimiterAction.STOP_PARSING):
+ raise ValueError(
+ f"The delimiter action for {k} is not a valid combination ({v})"
+ )
+ # Build a pattern but removing eols
+ _pat_dlm = tuple(set(delimiters.keys()) - _EOLs_set)
+ if _pat_dlm:
+ self._pattern = _build_delimiter_pattern(_pat_dlm)
+ else:
+ self._pattern = None
+ # We add the end of line as delimiters if not present.
+ self._delimiters = {**DO_NOT_SPLIT_EOL, **delimiters}
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ if self.__lineno >= len(self.__lines):
+ raise StopIteration
+
+ while True:
+ if self.__stop_searching_in_line:
+ # There must be part of a line pending to parse
+ # due to stop
+ line = self.__lines[self.__lineno]
+ mo = None
+ self.__stop_searching_in_line = False
+ else:
+ # We get the current line and the find the first delimiter.
+ line = self.__lines[self.__lineno]
+ if self._pattern is None:
+ mo = None
+ else:
+ mo = self._pattern.search(line, self.__colno)
+
+ if mo is None:
+ # No delimiter was found,
+ # which should happen at end of the content or end of line
+ for k in DO_NOT_SPLIT_EOL.keys():
+ if line.endswith(k):
+ dlm = line[-len(k) :]
+ end_col, next_col = len(line) - len(k), 0
+ break
+ else:
+ # No EOL found, this is end of content
+ dlm = None
+ end_col, next_col = len(line), 0
+
+ next_line = self.__lineno + 1
+
+ else:
+ next_line = self.__lineno
+ end_col, next_col = mo.span()
+ dlm = mo.group()
+
+ part = line[self.__colno : end_col]
+
+ include, action = self._delimiters.get(
+ dlm, (DelimiterInclude.SPLIT, DelimiterAction.STOP_PARSING)
+ )
+
+ if include == DelimiterInclude.SPLIT:
+ next_pending = ""
+ elif include == DelimiterInclude.SPLIT_AFTER:
+ end_col += len(dlm)
+ part = part + dlm
+ next_pending = ""
+ elif include == DelimiterInclude.SPLIT_BEFORE:
+ next_pending = dlm
+ elif include == DelimiterInclude.DO_NOT_SPLIT:
+ self.__pending += line[self.__colno : end_col] + dlm
+ next_pending = ""
+ else:
+ raise ValueError(f"Unknown action {include}.")
+
+ if action == DelimiterAction.STOP_PARSING:
+ # this will raise a StopIteration in the next call.
+ next_line = len(self.__lines)
+ elif action == DelimiterAction.STOP_PARSING_LINE:
+ next_line = self.__lineno + 1
+ next_col = 0
+
+ start_line = self.__lineno
+ start_col = self.__colno
+ end_line = self.__lineno
+
+ self.__lineno = next_line
+ self.__colno = next_col
+
+ if action == DelimiterAction.CAPTURE_NEXT_TIL_EOL:
+ self.__stop_searching_in_line = True
+
+ if include == DelimiterInclude.DO_NOT_SPLIT:
+ self.__first_line_col = start_line, start_col
+ else:
+ if self.__first_line_col is None:
+ out = (
+ start_line,
+ start_col - len(self.__pending),
+ end_line,
+ end_col,
+ self.__pending + part,
+ )
+ else:
+ out = (
+ *self.__first_line_col,
+ end_line,
+ end_col,
+ self.__pending + part,
+ )
+ self.__first_line_col = None
+ self.__pending = next_pending
+ return out
+
+
+class StatementIterator:
+ """Content peekable iterator splitting according to given delimiters.
+
+ The pattern can be changed dynamically sending a new pattern to the generator,
+ see DelimiterInclude and DelimiterAction for more information.
+
+ Parameters
+ ----------
+ content : str
+ delimiters : dict[str, ty.Tuple[DelimiterInclude, DelimiterAction]]
+
+ Yields
+ ------
+ Statement
+ """
+
+ _cache: ty.Deque[Statement]
+
+ def __init__(
+ self, content: str, delimiters: DelimiterDictT, strip_spaces: bool = True
+ ):
+ self._cache = collections.deque()
+ self._spliter = Spliter(content, delimiters)
+ self._strip_spaces = strip_spaces
+
+ def __iter__(self):
+ return self
+
+ def set_delimiters(self, delimiters: DelimiterDictT):
+ self._spliter.set_delimiters(delimiters)
+ if self._cache:
+ value = self.peek()
+ # Elements are 1 based indexing, while splitter is 0 based.
+ self._spliter.set_position(value.start_line - 1, value.start_col)
+ self._cache.clear()
+
+ def _get_next_strip(self) -> Statement:
+ part = ""
+ while not part:
+ start_line, start_col, end_line, end_col, part = next(self._spliter)
+ lo = len(part)
+ part = part.lstrip()
+ start_col += lo - len(part)
+
+ lo = len(part)
+ part = part.rstrip()
+ end_col -= lo - len(part)
+
+ return Statement.from_statement_iterator_element(
+ (start_line + 1, start_col, end_line + 1, end_col, part)
+ )
+
+ def _get_next(self) -> Statement:
+ if self._strip_spaces:
+ return self._get_next_strip()
+
+ part = ""
+ while not part:
+ start_line, start_col, end_line, end_col, part = next(self._spliter)
+
+ return Statement.from_statement_iterator_element(
+ (start_line + 1, start_col, end_line + 1, end_col, part)
+ )
+
+ def peek(self, default=_SENTINEL) -> Statement:
+ """Return the item that will be next returned from ``next()``.
+
+ Return ``default`` if there are no items left. If ``default`` is not
+ provided, raise ``StopIteration``.
+
+ """
+ if not self._cache:
+ try:
+ self._cache.append(self._get_next())
+ except StopIteration:
+ if default is _SENTINEL:
+ raise
+ return default
+ return self._cache[0]
+
+ def __next__(self) -> Statement:
+ if self._cache:
+ return self._cache.popleft()
+ else:
+ return self._get_next()
+
+
+###########
+# Parsing
+###########
+
+# Configuration type
+CT = ty.TypeVar("CT")
+PST = ty.TypeVar("PST", bound="ParsedStatement")
+LineColStr = Tuple[int, int, str]
+FromString = ty.Union[None, PST, ParsingError]
+Consume = ty.Union[PST, ParsingError]
+NullableConsume = ty.Union[None, PST, ParsingError]
+
+Single = ty.Union[PST, ParsingError]
+Multi = ty.Tuple[ty.Union[PST, ParsingError], ...]
+
+
+@dataclass(frozen=True)
+class ParsedStatement(ty.Generic[CT], Statement):
+ """A single parsed statement.
+
+ In order to write your own, you need to subclass it as a
+ frozen dataclass and implement the parsing logic by overriding
+ `from_string` classmethod.
+
+ Takes two arguments: the string to parse and an object given
+ by the parser which can be used to store configuration information.
+
+ It should return an instance of this class if parsing
+ was successful or None otherwise
+ """
+
+ @classmethod
+ def from_string(cls: Type[PST], s: str) -> FromString[PST]:
+ """Parse a string into a ParsedStatement.
+
+ Return files and their meaning:
+ 1. None: the string cannot be parsed with this class.
+ 2. A subclass of ParsedStatement: the string was parsed successfully
+ 3. A subclass of ParsingError the string could be parsed with this class but there is
+ an error.
+ """
+ raise NotImplementedError(
+ "ParsedStatement subclasses must implement "
+ "'from_string' or 'from_string_and_config'"
+ )
+
+ @classmethod
+ def from_string_and_config(cls: Type[PST], s: str, config: CT) -> FromString[PST]:
+ """Parse a string into a ParsedStatement.
+
+ Return files and their meaning:
+ 1. None: the string cannot be parsed with this class.
+ 2. A subclass of ParsedStatement: the string was parsed successfully
+ 3. A subclass of ParsingError the string could be parsed with this class but there is
+ an error.
+ """
+ return cls.from_string(s)
+
+ @classmethod
+ def from_statement_and_config(
+ cls: Type[PST], statement: Statement, config: CT
+ ) -> FromString[PST]:
+ try:
+ out = cls.from_string_and_config(statement.raw, config)
+ except Exception as ex:
+ out = UnhandledParsingError(ex)
+
+ if out is None:
+ return None
+
+ out.set_position(*statement.get_position())
+ out.set_raw(statement.raw)
+ return out
+
+ @classmethod
+ def consume(
+ cls: Type[PST], statement_iterator: StatementIterator, config: CT
+ ) -> NullableConsume[PST]:
+ """Peek into the iterator and try to parse.
+
+ Return files and their meaning:
+ 1. None: the string cannot be parsed with this class, the iterator is kept an the current place.
+ 2. a subclass of ParsedStatement: the string was parsed successfully, advance the iterator.
+ 3. a subclass of ParsingError: the string could be parsed with this class but there is
+ an error, advance the iterator.
+ """
+ statement = statement_iterator.peek()
+ parsed_statement = cls.from_statement_and_config(statement, config)
+ if parsed_statement is None:
+ return None
+ next(statement_iterator)
+ return parsed_statement
+
+
+OPST = ty.TypeVar("OPST", bound="ParsedStatement")
+IPST = ty.TypeVar("IPST", bound="ParsedStatement")
+CPST = ty.TypeVar("CPST", bound="ParsedStatement")
+BT = ty.TypeVar("BT", bound="Block")
+RBT = ty.TypeVar("RBT", bound="RootBlock")
+
+
+@dataclass(frozen=True)
+class Block(ty.Generic[OPST, IPST, CPST, CT]):
+ """A sequence of statements with an opening, body and closing."""
+
+ opening: Consume[OPST]
+ body: Tuple[Consume[IPST], ...]
+ closing: Consume[CPST]
+
+ delimiters = {}
+
+ @property
+ def start_line(self):
+ return self.opening.start_line
+
+ @property
+ def start_col(self):
+ return self.opening.start_col
+
+ @property
+ def end_line(self):
+ return self.closing.end_line
+
+ @property
+ def end_col(self):
+ return self.closing.end_col
+
+ def get_position(self):
+ return self.start_line, self.start_col, self.end_line, self.end_col
+
+ @property
+ def format_position(self):
+ if self.start_line is None:
+ return "N/A"
+ return "%d,%d-%d,%d" % self.get_position()
+
+ @classmethod
+ def subclass_with(cls, *, opening=None, body=None, closing=None):
+ @dataclass(frozen=True)
+ class CustomBlock(Block):
+ pass
+
+ if opening:
+ CustomBlock.__annotations__["opening"] = Single[ty.Union[opening]]
+ if body:
+ CustomBlock.__annotations__["body"] = Multi[ty.Union[body]]
+ if closing:
+ CustomBlock.__annotations__["closing"] = Single[ty.Union[closing]]
+
+ return CustomBlock
+
+ def __iter__(self) -> Iterator[Statement]:
+ yield self.opening
+ for el in self.body:
+ if isinstance(el, Block):
+ yield from el
+ else:
+ yield el
+ yield self.closing
+
+ def iter_blocks(self) -> Iterator[ty.Union[Block, Statement]]:
+ yield self.opening
+ yield from self.body
+ yield self.closing
+
+ ###################################################
+ # Convenience methods to iterate parsed statements
+ ###################################################
+
+ _ElementT = ty.TypeVar("_ElementT", bound=Statement)
+
+ def filter_by(self, *klass: Type[_ElementT]) -> Iterator[_ElementT]:
+ """Yield elements of a given class or classes."""
+ yield from (el for el in self if isinstance(el, klass)) # noqa Bug in pycharm.
+
+ @cached_property
+ def errors(self) -> ty.Tuple[ParsingError, ...]:
+ """Tuple of errors found."""
+ return tuple(self.filter_by(ParsingError))
+
+ @property
+ def has_errors(self) -> bool:
+ """True if errors were found during parsing."""
+ return bool(self.errors)
+
+ ####################
+ # Statement classes
+ ####################
+
+ @classproperty
+ def opening_classes(cls) -> Iterator[Type[OPST]]:
+ """Classes representing any of the parsed statement that can open this block."""
+ opening = ty.get_type_hints(cls)["opening"]
+ yield from _yield_types(opening, ParsedStatement)
+
+ @classproperty
+ def body_classes(cls) -> Iterator[Type[IPST]]:
+ """Classes representing any of the parsed statement that can be in the body."""
+ body = ty.get_type_hints(cls)["body"]
+ yield from _yield_types(body, (ParsedStatement, Block))
+
+ @classproperty
+ def closing_classes(cls) -> Iterator[Type[CPST]]:
+ """Classes representing any of the parsed statement that can close this block."""
+ closing = ty.get_type_hints(cls)["closing"]
+ yield from _yield_types(closing, ParsedStatement)
+
+ ##########
+ # Consume
+ ##########
+
+ @classmethod
+ def consume_opening(
+ cls: Type[BT], statement_iterator: StatementIterator, config: CT
+ ) -> NullableConsume[OPST]:
+ """Peek into the iterator and try to parse with any of the opening classes.
+
+ See `ParsedStatement.consume` for more details.
+ """
+ for c in cls.opening_classes:
+ el = c.consume(statement_iterator, config)
+ if el is not None:
+ return el
+ return None
+
+ @classmethod
+ def consume_body(
+ cls, statement_iterator: StatementIterator, config: CT
+ ) -> Consume[IPST]:
+ """Peek into the iterator and try to parse with any of the body classes.
+
+ If the statement cannot be parsed, a UnknownStatement is returned.
+ """
+ for c in cls.body_classes:
+ el = c.consume(statement_iterator, config)
+ if el is not None:
+ return el
+ el = next(statement_iterator)
+ return UnknownStatement.from_statement(el)
+
+ @classmethod
+ def consume_closing(
+ cls: Type[BT], statement_iterator: StatementIterator, config: CT
+ ) -> NullableConsume[CPST]:
+ """Peek into the iterator and try to parse with any of the opening classes.
+
+ See `ParsedStatement.consume` for more details.
+ """
+ for c in cls.closing_classes:
+ el = c.consume(statement_iterator, config)
+ if el is not None:
+ return el
+ return None
+
+ @classmethod
+ def consume_body_closing(
+ cls: Type[BT], opening: OPST, statement_iterator: StatementIterator, config: CT
+ ) -> BT:
+ body = []
+ closing = None
+ last_line = opening.end_line
+ while closing is None:
+ try:
+ closing = cls.consume_closing(statement_iterator, config)
+ if closing is not None:
+ continue
+ el = cls.consume_body(statement_iterator, config)
+ body.append(el)
+ last_line = el.end_line
+ except StopIteration:
+ closing = cls.on_stop_iteration(config)
+ closing.set_position(last_line + 1, 0, last_line + 1, 0)
+
+ return cls(opening, tuple(body), closing)
+
+ @classmethod
+ def consume(
+ cls: Type[BT], statement_iterator: StatementIterator, config: CT
+ ) -> Optional[BT]:
+ """Try consume the block.
+
+ Possible outcomes:
+ 1. The opening was not matched, return None.
+ 2. A subclass of Block, where body and closing migh contain errors.
+ """
+ opening = cls.consume_opening(statement_iterator, config)
+ if opening is None:
+ return None
+
+ return cls.consume_body_closing(opening, statement_iterator, config)
+
+ @classmethod
+ def on_stop_iteration(cls, config):
+ return UnexpectedEOF()
+
+
+@dataclass(frozen=True)
+class BOS(ParsedStatement[CT]):
+ """Beginning of source."""
+
+ # Hasher algorithm name and hexdigest
+ content_hash: Hash
+
+ @classmethod
+ def from_string_and_config(cls: Type[PST], s: str, config: CT) -> FromString[PST]:
+ raise RuntimeError("BOS cannot be constructed from_string_and_config")
+
+ @property
+ def location(self) -> SourceLocationT:
+ return "<undefined>"
+
+
+@dataclass(frozen=True)
+class BOF(BOS):
+ """Beginning of file."""
+
+ path: pathlib.Path
+
+ # Modification time of the file.
+ mtime: float
+
+ @property
+ def location(self) -> SourceLocationT:
+ return self.path
+
+
+@dataclass(frozen=True)
+class BOR(BOS):
+ """Beginning of resource."""
+
+ package: str
+ resource_name: str
+
+ @property
+ def location(self) -> SourceLocationT:
+ return self.package, self.resource_name
+
+
+@dataclass(frozen=True)
+class EOS(ParsedStatement[CT]):
+ """End of sequence."""
+
+ @classmethod
+ def from_string_and_config(cls: Type[PST], s: str, config: CT) -> FromString[PST]:
+ return cls()
+
+
+class RootBlock(ty.Generic[IPST, CT], Block[BOS, IPST, EOS, CT]):
+ """A sequence of statement flanked by the beginning and ending of stream."""
+
+ opening: Single[BOS]
+ closing: Single[EOS]
+
+ @classmethod
+ def subclass_with(cls, *, body=None):
+ @dataclass(frozen=True)
+ class CustomRootBlock(RootBlock):
+ pass
+
+ if body:
+ CustomRootBlock.__annotations__["body"] = Multi[ty.Union[body]]
+
+ return CustomRootBlock
+
+ @classmethod
+ def consume_opening(
+ cls: Type[RBT], statement_iterator: StatementIterator, config: CT
+ ) -> NullableConsume[BOS]:
+ raise RuntimeError(
+ "Implementation error, 'RootBlock.consume_opening' should never be called"
+ )
+
+ @classmethod
+ def consume(
+ cls: Type[RBT], statement_iterator: StatementIterator, config: CT
+ ) -> RBT:
+ block = super().consume(statement_iterator, config)
+ if block is None:
+ raise RuntimeError(
+ "Implementation error, 'RootBlock.consume' should never return None"
+ )
+ return block
+
+ @classmethod
+ def consume_closing(
+ cls: Type[RBT], statement_iterator: StatementIterator, config: CT
+ ) -> NullableConsume[EOS]:
+ return None
+
+ @classmethod
+ def on_stop_iteration(cls, config):
+ return EOS()
+
+
+#################
+# Source parsing
+#################
+
+ResourceT = ty.Tuple[str, str] # package name, resource name
+StrictLocationT = ty.Union[pathlib.Path, ResourceT]
+SourceLocationT = ty.Union[str, StrictLocationT]
+
+
+@dataclass(frozen=True)
+class ParsedSource(ty.Generic[RBT, CT]):
+
+ parsed_source: RBT
+
+ # Parser configuration.
+ config: CT
+
+ @property
+ def location(self) -> StrictLocationT:
+ return self.parsed_source.opening.location
+
+ @cached_property
+ def has_errors(self) -> bool:
+ return self.parsed_source.has_errors
+
+ def errors(self):
+ yield from self.parsed_source.errors
+
+
+@dataclass(frozen=True)
+class CannotParseResourceAsFile(Exception):
+ """The requested python package resource cannot be located as a file
+ in the file system.
+ """
+
+ package: str
+ resource_name: str
+
+
+class Parser(ty.Generic[RBT, CT]):
+ """Parser class."""
+
+ #: class to iterate through statements in a source unit.
+ _statement_iterator_class: Type[StatementIterator] = StatementIterator
+
+ #: Delimiters.
+ _delimiters: DelimiterDictT = SPLIT_EOL
+
+ _strip_spaces: bool = True
+
+ #: root block class containing statements and blocks can be parsed.
+ _root_block_class: Type[RBT]
+
+ #: source file text encoding.
+ _encoding = "utf-8"
+
+ #: configuration passed to from_string functions.
+ _config: CT
+
+ #: try to open resources as files.
+ _prefer_resource_as_file: bool
+
+ #: parser algorithm to us. Must be a callable member of hashlib
+ _hasher = hashlib.blake2b
+
+ def __init__(self, config: CT, prefer_resource_as_file=True):
+ self._config = config
+ self._prefer_resource_as_file = prefer_resource_as_file
+
+ def parse(self, source_location: SourceLocationT) -> ParsedSource[RBT, CT]:
+ """Parse a file into a ParsedSourceFile or ParsedResource.
+
+ Parameters
+ ----------
+ source_location:
+ if str or pathlib.Path is interpreted as a file.
+ if (str, str) is interpreted as (package, resource) using the resource python api.
+ """
+ if isinstance(source_location, tuple) and len(source_location) == 2:
+ if self._prefer_resource_as_file:
+ try:
+ return self.parse_resource_from_file(*source_location)
+ except CannotParseResourceAsFile:
+ pass
+ return self.parse_resource(*source_location)
+
+ if isinstance(source_location, str):
+ return self.parse_file(pathlib.Path(source_location))
+
+ if isinstance(source_location, pathlib.Path):
+ return self.parse_file(source_location)
+
+ raise TypeError(
+ f"Unknown type {type(source_location)}, "
+ "use str or pathlib.Path for files or "
+ "(package: str, resource_name: str) tuple "
+ "for a resource."
+ )
+
+ def parse_bytes(self, b: bytes, bos: BOS = None) -> ParsedSource[RBT, CT]:
+ if bos is None:
+ bos = BOS(Hash.from_bytes(self._hasher, b)).set_simple_position(0, 0, 0)
+
+ sic = self._statement_iterator_class(
+ b.decode(self._encoding), self._delimiters, self._strip_spaces
+ )
+
+ parsed = self._root_block_class.consume_body_closing(bos, sic, self._config)
+
+ return ParsedSource(
+ parsed,
+ self._config,
+ )
+
+ def parse_file(self, path: pathlib.Path) -> ParsedSource[RBT, CT]:
+ """Parse a file into a ParsedSourceFile.
+
+ Parameters
+ ----------
+ path
+ path of the file.
+ """
+ with path.open(mode="rb") as fi:
+ content = fi.read()
+
+ bos = BOF(
+ Hash.from_bytes(self._hasher, content), path, path.stat().st_mtime
+ ).set_simple_position(0, 0, 0)
+ return self.parse_bytes(content, bos)
+
+ def parse_resource_from_file(
+ self, package: str, resource_name: str
+ ) -> ParsedSource[RBT, CT]:
+ """Parse a resource into a ParsedSourceFile, opening as a file.
+
+ Parameters
+ ----------
+ package
+ package name where the resource is located.
+ resource_name
+ name of the resource
+ """
+ if sys.version_info < (3, 9):
+ # Remove when Python 3.8 is dropped
+ with resources.path(package, resource_name) as p:
+ path = p.resolve()
+ else:
+ with resources.as_file(
+ resources.files(package).joinpath(resource_name)
+ ) as p:
+ path = p.resolve()
+
+ if path.exists():
+ return self.parse_file(path)
+
+ raise CannotParseResourceAsFile(package, resource_name)
+
+ def parse_resource(self, package: str, resource_name: str) -> ParsedSource[RBT, CT]:
+ """Parse a resource into a ParsedResource.
+
+ Parameters
+ ----------
+ package
+ package name where the resource is located.
+ resource_name
+ name of the resource
+ """
+ if sys.version_info < (3, 9):
+ # Remove when Python 3.8 is dropped
+ with resources.open_binary(package, resource_name) as fi:
+ content = fi.read()
+ else:
+ with resources.files(package).joinpath(resource_name).open("rb") as fi:
+ content = fi.read()
+
+ bos = BOR(
+ Hash.from_bytes(self._hasher, content), package, resource_name
+ ).set_simple_position(0, 0, 0)
+
+ return self.parse_bytes(content, bos)
+
+
+##########
+# Project
+##########
+
+
+class IncludeStatement(ParsedStatement):
+ """ "Include statements allow to merge files."""
+
+ @property
+ def target(self) -> str:
+ raise NotImplementedError(
+ "IncludeStatement subclasses must implement target property."
+ )
+
+
+class ParsedProject(
+ ty.Dict[
+ ty.Optional[ty.Tuple[StrictLocationT, str]],
+ ParsedSource,
+ ]
+):
+ """Collection of files, independent or connected via IncludeStatement.
+
+ Keys are either an absolute pathname or a tuple package name, resource name.
+
+ None is the name of the root.
+
+ """
+
+ @cached_property
+ def has_errors(self) -> bool:
+ return any(el.has_errors for el in self.values())
+
+ def errors(self):
+ for el in self.values():
+ yield from el.errors()
+
+ def _iter_statements(self, items, seen, include_only_once):
+ """Iter all definitions in the order they appear,
+ going into the included files.
+ """
+ for source_location, parsed in items:
+ seen.add(source_location)
+ for parsed_statement in parsed.parsed_source:
+ if isinstance(parsed_statement, IncludeStatement):
+ location = parsed.location, parsed_statement.target
+ if location in seen and include_only_once:
+ raise ValueError(f"{location} was already included.")
+ yield from self._iter_statements(
+ ((location, self[location]),), seen, include_only_once
+ )
+ else:
+ yield parsed_statement
+
+ def iter_statements(self, include_only_once=True):
+ """Iter all definitions in the order they appear,
+ going into the included files.
+
+ Parameters
+ ----------
+ include_only_once
+ if true, each file cannot be included more than once.
+ """
+ yield from self._iter_statements([(None, self[None])], set(), include_only_once)
+
+ def _iter_blocks(self, items, seen, include_only_once):
+ """Iter all definitions in the order they appear,
+ going into the included files.
+ """
+ for source_location, parsed in items:
+ seen.add(source_location)
+ for parsed_statement in parsed.parsed_source.iter_blocks():
+ if isinstance(parsed_statement, IncludeStatement):
+ location = parsed.location, parsed_statement.target
+ if location in seen and include_only_once:
+ raise ValueError(f"{location} was already included.")
+ yield from self._iter_blocks(
+ ((location, self[location]),), seen, include_only_once
+ )
+ else:
+ yield parsed_statement
+
+ def iter_blocks(self, include_only_once=True):
+ """Iter all definitions in the order they appear,
+ going into the included files.
+
+ Parameters
+ ----------
+ include_only_once
+ if true, each file cannot be included more than once.
+ """
+ yield from self._iter_blocks([(None, self[None])], set(), include_only_once)
+
+
+def default_locator(source_location: StrictLocationT, target: str) -> StrictLocationT:
+ """Return a new location from current_location and target."""
+
+ if isinstance(source_location, pathlib.Path):
+ current_location = pathlib.Path(source_location).resolve()
+
+ if current_location.is_file():
+ current_path = current_location.parent
+ else:
+ current_path = current_location
+
+ target_path = pathlib.Path(target)
+ if target_path.is_absolute():
+ raise ValueError(
+ f"Cannot refer to absolute paths in import statements ({source_location}, {target})."
+ )
+
+ tmp = (current_path / target_path).resolve()
+ if not is_relative_to(tmp, current_path):
+ raise ValueError(
+ f"Cannot refer to locations above the current location ({source_location}, {target})"
+ )
+
+ return tmp.absolute()
+
+ elif isinstance(source_location, tuple) and len(source_location) == 2:
+ return source_location[0], target
+
+ raise TypeError(
+ f"Cannot handle type {type(source_location)}, "
+ "use str or pathlib.Path for files or "
+ "(package: str, resource_name: str) tuple "
+ "for a resource."
+ )
+
+
+DefinitionT = ty.Union[ty.Type[Block], ty.Type[ParsedStatement]]
+
+SpecT = ty.Union[
+ ty.Type[Parser],
+ DefinitionT,
+ ty.Iterable[DefinitionT],
+ ty.Type[RootBlock],
+]
+
+
+def build_parser_class(spec: SpecT, *, strip_spaces: bool = True, delimiters=None):
+ """Build a custom parser class.
+
+ Parameters
+ ----------
+ spec
+ specification of the content to parse. Can be one of the following things:
+ - Parser class.
+ - Block or ParsedStatement derived class.
+ - Iterable of Block or ParsedStatement derived class.
+ - RootBlock derived class.
+ strip_spaces : bool
+ if True, spaces will be stripped for each statement before calling
+ ``from_string_and_config``.
+ delimiters : dict
+ Specify how the source file is split into statements (See below).
+
+ Delimiters dictionary
+ ---------------------
+ The delimiters are specified with the keys of the delimiters dict.
+ The dict files can be used to further customize the iterator. Each
+ consist of a tuple of two elements:
+ 1. A value of the DelimiterMode to indicate what to do with the
+ delimiter string: skip it, attach keep it with previous or next string
+ 2. A boolean indicating if parsing should stop after fiSBT
+ encountering this delimiter.
+ """
+
+ if delimiters is None:
+ delimiters = SPLIT_EOL
+
+ if isinstance(spec, type) and issubclass(spec, Parser):
+ CustomParser = spec
+ else:
+ if isinstance(spec, (tuple, list)):
+
+ for el in spec:
+ if not issubclass(el, (Block, ParsedStatement)):
+ raise TypeError(
+ "Elements in root_block_class must be of type Block or ParsedStatement, "
+ f"not {el}"
+ )
+
+ @dataclass(frozen=True)
+ class CustomRootBlock(RootBlock):
+ pass
+
+ CustomRootBlock.__annotations__["body"] = Multi[ty.Union[spec]]
+
+ elif isinstance(spec, type) and issubclass(spec, RootBlock):
+
+ CustomRootBlock = spec
+
+ elif isinstance(spec, type) and issubclass(spec, (Block, ParsedStatement)):
+
+ @dataclass(frozen=True)
+ class CustomRootBlock(RootBlock):
+ pass
+
+ CustomRootBlock.__annotations__["body"] = Multi[spec]
+
+ else:
+ raise TypeError(
+ "`spec` must be of type RootBlock or tuple of type Block or ParsedStatement, "
+ f"not {type(spec)}"
+ )
+
+ class CustomParser(Parser):
+
+ _delimiters = delimiters
+ _root_block_class = CustomRootBlock
+ _strip_spaces = strip_spaces
+
+ return CustomParser
+
+
+def parse(
+ entry_point: SourceLocationT,
+ spec: SpecT,
+ config=None,
+ *,
+ strip_spaces: bool = True,
+ delimiters=None,
+ locator: ty.Callable[[StrictLocationT, str], StrictLocationT] = default_locator,
+ prefer_resource_as_file: bool = True,
+ **extra_parser_kwargs,
+) -> ParsedProject:
+ """Parse sources into a ParsedProject dictionary.
+
+ Parameters
+ ----------
+ entry_point
+ file or resource, given as (package_name, resource_name).
+ spec
+ specification of the content to parse. Can be one of the following things:
+ - Parser class.
+ - Block or ParsedStatement derived class.
+ - Iterable of Block or ParsedStatement derived class.
+ - RootBlock derived class.
+ config
+ a configuration object that will be passed to `from_string_and_config`
+ classmethod.
+ strip_spaces : bool
+ if True, spaces will be stripped for each statement before calling
+ ``from_string_and_config``.
+ delimiters : dict
+ Specify how the source file is split into statements (See below).
+ locator : Callable
+ function that takes the current location and a target of an IncludeStatement
+ and returns a new location.
+ prefer_resource_as_file : bool
+ if True, resources will try to be located in the filesystem if
+ available.
+ extra_parser_kwargs
+ extra keyword arguments to be given to the parser.
+
+ Delimiters dictionary
+ ---------------------
+ The delimiters are specified with the keys of the delimiters dict.
+ The dict files can be used to further customize the iterator. Each
+ consist of a tuple of two elements:
+ 1. A value of the DelimiterMode to indicate what to do with the
+ delimiter string: skip it, attach keep it with previous or next string
+ 2. A boolean indicating if parsing should stop after fiSBT
+ encountering this delimiter.
+ """
+
+ CustomParser = build_parser_class(
+ spec, strip_spaces=strip_spaces, delimiters=delimiters
+ )
+ parser = CustomParser(
+ config, prefer_resource_as_file=prefer_resource_as_file, **extra_parser_kwargs
+ )
+
+ pp = ParsedProject()
+
+ # : ty.List[Optional[ty.Union[LocatorT, str]], ...]
+ pending: ty.List[ty.Tuple[StrictLocationT, str]] = []
+ if isinstance(entry_point, (str, pathlib.Path)):
+ entry_point = pathlib.Path(entry_point)
+ if not entry_point.is_absolute():
+ entry_point = pathlib.Path.cwd() / entry_point
+
+ elif not (isinstance(entry_point, tuple) and len(entry_point) == 2):
+ raise TypeError(
+ f"Cannot handle type {type(entry_point)}, "
+ "use str or pathlib.Path for files or "
+ "(package: str, resource_name: str) tuple "
+ "for a resource."
+ )
+
+ pp[None] = parsed = parser.parse(entry_point)
+ pending.extend(
+ (parsed.location, el.target)
+ for el in parsed.parsed_source.filter_by(IncludeStatement)
+ )
+
+ while pending:
+ source_location, target = pending.pop(0)
+ pp[(source_location, target)] = parsed = parser.parse(
+ locator(source_location, target)
+ )
+ pending.extend(
+ (parsed.location, el.target)
+ for el in parsed.parsed_source.filter_by(IncludeStatement)
+ )
+
+ return pp
+
+
+def parse_bytes(
+ content: bytes,
+ spec: SpecT,
+ config=None,
+ *,
+ strip_spaces: bool = True,
+ delimiters=None,
+ **extra_parser_kwargs,
+) -> ParsedProject:
+ """Parse sources into a ParsedProject dictionary.
+
+ Parameters
+ ----------
+ content
+ bytes.
+ spec
+ specification of the content to parse. Can be one of the following things:
+ - Parser class.
+ - Block or ParsedStatement derived class.
+ - Iterable of Block or ParsedStatement derived class.
+ - RootBlock derived class.
+ config
+ a configuration object that will be passed to `from_string_and_config`
+ classmethod.
+ strip_spaces : bool
+ if True, spaces will be stripped for each statement before calling
+ ``from_string_and_config``.
+ delimiters : dict
+ Specify how the source file is split into statements (See below).
+ """
+
+ CustomParser = build_parser_class(
+ spec, strip_spaces=strip_spaces, delimiters=delimiters
+ )
+ parser = CustomParser(config, prefer_resource_as_file=False, **extra_parser_kwargs)
+
+ pp = ParsedProject()
+
+ pp[None] = parsed = parser.parse_bytes(content)
+
+ if any(parsed.parsed_source.filter_by(IncludeStatement)):
+ raise ValueError("parse_bytes does not support using an IncludeStatement")
+
+ return pp
diff --git a/pint/compat.py b/pint/compat.py
index a76e15c..de149ac 100644
--- a/pint/compat.py
+++ b/pint/compat.py
@@ -13,6 +13,7 @@ from __future__ import annotations
import math
import tokenize
from decimal import Decimal
+from importlib import import_module
from io import BytesIO
from numbers import Number
@@ -80,7 +81,6 @@ try:
NP_NO_VALUE = np._NoValue
except ImportError:
-
np = None
class ndarray:
@@ -166,53 +166,58 @@ if not HAS_MIP:
# Define location of pint.Quantity in NEP-13 type cast hierarchy by defining upcast
# types using guarded imports
-upcast_types = []
-# pint-pandas (PintArray)
try:
- from pint_pandas import PintArray
-
- upcast_types.append(PintArray)
+ from dask import array as dask_array
+ from dask.base import compute, persist, visualize
except ImportError:
- pass
+ compute, persist, visualize = None, None, None
+ dask_array = None
-# Pandas (Series)
-try:
- from pandas import Series
- upcast_types.append(Series)
-except ImportError:
- pass
+upcast_type_names = (
+ "pint_pandas.PintArray",
+ "pandas.Series",
+ "xarray.core.dataarray.DataArray",
+ "xarray.core.dataset.Dataset",
+ "xarray.core.variable.Variable",
+ "pandas.core.series.Series",
+ "xarray.core.dataarray.DataArray",
+)
-# xarray (DataArray, Dataset, Variable)
-try:
- from xarray import DataArray, Dataset, Variable
+upcast_type_map = {k: None for k in upcast_type_names}
- upcast_types += [DataArray, Dataset, Variable]
-except ImportError:
- pass
-try:
- from dask import array as dask_array
- from dask.base import compute, persist, visualize
+def fully_qualified_name(obj):
+ t = type(obj)
+ module = t.__module__
+ name = t.__qualname__
-except ImportError:
- compute, persist, visualize = None, None, None
- dask_array = None
+ if module is None or module == "__builtin__":
+ return name
+ return f"{module}.{name}"
-def is_upcast_type(other) -> bool:
- """Check if the type object is a upcast type using preset list.
- Parameters
- ----------
- other : object
+def check_upcast_type(obj):
+ fqn = fully_qualified_name(obj)
+ if fqn not in upcast_type_map:
+ return False
+ else:
+ module_name, class_name = fqn.rsplit(".", 1)
+ cls = getattr(import_module(module_name), class_name)
- Returns
- -------
- bool
- """
- return other in upcast_types
+ upcast_type_map[fqn] = cls
+ # This is to check we are importing the same thing.
+ # and avoid weird problems. Maybe instead of return
+ # we should raise an error if false.
+ return isinstance(obj, cls)
+
+
+def is_upcast_type(other):
+ if other in upcast_type_map.values():
+ return True
+ return check_upcast_type(other)
def is_duck_array_type(cls) -> bool:
diff --git a/pint/default_en.txt b/pint/default_en.txt
index a3d3a2a..5fc7f82 100644
--- a/pint/default_en.txt
+++ b/pint/default_en.txt
@@ -62,6 +62,8 @@
#### PREFIXES ####
# decimal prefixes
+quecto- = 1e-30 = q-
+ronto- = 1e-27 = r-
yocto- = 1e-24 = y-
zepto- = 1e-21 = z-
atto- = 1e-18 = a-
@@ -84,6 +86,8 @@ peta- = 1e15 = P-
exa- = 1e18 = E-
zetta- = 1e21 = Z-
yotta- = 1e24 = Y-
+ronna- = 1e27 = R-
+quetta- = 1e30 = Q-
# binary_prefixes
kibi- = 2**10 = Ki-
@@ -144,6 +148,10 @@ byte = 8 * bit = B = octet
# byte = 8 * bit = _ = octet
## NOTE: B (byte) symbol can conflict with Bell
+# Ratios
+percent = 0.01 = %
+ppm = 1e-6
+
# Length
angstrom = 1e-10 * meter = Å = ångström = Å
micron = micrometer = µ = μ
@@ -230,7 +238,8 @@ counts_per_second = count / second = cps
reciprocal_centimeter = 1 / cm = cm_1 = kayser
# Velocity
-[velocity] = [length] / [time] = [speed]
+[velocity] = [length] / [time]
+[speed] = [velocity]
knot = nautical_mile / hour = kt = knot_international = international_knot
mile_per_hour = mile / hour = mph = MPH
kilometer_per_hour = kilometer / hour = kph = KPH
@@ -421,6 +430,10 @@ atomic_unit_of_electric_field = e * k_C / a_0 ** 2 = a_u_electric_field
# Electric displacement field
[electric_displacement_field] = [charge] / [area]
+# Reduced electric field
+[reduced_electric_field] = [electric_field] * [area]
+townsend = 1e-21 * V * m^2 = Td
+
# Resistance
[resistance] = [electric_potential] / [current]
ohm = volt / ampere = Ω
@@ -443,17 +456,17 @@ farad = coulomb / volt = F
abfarad = 1e9 * farad = abF
conventional_farad_90 = R_K90 / R_K * farad = F_90
+# Magnetic flux
+[magnetic_flux] = [electric_potential] * [time]
+weber = volt * second = Wb
+unit_pole = µ_0 * biot * centimeter
+
# Inductance
[inductance] = [magnetic_flux] / [current]
henry = weber / ampere = H
abhenry = 1e-9 * henry = abH
conventional_henry_90 = R_K / R_K90 * henry = H_90
-# Magnetic flux
-[magnetic_flux] = [electric_potential] * [time]
-weber = volt * second = Wb
-unit_pole = µ_0 * biot * centimeter
-
# Magnetic field
[magnetic_field] = [magnetic_flux] / [area]
tesla = weber / meter ** 2 = T
@@ -659,7 +672,7 @@ neper = 1 ; logbase: 2.71828182845904523536028747135266249775724709369995; logfa
@group Textile
tex = gram / kilometer = Tt
dtex = decitex
- denier = gram / (9 * kilometer) = den = Td
+ denier = gram / (9 * kilometer) = den
jute = pound / (14400 * yard) = Tj
aberdeen = jute = Ta
RKM = gf / tex
diff --git a/pint/definitions.py b/pint/definitions.py
index 2459b4c..789d9e3 100644
--- a/pint/definitions.py
+++ b/pint/definitions.py
@@ -2,146 +2,27 @@
pint.definitions
~~~~~~~~~~~~~~~~
- Functions and classes related to unit definitions.
+ Kept for backwards compatibility
- :copyright: 2016 by Pint Authors, see AUTHORS for more details.
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""
-from __future__ import annotations
+from . import errors
+from ._vendor import flexparser as fp
+from .delegates import ParserConfig, txt_defparser
-from dataclasses import dataclass
-from typing import Callable, Optional, Tuple, Union
-from .converters import Converter
-
-
-@dataclass(frozen=True)
-class PreprocessedDefinition:
- """Splits a definition into the constitutive parts.
-
- A definition is given as a string with equalities in a single line::
-
- ---------------> rhs
- a = b = c = d = e
- | | | -------> aliases (optional)
- | | |
- | | -----------> symbol (use "_" for no symbol)
- | |
- | ---------------> value
- |
- -------------------> name
- """
-
- name: str
- symbol: Optional[str]
- aliases: Tuple[str, ...]
- value: str
- rhs_parts: Tuple[str, ...]
-
- @classmethod
- def from_string(cls, definition: str) -> PreprocessedDefinition:
- name, definition = definition.split("=", 1)
- name = name.strip()
-
- rhs_parts = tuple(res.strip() for res in definition.split("="))
-
- value, aliases = rhs_parts[0], tuple([x for x in rhs_parts[1:] if x != ""])
- symbol, aliases = (aliases[0], aliases[1:]) if aliases else (None, aliases)
- if symbol == "_":
- symbol = None
- aliases = tuple([x for x in aliases if x != "_"])
-
- return cls(name, symbol, aliases, value, rhs_parts)
-
-
-@dataclass(frozen=True)
class Definition:
- """Base class for definitions.
-
- Parameters
- ----------
- name : str
- Canonical name of the unit/prefix/etc.
- defined_symbol : str or None
- A short name or symbol for the definition.
- aliases : iterable of str
- Other names for the unit/prefix/etc.
- converter : callable or Converter or None
- """
-
- name: str
- defined_symbol: Optional[str]
- aliases: Tuple[str, ...]
- converter: Optional[Union[Callable, Converter]]
-
- _subclasses = []
- _default_subclass = None
-
- def __init_subclass__(cls, **kwargs):
- if kwargs.pop("default", False):
- if cls._default_subclass is not None:
- raise ValueError("There is already a registered default definition.")
- Definition._default_subclass = cls
- super().__init_subclass__(**kwargs)
- cls._subclasses.append(cls)
-
- def __post_init__(self):
- if isinstance(self.converter, str):
- raise TypeError(
- "The converter parameter cannot be an instance of `str`. Use `from_string` method"
- )
-
- @property
- def is_multiplicative(self) -> bool:
- return self.converter.is_multiplicative
-
- @property
- def is_logarithmic(self) -> bool:
- return self.converter.is_logarithmic
-
- @classmethod
- def accept_to_parse(cls, preprocessed: PreprocessedDefinition):
- return False
+ """This is kept for backwards compatibility"""
@classmethod
- def from_string(
- cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
- ) -> Definition:
- """Parse a definition.
-
- Parameters
- ----------
- definition : str or PreprocessedDefinition
- non_int_type : type
-
- Returns
- -------
- Definition or subclass of Definition
- """
-
- if isinstance(definition, str):
- definition = PreprocessedDefinition.from_string(definition)
-
- for subclass in cls._subclasses:
- if subclass.accept_to_parse(definition):
- return subclass.from_string(definition, non_int_type)
-
- if cls._default_subclass is None:
- raise ValueError("No matching definition (and no default parser).")
-
- return cls._default_subclass.from_string(definition, non_int_type)
-
- @property
- def symbol(self) -> str:
- return self.defined_symbol or self.name
-
- @property
- def has_symbol(self) -> bool:
- return bool(self.defined_symbol)
-
- def add_aliases(self, *alias: str) -> None:
- raise Exception("Cannot add aliases, definitions are inmutable.")
-
- def __str__(self) -> str:
- return self.name
+ def from_string(cls, s: str, non_int_type=float):
+ cfg = ParserConfig(non_int_type)
+ parser = txt_defparser.DefParser(cfg, None)
+ pp = parser.parse_string(s)
+ for definition in parser.iter_parsed_project(pp):
+ if isinstance(definition, Exception):
+ raise errors.DefinitionSyntaxError(str(definition))
+ if not isinstance(definition, (fp.BOS, fp.BOF, fp.BOS)):
+ return definition
diff --git a/pint/delegates/__init__.py b/pint/delegates/__init__.py
new file mode 100644
index 0000000..363ef9c
--- /dev/null
+++ b/pint/delegates/__init__.py
@@ -0,0 +1,14 @@
+"""
+ pint.delegates
+ ~~~~~~~~~~~~~~
+
+ Defines methods and classes to handle autonomous tasks.
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from . import txt_defparser
+from .base_defparser import ParserConfig, build_disk_cache_class
+
+__all__ = [txt_defparser, ParserConfig, build_disk_cache_class]
diff --git a/pint/delegates/base_defparser.py b/pint/delegates/base_defparser.py
new file mode 100644
index 0000000..d35f3e3
--- /dev/null
+++ b/pint/delegates/base_defparser.py
@@ -0,0 +1,105 @@
+"""
+ pint.delegates.base_defparser
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Common class and function for all parsers.
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+import functools
+import itertools
+import numbers
+import pathlib
+import typing as ty
+from dataclasses import dataclass, field
+
+from pint import errors
+from pint.facets.plain.definitions import NotNumeric
+from pint.util import ParserHelper, UnitsContainer
+
+from .._vendor import flexcache as fc
+from .._vendor import flexparser as fp
+
+
+@dataclass(frozen=True)
+class ParserConfig:
+ """Configuration used by the parser."""
+
+ #: Indicates the output type of non integer numbers.
+ non_int_type: ty.Type[numbers.Number] = float
+
+ def to_scaled_units_container(self, s: str):
+ return ParserHelper.from_string(s, self.non_int_type)
+
+ def to_units_container(self, s: str):
+ v = self.to_scaled_units_container(s)
+ if v.scale != 1:
+ raise errors.UnexpectedScaleInContainer(str(v.scale))
+ return UnitsContainer(v)
+
+ def to_dimension_container(self, s: str):
+ v = self.to_units_container(s)
+ invalid = tuple(itertools.filterfalse(errors.is_valid_dimension_name, v.keys()))
+ if invalid:
+ raise errors.DefinitionSyntaxError(
+ f"Cannot build a dimension container with {', '.join(invalid)} that "
+ + errors.MSG_INVALID_DIMENSION_NAME
+ )
+ return v
+
+ def to_number(self, s: str) -> numbers.Number:
+ """Try parse a string into a number (without using eval).
+
+ The string can contain a number or a simple equation (3 + 4)
+
+ Raises
+ ------
+ _NotNumeric
+ If the string cannot be parsed as a number.
+ """
+ val = self.to_scaled_units_container(s)
+ if len(val):
+ raise NotNumeric(s)
+ return val.scale
+
+
+@functools.lru_cache()
+def build_disk_cache_class(non_int_type: type):
+ """Build disk cache class, taking into account the non_int_type."""
+
+ @dataclass(frozen=True)
+ class PintHeader(fc.InvalidateByExist, fc.NameByFields, fc.BasicPythonHeader):
+ from .. import __version__
+
+ pint_version: str = __version__
+ non_int_type: str = field(default_factory=lambda: non_int_type.__qualname__)
+
+ class PathHeader(fc.NameByFileContent, PintHeader):
+ pass
+
+ class ParsedProjecHeader(fc.NameByHashIter, PintHeader):
+ @classmethod
+ def from_parsed_project(cls, pp: fp.ParsedProject, reader_id):
+ tmp = []
+ for stmt in pp.iter_statements():
+ if isinstance(stmt, fp.BOS):
+ tmp.append(
+ stmt.content_hash.algorithm_name
+ + ":"
+ + stmt.content_hash.hexdigest
+ )
+
+ return cls(tuple(tmp), reader_id)
+
+ class PintDiskCache(fc.DiskCache):
+ _header_classes = {
+ pathlib.Path: PathHeader,
+ str: PathHeader.from_string,
+ fp.ParsedProject: ParsedProjecHeader.from_parsed_project,
+ }
+
+ return PintDiskCache
diff --git a/pint/delegates/txt_defparser/__init__.py b/pint/delegates/txt_defparser/__init__.py
new file mode 100644
index 0000000..5572ca1
--- /dev/null
+++ b/pint/delegates/txt_defparser/__init__.py
@@ -0,0 +1,14 @@
+"""
+ pint.delegates.txt_defparser
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Parser for the original textual Pint Definition file.
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+
+from .defparser import DefParser
+
+__all__ = [DefParser]
diff --git a/pint/delegates/txt_defparser/block.py b/pint/delegates/txt_defparser/block.py
new file mode 100644
index 0000000..20ebcba
--- /dev/null
+++ b/pint/delegates/txt_defparser/block.py
@@ -0,0 +1,45 @@
+"""
+ pint.delegates.txt_defparser.block
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Classes for Pint Blocks, which are defined by:
+
+ @<block name>
+ <content>
+ @end
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from ..._vendor import flexparser as fp
+
+
+@dataclass(frozen=True)
+class EndDirectiveBlock(fp.ParsedStatement):
+ """An EndDirectiveBlock is simply an "@end" statement."""
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[EndDirectiveBlock]:
+ if s == "@end":
+ return cls()
+ return None
+
+
+@dataclass(frozen=True)
+class DirectiveBlock(fp.Block):
+ """Directive blocks have beginning statement starting with a @ character.
+ and ending with a "@end" (captured using a EndDirectiveBlock).
+
+ Subclass this class for convenience.
+ """
+
+ closing: EndDirectiveBlock
+
+ def derive_definition(self):
+ pass
diff --git a/pint/delegates/txt_defparser/common.py b/pint/delegates/txt_defparser/common.py
new file mode 100644
index 0000000..493d0ec
--- /dev/null
+++ b/pint/delegates/txt_defparser/common.py
@@ -0,0 +1,57 @@
+"""
+ pint.delegates.txt_defparser.common
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Definitions for parsing an Import Statement
+
+ Also DefinitionSyntaxError
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+
+from ... import errors
+from ..._vendor import flexparser as fp
+
+
+@dataclass(frozen=True)
+class DefinitionSyntaxError(errors.DefinitionSyntaxError, fp.ParsingError):
+ """A syntax error was found in a definition. Combines:
+
+ DefinitionSyntaxError: which provides a message placeholder.
+ fp.ParsingError: which provides raw text, and start and end column and row
+
+ and an extra location attribute in which the filename or reseource is stored.
+ """
+
+ location: str = field(init=False, default="")
+
+ def __str__(self):
+ msg = (
+ self.msg + "\n " + (self.format_position or "") + " " + (self.raw or "")
+ )
+ if self.location:
+ msg += "\n " + self.location
+ return msg
+
+ def set_location(self, value):
+ super().__setattr__("location", value)
+
+
+@dataclass(frozen=True)
+class ImportDefinition(fp.IncludeStatement):
+ value: str
+
+ @property
+ def target(self):
+ return self.value
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[ImportDefinition]:
+ if s.startswith("@import"):
+ return ImportDefinition(s[len("@import") :].strip())
+ return None
diff --git a/pint/delegates/txt_defparser/context.py b/pint/delegates/txt_defparser/context.py
new file mode 100644
index 0000000..5c54b4c
--- /dev/null
+++ b/pint/delegates/txt_defparser/context.py
@@ -0,0 +1,196 @@
+"""
+ pint.delegates.txt_defparser.context
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Definitions for parsing Context and their related objects
+
+ Notices that some of the checks are done within the
+ format agnostic parent definition class.
+
+ See each one for a slighly longer description of the
+ syntax.
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+import numbers
+import re
+import typing as ty
+from dataclasses import dataclass
+from typing import Dict, Tuple
+
+from ..._vendor import flexparser as fp
+from ...facets.context import definitions
+from ..base_defparser import ParserConfig
+from . import block, common, plain
+
+
+@dataclass(frozen=True)
+class Relation(definitions.Relation):
+ @classmethod
+ def _from_string_and_context_sep(
+ cls, s: str, config: ParserConfig, separator: str
+ ) -> fp.FromString[Relation]:
+ if separator not in s:
+ return None
+ if ":" not in s:
+ return None
+
+ rel, eq = s.split(":")
+
+ parts = rel.split(separator)
+
+ src, dst = (config.to_dimension_container(s) for s in parts)
+
+ return cls(src, dst, eq.strip())
+
+
+@dataclass(frozen=True)
+class ForwardRelation(fp.ParsedStatement, definitions.ForwardRelation, Relation):
+ """A relation connecting a dimension to another via a transformation function.
+
+ <source dimension> -> <target dimension>: <transformation function>
+ """
+
+ @classmethod
+ def from_string_and_config(
+ cls, s: str, config: ParserConfig
+ ) -> fp.FromString[ForwardRelation]:
+ return super()._from_string_and_context_sep(s, config, "->")
+
+
+@dataclass(frozen=True)
+class BidirectionalRelation(
+ fp.ParsedStatement, definitions.BidirectionalRelation, Relation
+):
+ """A bidirectional relation connecting a dimension to another
+ via a simple transformation function.
+
+ <source dimension> <-> <target dimension>: <transformation function>
+
+ """
+
+ @classmethod
+ def from_string_and_config(
+ cls, s: str, config: ParserConfig
+ ) -> fp.FromString[BidirectionalRelation]:
+ return super()._from_string_and_context_sep(s, config, "<->")
+
+
+@dataclass(frozen=True)
+class BeginContext(fp.ParsedStatement):
+ """Being of a context directive.
+
+ @context[(defaults)] <canonical name> [= <alias>] [= <alias>]
+ """
+
+ _header_re = re.compile(
+ r"@context\s*(?P<defaults>\(.*\))?\s+(?P<name>\w+)\s*(=(?P<aliases>.*))*"
+ )
+
+ name: str
+ aliases: Tuple[str, ...]
+ defaults: Dict[str, numbers.Number]
+
+ @classmethod
+ def from_string_and_config(
+ cls, s: str, config: ParserConfig
+ ) -> fp.FromString[BeginContext]:
+ try:
+ r = cls._header_re.search(s)
+ if r is None:
+ return None
+ name = r.groupdict()["name"].strip()
+ aliases = r.groupdict()["aliases"]
+ if aliases:
+ aliases = tuple(a.strip() for a in r.groupdict()["aliases"].split("="))
+ else:
+ aliases = ()
+ defaults = r.groupdict()["defaults"]
+ except Exception as exc:
+ return common.DefinitionSyntaxError(
+ f"Could not parse the Context header '{s}': {exc}"
+ )
+
+ if defaults:
+ txt = defaults
+ try:
+ defaults = (part.split("=") for part in defaults.strip("()").split(","))
+ defaults = {str(k).strip(): config.to_number(v) for k, v in defaults}
+ except (ValueError, TypeError) as exc:
+ return common.DefinitionSyntaxError(
+ f"Could not parse Context definition defaults '{txt}' {exc}"
+ )
+ else:
+ defaults = {}
+
+ return cls(name, tuple(aliases), defaults)
+
+
+@dataclass(frozen=True)
+class ContextDefinition(block.DirectiveBlock):
+ """Definition of a Context
+
+ @context[(defaults)] <canonical name> [= <alias>] [= <alias>]
+ # units can be redefined within the context
+ <redefined unit> = <relation to another unit>
+
+ # can establish unidirectional relationships between dimensions
+ <dimension 1> -> <dimension 2>: <transformation function>
+
+ # can establish bidirectionl relationships between dimensions
+ <dimension 3> <-> <dimension 4>: <transformation function>
+ @end
+
+ See BeginContext, Equality, ForwardRelation, BidirectionalRelation and
+ Comment for more parsing related information.
+
+ Example::
+
+ @context(n=1) spectroscopy = sp
+ # n index of refraction of the medium.
+ [length] <-> [frequency]: speed_of_light / n / value
+ [frequency] -> [energy]: planck_constant * value
+ [energy] -> [frequency]: value / planck_constant
+ # allow wavenumber / kayser
+ [wavenumber] <-> [length]: 1 / value
+ @end
+ """
+
+ opening: fp.Single[BeginContext]
+ body: fp.Multi[
+ ty.Union[
+ plain.CommentDefinition,
+ BidirectionalRelation,
+ ForwardRelation,
+ plain.UnitDefinition,
+ ]
+ ]
+
+ def derive_definition(self):
+ return definitions.ContextDefinition(
+ self.name, self.aliases, self.defaults, self.relations, self.redefinitions
+ )
+
+ @property
+ def name(self):
+ return self.opening.name
+
+ @property
+ def aliases(self):
+ return self.opening.aliases
+
+ @property
+ def defaults(self):
+ return self.opening.defaults
+
+ @property
+ def relations(self):
+ return tuple(r for r in self.body if isinstance(r, Relation))
+
+ @property
+ def redefinitions(self):
+ return tuple(r for r in self.body if isinstance(r, plain.UnitDefinition))
diff --git a/pint/delegates/txt_defparser/defaults.py b/pint/delegates/txt_defparser/defaults.py
new file mode 100644
index 0000000..af6e31f
--- /dev/null
+++ b/pint/delegates/txt_defparser/defaults.py
@@ -0,0 +1,77 @@
+"""
+ pint.delegates.txt_defparser.defaults
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Definitions for parsing Default sections.
+
+ See each one for a slighly longer description of the
+ syntax.
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+import typing as ty
+from dataclasses import dataclass, fields
+
+from ..._vendor import flexparser as fp
+from ...facets.plain import definitions
+from . import block, plain
+
+
+@dataclass(frozen=True)
+class BeginDefaults(fp.ParsedStatement):
+ """Being of a defaults directive.
+
+ @defaults
+ """
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[BeginDefaults]:
+ if s.strip() == "@defaults":
+ return cls()
+ return None
+
+
+@dataclass(frozen=True)
+class DefaultsDefinition(block.DirectiveBlock):
+ """Directive to store values.
+
+ @defaults
+ system = mks
+ @end
+
+ See Equality and Comment for more parsing related information.
+ """
+
+ opening: fp.Single[BeginDefaults]
+ body: fp.Multi[
+ ty.Union[
+ plain.CommentDefinition,
+ plain.Equality,
+ ]
+ ]
+
+ @property
+ def _valid_fields(self):
+ return tuple(f.name for f in fields(definitions.DefaultsDefinition))
+
+ def derive_definition(self):
+ for definition in self.filter_by(plain.Equality):
+ if definition.lhs not in self._valid_fields:
+ raise ValueError(
+ f"`{definition.lhs}` is not a valid key "
+ f"for the default section. {self._valid_fields}"
+ )
+
+ return definitions.DefaultsDefinition(
+ *tuple(self.get_key(key) for key in self._valid_fields)
+ )
+
+ def get_key(self, key):
+ for stmt in self.body:
+ if isinstance(stmt, plain.Equality) and stmt.lhs == key:
+ return stmt.rhs
+ raise KeyError(key)
diff --git a/pint/delegates/txt_defparser/defparser.py b/pint/delegates/txt_defparser/defparser.py
new file mode 100644
index 0000000..0b99d6d
--- /dev/null
+++ b/pint/delegates/txt_defparser/defparser.py
@@ -0,0 +1,118 @@
+from __future__ import annotations
+
+import pathlib
+import typing as ty
+
+from ..._vendor import flexcache as fc
+from ..._vendor import flexparser as fp
+from .. import base_defparser
+from . import block, common, context, defaults, group, plain, system
+
+
+class PintRootBlock(fp.RootBlock):
+ body: fp.Multi[
+ ty.Union[
+ plain.CommentDefinition,
+ common.ImportDefinition,
+ context.ContextDefinition,
+ defaults.DefaultsDefinition,
+ system.SystemDefinition,
+ group.GroupDefinition,
+ plain.AliasDefinition,
+ plain.DerivedDimensionDefinition,
+ plain.DimensionDefinition,
+ plain.PrefixDefinition,
+ plain.UnitDefinition,
+ ]
+ ]
+
+
+class HashTuple(tuple):
+ pass
+
+
+class _PintParser(fp.Parser):
+ """Parser for the original Pint definition file, with cache."""
+
+ _delimiters = {
+ "#": (
+ fp.DelimiterInclude.SPLIT_BEFORE,
+ fp.DelimiterAction.CAPTURE_NEXT_TIL_EOL,
+ ),
+ **fp.SPLIT_EOL,
+ }
+ _root_block_class = PintRootBlock
+ _strip_spaces = True
+
+ _diskcache: fc.DiskCache
+
+ def __init__(self, config: base_defparser.ParserConfig, *args, **kwargs):
+ self._diskcache = kwargs.pop("diskcache", None)
+ super().__init__(config, *args, **kwargs)
+
+ def parse_file(self, path: pathlib.Path) -> fp.ParsedSource:
+ if self._diskcache is None:
+ return super().parse_file(path)
+ content, basename = self._diskcache.load(path, super().parse_file)
+ return content
+
+
+class DefParser:
+ skip_classes = (fp.BOF, fp.BOR, fp.BOS, fp.EOS, plain.CommentDefinition)
+
+ def __init__(self, default_config, diskcache):
+ self._default_config = default_config
+ self._diskcache = diskcache
+
+ def iter_parsed_project(self, parsed_project: fp.ParsedProject):
+ last_location = None
+ for stmt in parsed_project.iter_blocks():
+ if isinstance(stmt, fp.BOF):
+ last_location = str(stmt.path)
+ elif isinstance(stmt, fp.BOR):
+ last_location = (
+ f"[package: {stmt.package}, resource: {stmt.resource_name}]"
+ )
+
+ if isinstance(stmt, self.skip_classes):
+ continue
+
+ if isinstance(stmt, common.DefinitionSyntaxError):
+ stmt.set_location(last_location)
+ raise stmt
+ elif isinstance(stmt, block.DirectiveBlock):
+ for exc in stmt.errors:
+ exc = common.DefinitionSyntaxError(str(exc))
+ exc.set_position(*stmt.get_position())
+ exc.set_raw(
+ (stmt.opening.raw or "") + " [...] " + (stmt.closing.raw or "")
+ )
+ exc.set_location(last_location)
+ raise exc
+
+ try:
+ yield stmt.derive_definition()
+ except Exception as exc:
+ exc = common.DefinitionSyntaxError(str(exc))
+ exc.set_position(*stmt.get_position())
+ exc.set_raw(stmt.opening.raw + " [...] " + stmt.closing.raw)
+ exc.set_location(last_location)
+ raise exc
+ else:
+ yield stmt
+
+ def parse_file(self, filename: pathlib.Path, cfg=None):
+ return fp.parse(
+ filename,
+ _PintParser,
+ cfg or self._default_config,
+ diskcache=self._diskcache,
+ )
+
+ def parse_string(self, content: str, cfg=None):
+ return fp.parse_bytes(
+ content.encode("utf-8"),
+ _PintParser,
+ cfg or self._default_config,
+ diskcache=self._diskcache,
+ )
diff --git a/pint/delegates/txt_defparser/group.py b/pint/delegates/txt_defparser/group.py
new file mode 100644
index 0000000..5be42ac
--- /dev/null
+++ b/pint/delegates/txt_defparser/group.py
@@ -0,0 +1,106 @@
+"""
+ pint.delegates.txt_defparser.group
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Definitions for parsing Group and their related objects
+
+ Notices that some of the checks are done within the
+ format agnostic parent definition class.
+
+ See each one for a slighly longer description of the
+ syntax.
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+import re
+import typing as ty
+from dataclasses import dataclass
+
+from ..._vendor import flexparser as fp
+from ...facets.group import definitions
+from . import block, common, plain
+
+
+@dataclass(frozen=True)
+class BeginGroup(fp.ParsedStatement):
+ """Being of a group directive.
+
+ @group <name> [using <group 1>, ..., <group N>]
+ """
+
+ #: Regex to match the header parts of a definition.
+ _header_re = re.compile(r"@group\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*")
+
+ name: str
+ using_group_names: ty.Tuple[str, ...]
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[BeginGroup]:
+ if not s.startswith("@group"):
+ return None
+
+ r = cls._header_re.search(s)
+
+ if r is None:
+ return common.DefinitionSyntaxError(f"Invalid Group header syntax: '{s}'")
+
+ name = r.groupdict()["name"].strip()
+ groups = r.groupdict()["used_groups"]
+ if groups:
+ parent_group_names = tuple(a.strip() for a in groups.split(","))
+ else:
+ parent_group_names = ()
+
+ return cls(name, parent_group_names)
+
+
+@dataclass(frozen=True)
+class GroupDefinition(block.DirectiveBlock):
+ """Definition of a group.
+
+ @group <name> [using <group 1>, ..., <group N>]
+ <definition 1>
+ ...
+ <definition N>
+ @end
+
+ See UnitDefinition and Comment for more parsing related information.
+
+ Example::
+
+ @group AvoirdupoisUS using Avoirdupois
+ US_hundredweight = hundredweight = US_cwt
+ US_ton = ton
+ US_force_ton = force_ton = _ = US_ton_force
+ @end
+
+ """
+
+ opening: fp.Single[BeginGroup]
+ body: fp.Multi[
+ ty.Union[
+ plain.CommentDefinition,
+ plain.UnitDefinition,
+ ]
+ ]
+
+ def derive_definition(self):
+ return definitions.GroupDefinition(
+ self.name, self.using_group_names, self.definitions
+ )
+
+ @property
+ def name(self):
+ return self.opening.name
+
+ @property
+ def using_group_names(self):
+ return self.opening.using_group_names
+
+ @property
+ def definitions(self) -> ty.Tuple[plain.UnitDefinition, ...]:
+ return tuple(el for el in self.body if isinstance(el, plain.UnitDefinition))
diff --git a/pint/delegates/txt_defparser/plain.py b/pint/delegates/txt_defparser/plain.py
new file mode 100644
index 0000000..428df10
--- /dev/null
+++ b/pint/delegates/txt_defparser/plain.py
@@ -0,0 +1,283 @@
+"""
+ pint.delegates.txt_defparser.plain
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Definitions for parsing:
+ - Equality
+ - CommentDefinition
+ - PrefixDefinition
+ - UnitDefinition
+ - DimensionDefinition
+ - DerivedDimensionDefinition
+ - AliasDefinition
+
+ Notices that some of the checks are done within the
+ format agnostic parent definition class.
+
+ See each one for a slighly longer description of the
+ syntax.
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from ..._vendor import flexparser as fp
+from ...converters import Converter
+from ...facets.plain import definitions
+from ...util import UnitsContainer
+from ..base_defparser import ParserConfig
+from . import common
+
+
+@dataclass(frozen=True)
+class Equality(fp.ParsedStatement, definitions.Equality):
+ """An equality statement contains a left and right hand separated
+
+ lhs and rhs should be space stripped.
+ """
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[Equality]:
+ if "=" not in s:
+ return None
+ parts = [p.strip() for p in s.split("=")]
+ if len(parts) != 2:
+ return common.DefinitionSyntaxError(
+ f"Exactly two terms expected, not {len(parts)} (`{s}`)"
+ )
+ return cls(*parts)
+
+
+@dataclass(frozen=True)
+class CommentDefinition(fp.ParsedStatement, definitions.CommentDefinition):
+ """Comments start with a # character.
+
+ # This is a comment.
+ ## This is also a comment.
+
+ Captured value does not include the leading # character and space stripped.
+ """
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[fp.ParsedStatement]:
+ if not s.startswith("#"):
+ return None
+ return cls(s[1:].strip())
+
+
+@dataclass(frozen=True)
+class PrefixDefinition(fp.ParsedStatement, definitions.PrefixDefinition):
+ """Definition of a prefix::
+
+ <prefix>- = <value> [= <symbol>] [= <alias>] [ = <alias> ] [...]
+
+ Example::
+
+ deca- = 1e+1 = da- = deka-
+ """
+
+ @classmethod
+ def from_string_and_config(
+ cls, s: str, config: ParserConfig
+ ) -> fp.FromString[PrefixDefinition]:
+ if "=" not in s:
+ return None
+
+ name, value, *aliases = s.split("=")
+
+ name = name.strip()
+ if not name.endswith("-"):
+ return None
+
+ name = name.rstrip("-")
+ aliases = tuple(alias.strip().rstrip("-") for alias in aliases)
+
+ defined_symbol = None
+ if aliases:
+ if aliases[0] == "_":
+ aliases = aliases[1:]
+ else:
+ defined_symbol, *aliases = aliases
+
+ aliases = tuple(alias for alias in aliases if alias not in ("", "_"))
+
+ try:
+ value = config.to_number(value)
+ except definitions.NotNumeric as ex:
+ return common.DefinitionSyntaxError(
+ f"Prefix definition ('{name}') must contain only numbers, not {ex.value}"
+ )
+
+ try:
+ return cls(name, value, defined_symbol, aliases)
+ except Exception as exc:
+ return common.DefinitionSyntaxError(str(exc))
+
+
+@dataclass(frozen=True)
+class UnitDefinition(fp.ParsedStatement, definitions.UnitDefinition):
+ """Definition of a unit::
+
+ <canonical name> = <relation to another unit or dimension> [= <symbol>] [= <alias>] [ = <alias> ] [...]
+
+ Example::
+
+ millennium = 1e3 * year = _ = millennia
+
+ Parameters
+ ----------
+ reference : UnitsContainer
+ Reference units.
+ is_base : bool
+ Indicates if it is a base unit.
+
+ """
+
+ @classmethod
+ def from_string_and_config(
+ cls, s: str, config: ParserConfig
+ ) -> fp.FromString[UnitDefinition]:
+ if "=" not in s:
+ return None
+
+ name, value, *aliases = (p.strip() for p in s.split("="))
+
+ defined_symbol = None
+ if aliases:
+ if aliases[0] == "_":
+ aliases = aliases[1:]
+ else:
+ defined_symbol, *aliases = aliases
+
+ aliases = tuple(alias for alias in aliases if alias not in ("", "_"))
+
+ if ";" in value:
+ [converter, modifiers] = value.split(";", 1)
+
+ try:
+ modifiers = dict(
+ (key.strip(), config.to_number(value))
+ for key, value in (part.split(":") for part in modifiers.split(";"))
+ )
+ except definitions.NotNumeric as ex:
+ return common.DefinitionSyntaxError(
+ f"Unit definition ('{name}') must contain only numbers in modifier, not {ex.value}"
+ )
+
+ else:
+ converter = value
+ modifiers = {}
+
+ converter = config.to_scaled_units_container(converter)
+
+ try:
+ reference = UnitsContainer(converter)
+ # reference = converter.to_units_container()
+ except common.DefinitionSyntaxError as ex:
+ return common.DefinitionSyntaxError(f"While defining {name}: {ex}")
+
+ try:
+ converter = Converter.from_arguments(scale=converter.scale, **modifiers)
+ except Exception as ex:
+ return common.DefinitionSyntaxError(
+ f"Unable to assign a converter to the unit {ex}"
+ )
+
+ try:
+ return cls(name, defined_symbol, tuple(aliases), converter, reference)
+ except Exception as ex:
+ return common.DefinitionSyntaxError(str(ex))
+
+
+@dataclass(frozen=True)
+class DimensionDefinition(fp.ParsedStatement, definitions.DimensionDefinition):
+ """Definition of a root dimension::
+
+ [dimension name]
+
+ Example::
+
+ [volume]
+ """
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[DimensionDefinition]:
+ s = s.strip()
+
+ if not (s.startswith("[") and "=" not in s):
+ return None
+
+ try:
+ s = definitions.check_dim(s)
+ except common.DefinitionSyntaxError as ex:
+ return ex
+
+ return cls(s)
+
+
+@dataclass(frozen=True)
+class DerivedDimensionDefinition(
+ fp.ParsedStatement, definitions.DerivedDimensionDefinition
+):
+ """Definition of a derived dimension::
+
+ [dimension name] = <relation to other dimensions>
+
+ Example::
+
+ [density] = [mass] / [volume]
+ """
+
+ @classmethod
+ def from_string_and_config(
+ cls, s: str, config: ParserConfig
+ ) -> fp.FromString[DerivedDimensionDefinition]:
+ if not (s.startswith("[") and "=" in s):
+ return None
+
+ name, value, *aliases = s.split("=")
+
+ if aliases:
+ return common.DefinitionSyntaxError(
+ "Derived dimensions cannot have aliases."
+ )
+
+ try:
+ reference = config.to_dimension_container(value)
+ except common.DefinitionSyntaxError as exc:
+ return common.DefinitionSyntaxError(
+ f"In {name} derived dimensions must only be referenced "
+ f"to dimensions. {exc}"
+ )
+
+ try:
+ return cls(name.strip(), reference)
+ except Exception as exc:
+ return common.DefinitionSyntaxError(str(exc))
+
+
+@dataclass(frozen=True)
+class AliasDefinition(fp.ParsedStatement, definitions.AliasDefinition):
+ """Additional alias(es) for an already existing unit::
+
+ @alias <canonical name or previous alias> = <alias> [ = <alias> ] [...]
+
+ Example::
+
+ @alias meter = my_meter
+ """
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[AliasDefinition]:
+ if not s.startswith("@alias "):
+ return None
+ name, *aliases = s[len("@alias ") :].split("=")
+
+ try:
+ return cls(name.strip(), tuple(alias.strip() for alias in aliases))
+ except Exception as exc:
+ return common.DefinitionSyntaxError(str(exc))
diff --git a/pint/delegates/txt_defparser/system.py b/pint/delegates/txt_defparser/system.py
new file mode 100644
index 0000000..b21fd7a
--- /dev/null
+++ b/pint/delegates/txt_defparser/system.py
@@ -0,0 +1,110 @@
+"""
+ pint.delegates.txt_defparser.system
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+import re
+import typing as ty
+from dataclasses import dataclass
+
+from ..._vendor import flexparser as fp
+from ...facets.system import definitions
+from . import block, common, plain
+
+
+@dataclass(frozen=True)
+class BaseUnitRule(fp.ParsedStatement, definitions.BaseUnitRule):
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[BaseUnitRule]:
+ if ":" not in s:
+ return cls(s.strip())
+ parts = [p.strip() for p in s.split(":")]
+ if len(parts) != 2:
+ return common.DefinitionSyntaxError(
+ f"Exactly two terms expected for rule, not {len(parts)} (`{s}`)"
+ )
+ return cls(*parts)
+
+
+@dataclass(frozen=True)
+class BeginSystem(fp.ParsedStatement):
+ """Being of a system directive.
+
+ @system <name> [using <group 1>, ..., <group N>]
+ """
+
+ #: Regex to match the header parts of a context.
+ _header_re = re.compile(r"@system\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*")
+
+ name: str
+ using_group_names: ty.Tuple[str, ...]
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[BeginSystem]:
+ if not s.startswith("@system"):
+ return None
+
+ r = cls._header_re.search(s)
+
+ if r is None:
+ raise ValueError("Invalid System header syntax '%s'" % s)
+
+ name = r.groupdict()["name"].strip()
+ groups = r.groupdict()["used_groups"]
+
+ # If the systems has no group, it automatically uses the root group.
+ if groups:
+ group_names = tuple(a.strip() for a in groups.split(","))
+ else:
+ group_names = ("root",)
+
+ return cls(name, group_names)
+
+
+@dataclass(frozen=True)
+class SystemDefinition(block.DirectiveBlock):
+ """Definition of a System:
+
+ @system <name> [using <group 1>, ..., <group N>]
+ <rule 1>
+ ...
+ <rule N>
+ @end
+
+ See Rule and Comment for more parsing related information.
+
+ The syntax for the rule is:
+
+ new_unit_name : old_unit_name
+
+ where:
+ - old_unit_name: a root unit part which is going to be removed from the system.
+ - new_unit_name: a non root unit which is going to replace the old_unit.
+
+ If the new_unit_name and the old_unit_name, the later and the colon can be omitted.
+ """
+
+ opening: fp.Single[BeginSystem]
+ body: fp.Multi[ty.Union[plain.CommentDefinition, BaseUnitRule]]
+
+ def derive_definition(self):
+ return definitions.SystemDefinition(
+ self.name, self.using_group_names, self.rules
+ )
+
+ @property
+ def name(self):
+ return self.opening.name
+
+ @property
+ def using_group_names(self):
+ return self.opening.using_group_names
+
+ @property
+ def rules(self):
+ return tuple(el for el in self.body if isinstance(el, BaseUnitRule))
diff --git a/pint/errors.py b/pint/errors.py
index 22b55f0..8f849da 100644
--- a/pint/errors.py
+++ b/pint/errors.py
@@ -10,91 +10,151 @@
from __future__ import annotations
-OFFSET_ERROR_DOCS_HTML = "https://pint.readthedocs.io/en/latest/nonmult.html"
-LOG_ERROR_DOCS_HTML = "https://pint.readthedocs.io/en/latest/nonmult.html"
+import typing as ty
+from dataclasses import dataclass, fields
+OFFSET_ERROR_DOCS_HTML = "https://pint.readthedocs.io/en/stable/user/nonmult.html"
+LOG_ERROR_DOCS_HTML = "https://pint.readthedocs.io/en/stable/user/log_units.html"
-def _file_prefix(filename=None, lineno=None):
- if filename and lineno is not None:
- return f"While opening {filename}, in line {lineno}: "
- elif filename:
- return f"While opening {filename}: "
- elif lineno is not None:
- return f"In line {lineno}: "
- else:
- return ""
+MSG_INVALID_UNIT_NAME = "is not a valid unit name (must follow Python identifier rules)"
+MSG_INVALID_UNIT_SYMBOL = "is not a valid unit symbol (must not contain spaces)"
+MSG_INVALID_UNIT_ALIAS = "is not a valid unit alias (must not contain spaces)"
+MSG_INVALID_PREFIX_NAME = (
+ "is not a valid prefix name (must follow Python identifier rules)"
+)
+MSG_INVALID_PREFIX_SYMBOL = "is not a valid prefix symbol (must not contain spaces)"
+MSG_INVALID_PREFIX_ALIAS = "is not a valid prefix alias (must not contain spaces)"
+MSG_INVALID_DIMENSION_NAME = "is not a valid dimension name (must follow Python identifier rules and enclosed by square brackets)"
+MSG_INVALID_CONTEXT_NAME = (
+ "is not a valid context name (must follow Python identifier rules)"
+)
+MSG_INVALID_GROUP_NAME = "is not a valid group name (must not contain spaces)"
+MSG_INVALID_SYSTEM_NAME = (
+ "is not a valid system name (must follow Python identifier rules)"
+)
+
+
+def is_dim(name):
+ return name[0] == "[" and name[-1] == "]"
+
+
+def is_valid_prefix_name(name):
+ return str.isidentifier(name) or name == ""
+
+
+is_valid_unit_name = is_valid_system_name = is_valid_context_name = str.isidentifier
+
+
+def _no_space(name):
+ return name.strip() == name and " " not in name
+
+
+is_valid_group_name = _no_space
+
+is_valid_unit_alias = (
+ is_valid_prefix_alias
+) = is_valid_unit_symbol = is_valid_prefix_symbol = _no_space
+
+
+def is_valid_dimension_name(name):
+ return name == "[]" or (
+ len(name) > 1 and is_dim(name) and str.isidentifier(name[1:-1])
+ )
+
+
+class WithDefErr:
+ """Mixing class to make some classes more readable."""
+
+ def def_err(self, msg):
+ return DefinitionError(self.name, self.__class__.__name__, msg)
+
+
+@dataclass(frozen=False)
class PintError(Exception):
"""Base exception for all Pint errors."""
-class DefinitionSyntaxError(SyntaxError, PintError):
- """Raised when a textual definition has a syntax error."""
+@dataclass(frozen=False)
+class DefinitionError(ValueError, PintError):
+ """Raised when a definition is not properly constructed."""
- def __init__(self, msg, *, filename=None, lineno=None):
- super().__init__(msg)
- self.filename = filename
- self.lineno = lineno
+ name: str
+ definition_type: ty.Type
+ msg: str
def __str__(self):
- return _file_prefix(self.filename, self.lineno) + str(self.args[0])
+ msg = f"Cannot define '{self.name}' ({self.definition_type}): {self.msg}"
+ return msg
+
+ def __reduce__(self):
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
- @property
- def __dict__(self):
- # SyntaxError.filename and lineno are special fields that don't appear in
- # the __dict__. This messes up pickling and deepcopy, as well
- # as any other Python library that expects sane behaviour.
- return {"filename": self.filename, "lineno": self.lineno}
+
+@dataclass(frozen=False)
+class DefinitionSyntaxError(ValueError, PintError):
+ """Raised when a textual definition has a syntax error."""
+
+ msg: str
+
+ def __str__(self):
+ return self.msg
def __reduce__(self):
- return DefinitionSyntaxError, self.args, self.__dict__
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
+@dataclass(frozen=False)
class RedefinitionError(ValueError, PintError):
"""Raised when a unit or prefix is redefined."""
- def __init__(self, name, definition_type, *, filename=None, lineno=None):
- super().__init__(name, definition_type)
- self.filename = filename
- self.lineno = lineno
+ name: str
+ definition_type: ty.Type
def __str__(self):
- msg = f"Cannot redefine '{self.args[0]}' ({self.args[1]})"
- return _file_prefix(self.filename, self.lineno) + msg
+ msg = f"Cannot redefine '{self.name}' ({self.definition_type})"
+ return msg
def __reduce__(self):
- return RedefinitionError, self.args, self.__dict__
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
+@dataclass(frozen=False)
class UndefinedUnitError(AttributeError, PintError):
"""Raised when the units are not defined in the unit registry."""
- def __init__(self, *unit_names):
- if len(unit_names) == 1 and not isinstance(unit_names[0], str):
- unit_names = unit_names[0]
- super().__init__(*unit_names)
+ unit_names: ty.Union[str, ty.Tuple[str, ...]]
def __str__(self):
- if len(self.args) == 1:
- return f"'{self.args[0]}' is not defined in the unit registry"
- return f"{self.args} are not defined in the unit registry"
+ if isinstance(self.unit_names, str):
+ return f"'{self.unit_names}' is not defined in the unit registry"
+ if (
+ isinstance(self.unit_names, (tuple, list, set))
+ and len(self.unit_names) == 1
+ ):
+ return f"'{tuple(self.unit_names)[0]}' is not defined in the unit registry"
+ return f"{tuple(self.unit_names)} are not defined in the unit registry"
+ def __reduce__(self):
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
+
+@dataclass(frozen=False)
class PintTypeError(TypeError, PintError):
- pass
+ def __reduce__(self):
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
+@dataclass(frozen=False)
class DimensionalityError(PintTypeError):
"""Raised when trying to convert between incompatible units."""
- def __init__(self, units1, units2, dim1="", dim2="", *, extra_msg=""):
- super().__init__()
- self.units1 = units1
- self.units2 = units2
- self.dim1 = dim1
- self.dim2 = dim2
- self.extra_msg = extra_msg
+ units1: ty.Any
+ units2: ty.Any
+ dim1: str = ""
+ dim2: str = ""
+ extra_msg: str = ""
def __str__(self):
if self.dim1 or self.dim2:
@@ -110,34 +170,68 @@ class DimensionalityError(PintTypeError):
)
def __reduce__(self):
- return TypeError.__new__, (DimensionalityError,), self.__dict__
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
+@dataclass(frozen=False)
class OffsetUnitCalculusError(PintTypeError):
"""Raised on ambiguous operations with offset units."""
+ units1: ty.Any
+ units2: ty.Optional[ty.Any] = None
+
+ def yield_units(self):
+ yield self.units1
+ if self.units2:
+ yield self.units2
+
def __str__(self):
return (
"Ambiguous operation with offset unit (%s)."
- % ", ".join(str(u) for u in self.args)
+ % ", ".join(str(u) for u in self.yield_units())
+ " See "
+ OFFSET_ERROR_DOCS_HTML
+ " for guidance."
)
+ def __reduce__(self):
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
+
+@dataclass(frozen=False)
class LogarithmicUnitCalculusError(PintTypeError):
"""Raised on inappropriate operations with logarithmic units."""
+ units1: ty.Any
+ units2: ty.Optional[ty.Any] = None
+
+ def yield_units(self):
+ yield self.units1
+ if self.units2:
+ yield self.units2
+
def __str__(self):
return (
"Ambiguous operation with logarithmic unit (%s)."
- % ", ".join(str(u) for u in self.args)
+ % ", ".join(str(u) for u in self.yield_units())
+ " See "
+ LOG_ERROR_DOCS_HTML
+ " for guidance."
)
+ def __reduce__(self):
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
+
+@dataclass(frozen=False)
class UnitStrippedWarning(UserWarning, PintError):
- pass
+ msg: str
+
+ def __reduce__(self):
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
+
+
+@dataclass(frozen=False)
+class UnexpectedScaleInContainer(Exception):
+ def __reduce__(self):
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
diff --git a/pint/facets/__init__.py b/pint/facets/__init__.py
index a3f2439..d669b9f 100644
--- a/pint/facets/__init__.py
+++ b/pint/facets/__init__.py
@@ -7,8 +7,8 @@
keeping each part small enough to be hackable.
Each facet contains one or more of the following modules:
- - definitions: classes and functions to parse a string into an specific object.
- These objects must be immutable and pickable. (e.g. ContextDefinition)
+ - definitions: classes describing an specific unit related definiton.
+ These objects must be immutable, pickable and not reference the registry (e.g. ContextDefinition)
- objects: classes and functions that encapsulate behavior (e.g. Context)
- registry: implements a subclass of PlainRegistry or class that can be
mixed with it (e.g. ContextRegistry)
@@ -16,8 +16,7 @@
In certain cases, some of these modules might be collapsed into a single one
as the code is very short (like in dask) or expanded as the code is too long
(like in plain, where quantity and unit object are in their own module).
- Additionally, certain facets might not have one of them (e.g. dask adds no
- feature in relation to parsing).
+ Additionally, certain facets might not have one of them.
An important part of this scheme is that each facet should export only a few
classes in the __init__.py and everything else should not be accessed by any
diff --git a/pint/facets/context/__init__.py b/pint/facets/context/__init__.py
index 61685a2..db28436 100644
--- a/pint/facets/context/__init__.py
+++ b/pint/facets/context/__init__.py
@@ -15,4 +15,4 @@ from .definitions import ContextDefinition
from .objects import Context
from .registry import ContextRegistry
-__all__ = [ContextDefinition, Context, ContextRegistry]
+__all__ = ["ContextDefinition", "Context", "ContextRegistry"]
diff --git a/pint/facets/context/definitions.py b/pint/facets/context/definitions.py
index 96f1e90..fbdb390 100644
--- a/pint/facets/context/definitions.py
+++ b/pint/facets/context/definitions.py
@@ -8,182 +8,149 @@
from __future__ import annotations
+import itertools
import numbers
import re
from dataclasses import dataclass
-from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple
+from typing import TYPE_CHECKING, Any, Callable, Dict, Set, Tuple
-from ...definitions import Definition
-from ...errors import DefinitionSyntaxError
-from ...util import ParserHelper, SourceIterator
+from ... import errors
from ..plain import UnitDefinition
if TYPE_CHECKING:
- from pint import Quantity
+ from ..._typing import Quantity, UnitsContainer
-_header_re = re.compile(
- r"@context\s*(?P<defaults>\(.*\))?\s+(?P<name>\w+)\s*(=(?P<aliases>.*))*"
-)
-_varname_re = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
-# TODO: Put back annotation when possible
-# registry_cache: "UnitRegistry"
+@dataclass(frozen=True)
+class Relation:
+ """Base class for a relation between different dimensionalities."""
+
+ _varname_re = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
+
+ #: Source dimensionality
+ src: UnitsContainer
+ #: Destination dimensionality
+ dst: UnitsContainer
+ #: Equation connecting both dimensionalities from which the tranformation
+ #: will be built.
+ equation: str
+
+ # Instead of defining __post_init__ here,
+ # it will be added to the container class
+ # so that the name and a meaningfull class
+ # could be used.
+
+ @property
+ def variables(self) -> Set[str, ...]:
+ """Find all variables names in the equation."""
+ return set(self._varname_re.findall(self.equation))
+
+ @property
+ def transformation(self) -> Callable[..., Quantity[Any]]:
+ """Return a transformation callable that uses the registry
+ to parse the transformation equation.
+ """
+ return lambda ureg, value, **kwargs: ureg.parse_expression(
+ self.equation, value=value, **kwargs
+ )
+ @property
+ def bidirectional(self):
+ raise NotImplementedError
+
+
+@dataclass(frozen=True)
+class ForwardRelation(Relation):
+ """A relation connecting a dimension to another via a transformation function.
-class Expression:
- def __init__(self, eq):
- self._eq = eq
+ <source dimension> -> <target dimension>: <transformation function>
+ """
- def __call__(self, ureg, value: Any, **kwargs: Any):
- return ureg.parse_expression(self._eq, value=value, **kwargs)
+ @property
+ def bidirectional(self):
+ return False
@dataclass(frozen=True)
-class Relation:
+class BidirectionalRelation(Relation):
+ """A bidirectional relation connecting a dimension to another
+ via a simple transformation function.
+
+ <source dimension> <-> <target dimension>: <transformation function>
- bidirectional: True
- src: ParserHelper
- dst: ParserHelper
- tranformation: Callable[..., Quantity[Any]]
+ """
+
+ @property
+ def bidirectional(self):
+ return True
@dataclass(frozen=True)
-class ContextDefinition:
- """Definition of a Context
-
- @context[(defaults)] <canonical name> [= <alias>] [= <alias>]
- # units can be redefined within the context
- <redefined unit> = <relation to another unit>
-
- # can establish unidirectional relationships between dimensions
- <dimension 1> -> <dimension 2>: <transformation function>
-
- # can establish bidirectionl relationships between dimensions
- <dimension 3> <-> <dimension 4>: <transformation function>
- @end
-
- Example::
-
- @context(n=1) spectroscopy = sp
- # n index of refraction of the medium.
- [length] <-> [frequency]: speed_of_light / n / value
- [frequency] -> [energy]: planck_constant * value
- [energy] -> [frequency]: value / planck_constant
- # allow wavenumber / kayser
- [wavenumber] <-> [length]: 1 / value
- @end
- """
+class ContextDefinition(errors.WithDefErr):
+ """Definition of a Context"""
+ #: name of the context
name: str
+ #: other na
aliases: Tuple[str, ...]
- variables: Tuple[str, ...]
defaults: Dict[str, numbers.Number]
+ relations: Tuple[Relation, ...]
+ redefinitions: Tuple[UnitDefinition, ...]
- # Each element indicates: line number, is_bidirectional, src, dst, transformation func
- relations: Tuple[Tuple[int, Relation], ...]
- redefinitions: Tuple[Tuple[int, UnitDefinition], ...]
+ @property
+ def variables(self) -> Set[str, ...]:
+ """Return all variable names in all transformations."""
+ return set().union(*(r.variables for r in self.relations))
- @staticmethod
- def parse_definition(line, non_int_type) -> UnitDefinition:
- definition = Definition.from_string(line, non_int_type)
- if not isinstance(definition, UnitDefinition):
- raise DefinitionSyntaxError(
- "Expected <unit> = <converter>; got %s" % line.strip()
- )
- if definition.symbol != definition.name or definition.aliases:
- raise DefinitionSyntaxError(
- "Can't change a unit's symbol or aliases within a context"
+ @classmethod
+ def from_lines(cls, lines, non_int_type):
+ # TODO: this is to keep it backwards compatible
+ from ...delegates import ParserConfig, txt_defparser
+
+ cfg = ParserConfig(non_int_type)
+ parser = txt_defparser.DefParser(cfg, None)
+ pp = parser.parse_string("\n".join(lines) + "\n@end")
+ for definition in parser.iter_parsed_project(pp):
+ if isinstance(definition, cls):
+ return definition
+
+ def __post_init__(self):
+ if not errors.is_valid_context_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_GROUP_NAME)
+
+ for k in self.aliases:
+ if not errors.is_valid_context_name(k):
+ raise self.def_err(
+ f"refers to '{k}' that " + errors.MSG_INVALID_CONTEXT_NAME
+ )
+
+ for relation in self.relations:
+ invalid = tuple(
+ itertools.filterfalse(
+ errors.is_valid_dimension_name, relation.src.keys()
+ )
+ ) + tuple(
+ itertools.filterfalse(
+ errors.is_valid_dimension_name, relation.dst.keys()
+ )
)
- if definition.is_base:
- raise DefinitionSyntaxError("Can't define plain units within a context")
- return definition
- @classmethod
- def from_lines(cls, lines, non_int_type=float) -> ContextDefinition:
- lines = SourceIterator(lines)
-
- lineno, header = next(lines)
- try:
- r = _header_re.search(header)
- name = r.groupdict()["name"].strip()
- aliases = r.groupdict()["aliases"]
- if aliases:
- aliases = tuple(a.strip() for a in r.groupdict()["aliases"].split("="))
- else:
- aliases = ()
- defaults = r.groupdict()["defaults"]
- except Exception as exc:
- raise DefinitionSyntaxError(
- "Could not parse the Context header '%s'" % header, lineno=lineno
- ) from exc
-
- if defaults:
-
- def to_num(val):
- val = complex(val)
- if not val.imag:
- return val.real
- return val
-
- txt = defaults
- try:
- defaults = (part.split("=") for part in defaults.strip("()").split(","))
- defaults = {str(k).strip(): to_num(v) for k, v in defaults}
- except (ValueError, TypeError) as exc:
- raise DefinitionSyntaxError(
- f"Could not parse Context definition defaults: '{txt}'",
- lineno=lineno,
- ) from exc
- else:
- defaults = {}
-
- variables = set()
- redefitions = []
- relations = []
- for lineno, line in lines:
- try:
- if "=" in line:
- definition = cls.parse_definition(line, non_int_type)
- redefitions.append((lineno, definition))
- elif ":" in line:
- rel, eq = line.split(":")
- variables.update(_varname_re.findall(eq))
-
- func = Expression(eq)
-
- bidir = True
- parts = rel.split("<->")
- if len(parts) != 2:
- bidir = False
- parts = rel.split("->")
- if len(parts) != 2:
- raise Exception
-
- src, dst = (
- ParserHelper.from_string(s, non_int_type) for s in parts
- )
- relation = Relation(bidir, src, dst, func)
- relations.append((lineno, relation))
- else:
- raise Exception
- except Exception as exc:
- raise DefinitionSyntaxError(
- "Could not parse Context %s relation '%s': %s" % (name, line, exc),
- lineno=lineno,
- ) from exc
-
- if defaults:
- missing_pars = defaults.keys() - set(variables)
- if missing_pars:
- raise DefinitionSyntaxError(
- f"Context parameters {missing_pars} not found in any equation"
+ if invalid:
+ raise self.def_err(
+ f"relation refers to {', '.join(invalid)} that "
+ + errors.MSG_INVALID_DIMENSION_NAME
)
- return cls(
- name,
- aliases,
- tuple(variables),
- defaults,
- tuple(relations),
- tuple(redefitions),
- )
+ for definition in self.redefinitions:
+ if definition.symbol != definition.name or definition.aliases:
+ raise self.def_err(
+ "can't change a unit's symbol or aliases within a context"
+ )
+ if definition.is_base:
+ raise self.def_err("can't define plain units within a context")
+
+ missing_pars = set(self.defaults.keys()) - self.variables
+ if missing_pars:
+ raise self.def_err(
+ f"Context parameters {missing_pars} not found in any equation"
+ )
diff --git a/pint/facets/context/objects.py b/pint/facets/context/objects.py
index 0aca430..40c2bb5 100644
--- a/pint/facets/context/objects.py
+++ b/pint/facets/context/objects.py
@@ -12,7 +12,6 @@ import weakref
from collections import ChainMap, defaultdict
from typing import Optional, Tuple
-from ...errors import DefinitionSyntaxError, RedefinitionError
from ...facets.plain import UnitDefinition
from ...util import UnitsContainer, to_units_container
from .definitions import ContextDefinition
@@ -75,7 +74,6 @@ class Context:
aliases: Tuple[str, ...] = (),
defaults: Optional[dict] = None,
) -> None:
-
self.name = name
self.aliases = aliases
@@ -133,29 +131,22 @@ class Context:
def from_definition(cls, cd: ContextDefinition, to_base_func=None) -> Context:
ctx = cls(cd.name, cd.aliases, cd.defaults)
- for lineno, definition in cd.redefinitions:
- try:
- ctx._redefine(definition)
- except (RedefinitionError, DefinitionSyntaxError) as ex:
- if ex.lineno is None:
- ex.lineno = lineno
- raise ex
+ for definition in cd.redefinitions:
+ ctx._redefine(definition)
- for lineno, relation in cd.relations:
+ for relation in cd.relations:
try:
if to_base_func:
src = to_base_func(relation.src)
dst = to_base_func(relation.dst)
else:
src, dst = relation.src, relation.dst
- ctx.add_transformation(src, dst, relation.tranformation)
+ ctx.add_transformation(src, dst, relation.transformation)
if relation.bidirectional:
- ctx.add_transformation(dst, src, relation.tranformation)
+ ctx.add_transformation(dst, src, relation.transformation)
except Exception as exc:
- raise DefinitionSyntaxError(
- "Could not add Context %s relation on line '%s'"
- % (cd.name, lineno),
- lineno=lineno,
+ raise ValueError(
+ f"Could not add Context {cd.name} relation {relation}"
) from exc
return ctx
@@ -192,11 +183,16 @@ class Context:
definition : str
<unit> = <new definition>``, e.g. ``pound = 0.5 kg``
"""
-
- for line in definition.splitlines():
- # TODO: What is the right non_int_type value.
- definition = ContextDefinition.parse_definition(line, float)
- self._redefine(definition)
+ from ...delegates import ParserConfig, txt_defparser
+
+ # TODO: kept for backwards compatibility.
+ # this is not a good idea as we have no way of known the correct non_int_type
+ cfg = ParserConfig(float)
+ parser = txt_defparser.DefParser(cfg, None)
+ pp = parser.parse_string(definition)
+ for definition in parser.iter_parsed_project(pp):
+ if isinstance(definition, UnitDefinition):
+ self._redefine(definition)
def _redefine(self, definition: UnitDefinition):
self.redefinitions.append(definition)
diff --git a/pint/facets/context/registry.py b/pint/facets/context/registry.py
index 5483554..ccf69d2 100644
--- a/pint/facets/context/registry.py
+++ b/pint/facets/context/registry.py
@@ -14,7 +14,7 @@ from contextlib import contextmanager
from typing import Any, Callable, ContextManager, Dict, Union
from ..._typing import F
-from ...errors import DefinitionSyntaxError, UndefinedUnitError
+from ...errors import UndefinedUnitError
from ...util import find_connected_nodes, find_shortest_path, logger
from ..plain import PlainRegistry, UnitDefinition
from .definitions import ContextDefinition
@@ -67,17 +67,11 @@ class ContextRegistry(PlainRegistry):
# Allow contexts to add override layers to the units
self._units = ChainMap(self._units)
- def _register_directives(self) -> None:
- super()._register_directives()
- self._register_directive("@context", self._load_context, ContextDefinition)
+ def _register_definition_adders(self) -> None:
+ super()._register_definition_adders()
+ self._register_adder(ContextDefinition, self.add_context)
- def _load_context(self, cd: ContextDefinition) -> None:
- try:
- self.add_context(Context.from_definition(cd, self.get_dimensionality))
- except KeyError as e:
- raise DefinitionSyntaxError(f"unknown dimension {e} in context")
-
- def add_context(self, context: Context) -> None:
+ def add_context(self, context: Union[Context, ContextDefinition]) -> None:
"""Add a context object to the registry.
The context will be accessible by its name and aliases.
@@ -85,6 +79,9 @@ class ContextRegistry(PlainRegistry):
Notice that this method will NOT enable the context;
see :meth:`enable_contexts`.
"""
+ if isinstance(context, ContextDefinition):
+ context = Context.from_definition(context, self.get_dimensionality)
+
if not context.name:
raise ValueError("Can't add unnamed context to registry")
if context.name in self._contexts:
@@ -189,7 +186,6 @@ class ContextRegistry(PlainRegistry):
name=basedef.name,
defined_symbol=basedef.symbol,
aliases=basedef.aliases,
- is_base=False,
reference=definition.reference,
converter=definition.converter,
)
@@ -259,48 +255,47 @@ class ContextRegistry(PlainRegistry):
@contextmanager
def context(self, *names, **kwargs) -> ContextManager[Context]:
"""Used as a context manager, this function enables to activate a context
- which is removed after usage.
+ which is removed after usage.
- Parameters
- ----------
- *names :
- name(s) of the context(s).
- **kwargs :
- keyword arguments for the contexts.
+ Parameters
+ ----------
+ *names : name(s) of the context(s).
+ **kwargs : keyword arguments for the contexts.
- Examples
- --------
- Context can be called by their name:
+ Examples
+ --------
+ Context can be called by their name:
- import pint.facets.context.objects >>> import pint
- >>> ureg = pint.UnitRegistry()
- >>> ureg.add_context(pint.facets.context.objects.Context('one'))
- >>> ureg.add_context(pint.facets.context.objects.Context('two'))
- >>> with ureg.context('one'):
- ... pass
+ >>> import pint.facets.context.objects
+ >>> import pint
+ >>> ureg = pint.UnitRegistry()
+ >>> ureg.add_context(pint.facets.context.objects.Context('one'))
+ >>> ureg.add_context(pint.facets.context.objects.Context('two'))
+ >>> with ureg.context('one'):
+ ... pass
- If a context has an argument, you can specify its value as a keyword argument:
+ If a context has an argument, you can specify its value as a keyword argument:
- >>> with ureg.context('one', n=1):
- ... pass
+ >>> with ureg.context('one', n=1):
+ ... pass
- Multiple contexts can be entered in single call:
+ Multiple contexts can be entered in single call:
- >>> with ureg.context('one', 'two', n=1):
- ... pass
+ >>> with ureg.context('one', 'two', n=1):
+ ... pass
- Or nested allowing you to give different values to the same keyword argument:
+ Or nested allowing you to give different values to the same keyword argument:
- >>> with ureg.context('one', n=1):
- ... with ureg.context('two', n=2):
- ... pass
+ >>> with ureg.context('one', n=1):
+ ... with ureg.context('two', n=2):
+ ... pass
- A nested context inherits the defaults from the containing context:
+ A nested context inherits the defaults from the containing context:
- >>> with ureg.context('one', n=1):
- ... # Here n takes the value of the outer context
- ... with ureg.context('two'):
- ... pass
+ >>> with ureg.context('one', n=1):
+ ... # Here n takes the value of the outer context
+ ... with ureg.context('two'):
+ ... pass
"""
# Enable the contexts.
self.enable_contexts(*names, **kwargs)
@@ -318,7 +313,7 @@ class ContextRegistry(PlainRegistry):
"""Decorator to wrap a function call in a Pint context.
Use it to ensure that a certain context is active when
- calling a function::
+ calling a function.
Parameters
----------
@@ -330,14 +325,13 @@ class ContextRegistry(PlainRegistry):
Returns
-------
- callable
- the wrapped function.
+ callable: the wrapped function.
- Example
- -------
- >>> @ureg.with_context('sp')
- ... def my_cool_fun(wavelength):
- ... print('This wavelength is equivalent to: %s', wavelength.to('terahertz'))
+ Examples
+ --------
+ >>> @ureg.with_context('sp')
+ ... def my_cool_fun(wavelength):
+ ... print('This wavelength is equivalent to: %s', wavelength.to('terahertz'))
"""
def decorator(func):
@@ -384,7 +378,6 @@ class ContextRegistry(PlainRegistry):
# destination dimensionality. If it exists, we transform the source value
# by applying sequentially each transformation of the path.
if self._active_ctx:
-
src_dim = self._get_dimensionality(src)
dst_dim = self._get_dimensionality(dst)
diff --git a/pint/facets/dask/__init__.py b/pint/facets/dask/__init__.py
index 46fb38a..42fced0 100644
--- a/pint/facets/dask/__init__.py
+++ b/pint/facets/dask/__init__.py
@@ -1,6 +1,6 @@
"""
- pint.facets.numpy
- ~~~~~~~~~~~~~~~~~
+ pint.facets.dask
+ ~~~~~~~~~~~~~~~~
Adds pint the capability to interoperate with Dask
@@ -32,7 +32,6 @@ def check_dask_array(f):
class DaskQuantity:
-
# Dask.array.Array ducking
def __dask_graph__(self):
if isinstance(self._magnitude, dask_array.Array):
@@ -46,10 +45,7 @@ class DaskQuantity:
def __dask_tokenize__(self):
from dask.base import tokenize
- from pint import UnitRegistry
-
- # TODO: Check if this is the right class as first argument
- return (UnitRegistry.Quantity, tokenize(self._magnitude), self.units)
+ return (type(self), tokenize(self._magnitude), self.units)
@property
def __dask_optimize__(self):
@@ -67,14 +63,9 @@ class DaskQuantity:
func, args = self._magnitude.__dask_postpersist__()
return self._dask_finalize, (func, args, self.units)
- @staticmethod
- def _dask_finalize(results, func, args, units):
+ def _dask_finalize(self, results, func, args, units):
values = func(results, *args)
-
- from pint import Quantity
-
- # TODO: Check if this is the right class as first argument
- return Quantity(values, units)
+ return type(self)(values, units)
@check_dask_array
def compute(self, **kwargs):
@@ -129,5 +120,4 @@ class DaskQuantity:
class DaskRegistry(PlainRegistry):
-
_quantity_class = DaskQuantity
diff --git a/pint/facets/formatting/__init__.py b/pint/facets/formatting/__init__.py
index f9c8c82..e3f4381 100644
--- a/pint/facets/formatting/__init__.py
+++ b/pint/facets/formatting/__init__.py
@@ -13,4 +13,4 @@ from __future__ import annotations
from .objects import FormattingQuantity, FormattingUnit
from .registry import FormattingRegistry
-__all__ = [FormattingQuantity, FormattingUnit, FormattingRegistry]
+__all__ = ["FormattingQuantity", "FormattingUnit", "FormattingRegistry"]
diff --git a/pint/facets/formatting/objects.py b/pint/facets/formatting/objects.py
index a32b41a..1ba92c9 100644
--- a/pint/facets/formatting/objects.py
+++ b/pint/facets/formatting/objects.py
@@ -21,12 +21,10 @@ from ...formatting import (
siunitx_format_unit,
split_format,
)
-from ...util import iterable
-from ..plain import UnitsContainer
+from ...util import UnitsContainer, iterable
class FormattingQuantity:
-
_exp_pattern = re.compile(r"([0-9]\.?[0-9]*)e(-?)\+?0*([0-9]+)")
def __format__(self, spec: str) -> str:
diff --git a/pint/facets/formatting/registry.py b/pint/facets/formatting/registry.py
index 246cc43..bd9c74c 100644
--- a/pint/facets/formatting/registry.py
+++ b/pint/facets/formatting/registry.py
@@ -13,6 +13,5 @@ from .objects import FormattingQuantity, FormattingUnit
class FormattingRegistry(PlainRegistry):
-
_quantity_class = FormattingQuantity
_unit_class = FormattingUnit
diff --git a/pint/facets/group/__init__.py b/pint/facets/group/__init__.py
index d9d1606..e1fad04 100644
--- a/pint/facets/group/__init__.py
+++ b/pint/facets/group/__init__.py
@@ -14,4 +14,4 @@ from .definitions import GroupDefinition
from .objects import Group
from .registry import GroupRegistry
-__all__ = [GroupDefinition, Group, GroupRegistry]
+__all__ = ["GroupDefinition", "Group", "GroupRegistry"]
diff --git a/pint/facets/group/definitions.py b/pint/facets/group/definitions.py
index d2fa3f2..c0abced 100644
--- a/pint/facets/group/definitions.py
+++ b/pint/facets/group/definitions.py
@@ -1,6 +1,6 @@
"""
- pint.facets.group.defintions
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ pint.facets.group.definitions
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:copyright: 2022 by Pint Authors, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
@@ -8,88 +8,46 @@
from __future__ import annotations
-import re
+import typing as ty
from dataclasses import dataclass
-from typing import Tuple
-from ...definitions import Definition
-from ...errors import DefinitionSyntaxError
-from ...util import SourceIterator
-from ..plain import UnitDefinition
+from ... import errors
+from .. import plain
@dataclass(frozen=True)
-class GroupDefinition:
- """Definition of a group.
-
- @group <name> [using <group 1>, ..., <group N>]
- <definition 1>
- ...
- <definition N>
- @end
-
- Example::
-
- @group AvoirdupoisUS using Avoirdupois
- US_hundredweight = hundredweight = US_cwt
- US_ton = ton
- US_force_ton = force_ton = _ = US_ton_force
- @end
-
- """
-
- #: Regex to match the header parts of a definition.
- _header_re = re.compile(r"@group\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*")
+class GroupDefinition(errors.WithDefErr):
+ """Definition of a group."""
+ #: name of the group
name: str
- units: Tuple[Tuple[int, UnitDefinition], ...]
- using_group_names: Tuple[str, ...]
-
- @property
- def unit_names(self) -> Tuple[str, ...]:
- return tuple(u.name for lineno, u in self.units)
+ #: unit groups that will be included within the group
+ using_group_names: ty.Tuple[str, ...]
+ #: definitions for the units existing within the group
+ definitions: ty.Tuple[plain.UnitDefinition, ...]
@classmethod
- def from_lines(cls, lines, non_int_type=float):
- """Return a Group object parsing an iterable of lines.
+ def from_lines(cls, lines, non_int_type):
+ # TODO: this is to keep it backwards compatible
+ from ...delegates import ParserConfig, txt_defparser
- Parameters
- ----------
- lines : list[str]
- iterable
- define_func : callable
- Function to define a unit in the registry; it must accept a single string as
- a parameter.
+ cfg = ParserConfig(non_int_type)
+ parser = txt_defparser.DefParser(cfg, None)
+ pp = parser.parse_string("\n".join(lines) + "\n@end")
+ for definition in parser.iter_parsed_project(pp):
+ if isinstance(definition, cls):
+ return definition
- Returns
- -------
-
- """
-
- lines = SourceIterator(lines)
- lineno, header = next(lines)
-
- r = cls._header_re.search(header)
-
- if r is None:
- raise ValueError("Invalid Group header syntax: '%s'" % header)
+ @property
+ def unit_names(self) -> ty.Tuple[str, ...]:
+ return tuple(el.name for el in self.definitions)
- name = r.groupdict()["name"].strip()
- groups = r.groupdict()["used_groups"]
- if groups:
- parent_group_names = tuple(a.strip() for a in groups.split(","))
- else:
- parent_group_names = ()
+ def __post_init__(self):
+ if not errors.is_valid_group_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_GROUP_NAME)
- units = []
- for lineno, line in lines:
- definition = Definition.from_string(line, non_int_type=non_int_type)
- if not isinstance(definition, UnitDefinition):
- raise DefinitionSyntaxError(
- "Only UnitDefinition are valid inside _used_groups, not "
- + str(definition),
- lineno=lineno,
+ for k in self.using_group_names:
+ if not errors.is_valid_group_name(k):
+ raise self.def_err(
+ f"refers to '{k}' that " + errors.MSG_INVALID_GROUP_NAME
)
- units.append((lineno, definition))
-
- return cls(name, tuple(units), parent_group_names)
diff --git a/pint/facets/group/objects.py b/pint/facets/group/objects.py
index 4ff775c..558a107 100644
--- a/pint/facets/group/objects.py
+++ b/pint/facets/group/objects.py
@@ -8,7 +8,6 @@
from __future__ import annotations
-from ...errors import DefinitionSyntaxError, RedefinitionError
from ...util import SharedRegistryObject, getattr_maybe_raise
from .definitions import GroupDefinition
@@ -124,7 +123,6 @@ class Group(SharedRegistryObject):
"""Add groups to group."""
d = self._REGISTRY._groups
for group_name in group_names:
-
grp = d[group_name]
if grp.is_used_group(self.name):
@@ -169,19 +167,24 @@ class Group(SharedRegistryObject):
return cls.from_definition(group_definition, define_func)
@classmethod
- def from_definition(cls, group_definition: GroupDefinition, define_func) -> Group:
- for lineno, definition in group_definition.units:
- try:
- define_func(definition)
- except (RedefinitionError, DefinitionSyntaxError) as ex:
- if ex.lineno is None:
- ex.lineno = lineno
- raise ex
-
+ def from_definition(
+ cls, group_definition: GroupDefinition, add_unit_func=None
+ ) -> Group:
grp = cls(group_definition.name)
- grp.add_units(*(unit.name for lineno, unit in group_definition.units))
+ add_unit_func = add_unit_func or grp._REGISTRY._add_unit
+
+ # We first add all units defined within the group
+ # to the registry.
+ for definition in group_definition.definitions:
+ add_unit_func(definition)
+
+ # Then we add all units defined within the group
+ # to this group (by name)
+ grp.add_units(*group_definition.unit_names)
+ # Finally, we add all grou0ps used by this group
+ # tho this group (by name)
if group_definition.using_group_names:
grp.add_groups(*group_definition.using_group_names)
diff --git a/pint/facets/group/registry.py b/pint/facets/group/registry.py
index f8da191..7269082 100644
--- a/pint/facets/group/registry.py
+++ b/pint/facets/group/registry.py
@@ -10,8 +10,10 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Dict, FrozenSet
+from ... import errors
+
if TYPE_CHECKING:
- from pint import Unit
+ from ..._typing import Unit
from ...util import build_dependent_class, create_class_with_registry
from ..plain import PlainRegistry, UnitDefinition
@@ -29,6 +31,9 @@ class GroupRegistry(PlainRegistry):
- Parse @group directive.
"""
+ # TODO: Change this to Group: Group to specify class
+ # and use introspection to get system class as a way
+ # to enjoy typing goodies
_group_class = Group
def __init__(self, **kwargs):
@@ -69,13 +74,24 @@ class GroupRegistry(PlainRegistry):
all_units = self.get_group("root", False).members
grp.add_units(*(all_units - group_units))
- def _register_directives(self) -> None:
- super()._register_directives()
- self._register_directive(
- "@group",
- lambda gd: self.Group.from_definition(gd, self.define),
- GroupDefinition,
- )
+ def _register_definition_adders(self) -> None:
+ super()._register_definition_adders()
+ self._register_adder(GroupDefinition, self._add_group)
+
+ def _add_unit(self, definition: UnitDefinition):
+ super()._add_unit(definition)
+ # TODO: delta units are missing
+ self.get_group("root").add_units(definition.name)
+
+ def _add_group(self, gd: GroupDefinition):
+ if gd.name in self._groups:
+ raise ValueError(f"Group {gd.name} already present in registry")
+ try:
+ # As a Group is a SharedRegistryObject
+ # it adds itself to the registry.
+ self.Group.from_definition(gd)
+ except KeyError as e:
+ raise errors.DefinitionSyntaxError(f"unknown dimension {e} in context")
def get_group(self, name: str, create_if_needed: bool = True) -> Group:
"""Return a Group.
@@ -101,21 +117,7 @@ class GroupRegistry(PlainRegistry):
return self.Group(name)
- def _define(self, definition):
-
- # In addition to the what is done by the PlainRegistry,
- # this adds all units to the `root` group.
-
- definition, d, di = super()._define(definition)
-
- if isinstance(definition, UnitDefinition):
- # We add all units to the root group
- self.get_group("root").add_units(definition.name)
-
- return definition, d, di
-
def _get_compatible_units(self, input_units, group) -> FrozenSet["Unit"]:
-
ret = super()._get_compatible_units(input_units, group)
if not group:
diff --git a/pint/facets/measurement/__init__.py b/pint/facets/measurement/__init__.py
index 83454dc..21539dc 100644
--- a/pint/facets/measurement/__init__.py
+++ b/pint/facets/measurement/__init__.py
@@ -13,4 +13,4 @@ from __future__ import annotations
from .objects import Measurement, MeasurementQuantity
from .registry import MeasurementRegistry
-__all__ = [Measurement, MeasurementQuantity, MeasurementRegistry]
+__all__ = ["Measurement", "MeasurementQuantity", "MeasurementRegistry"]
diff --git a/pint/facets/measurement/objects.py b/pint/facets/measurement/objects.py
index 88fad0a..0fed93f 100644
--- a/pint/facets/measurement/objects.py
+++ b/pint/facets/measurement/objects.py
@@ -19,7 +19,6 @@ MISSING = object()
class MeasurementQuantity:
-
# Measurement support
def plus_minus(self, error, relative=False):
if isinstance(error, self.__class__):
@@ -102,7 +101,6 @@ class Measurement(PlainQuantity):
return "{}".format(self)
def __format__(self, spec):
-
spec = spec or self.default_format
# special cases
diff --git a/pint/facets/measurement/registry.py b/pint/facets/measurement/registry.py
index 9f051d7..e704399 100644
--- a/pint/facets/measurement/registry.py
+++ b/pint/facets/measurement/registry.py
@@ -1,6 +1,6 @@
"""
- pint.facets.measurement.objects
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ pint.facets.measurement.registry
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:copyright: 2022 by Pint Authors, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
@@ -16,7 +16,6 @@ from .objects import Measurement, MeasurementQuantity
class MeasurementRegistry(PlainRegistry):
-
_quantity_class = MeasurementQuantity
_measurement_class = Measurement
diff --git a/pint/facets/nonmultiplicative/__init__.py b/pint/facets/nonmultiplicative/__init__.py
index 56b8710..cbba410 100644
--- a/pint/facets/nonmultiplicative/__init__.py
+++ b/pint/facets/nonmultiplicative/__init__.py
@@ -18,5 +18,5 @@ from .definitions import LogarithmicConverter, OffsetConverter # noqa: F401
from .registry import NonMultiplicativeRegistry
__all__ = [
- NonMultiplicativeRegistry,
+ "NonMultiplicativeRegistry",
]
diff --git a/pint/facets/nonmultiplicative/registry.py b/pint/facets/nonmultiplicative/registry.py
index ae6b1b0..17b053e 100644
--- a/pint/facets/nonmultiplicative/registry.py
+++ b/pint/facets/nonmultiplicative/registry.py
@@ -8,12 +8,12 @@
from __future__ import annotations
-from typing import Any, Optional, Union
+from typing import Any, Optional
-from ...definitions import Definition
from ...errors import DimensionalityError, UndefinedUnitError
-from ...util import UnitsContainer
-from ..plain import PlainRegistry
+from ...util import UnitsContainer, logger
+from ..plain import PlainRegistry, UnitDefinition
+from .definitions import OffsetConverter, ScaleConverter
from .objects import NonMultiplicativeQuantity
@@ -65,31 +65,44 @@ class NonMultiplicativeRegistry(PlainRegistry):
return super()._parse_units(input_string, as_delta, case_sensitive)
- def _define(self, definition: Union[str, Definition]):
- """Add unit to the registry.
+ def _add_unit(self, definition: UnitDefinition):
+ super()._add_unit(definition)
- In addition to what is done by the PlainRegistry,
- registers also non-multiplicative units.
-
- Parameters
- ----------
- definition : str or Definition
- A dimension, unit or prefix definition.
-
- Returns
- -------
- Definition, dict, dict
- Definition instance, case sensitive unit dict, case insensitive unit dict.
-
- """
+ if definition.is_multiplicative:
+ return
- definition, d, di = super()._define(definition)
+ if definition.is_logarithmic:
+ return
- # define additional units for units with an offset
- if getattr(definition.converter, "offset", 0) != 0:
- self._define_adder(definition, d, di)
-
- return definition, d, di
+ if not isinstance(definition.converter, OffsetConverter):
+ logger.debug(
+ "Cannot autogenerate delta version for a unit in "
+ "which the converter is not an OffsetConverter"
+ )
+ return
+
+ delta_name = "delta_" + definition.name
+ if definition.symbol:
+ delta_symbol = "Δ" + definition.symbol
+ else:
+ delta_symbol = None
+
+ delta_aliases = tuple("Δ" + alias for alias in definition.aliases) + tuple(
+ "delta_" + alias for alias in definition.aliases
+ )
+
+ delta_reference = self.UnitsContainer(
+ {ref: value for ref, value in definition.reference.items()}
+ )
+
+ delta_def = UnitDefinition(
+ delta_name,
+ delta_symbol,
+ delta_aliases,
+ ScaleConverter(definition.converter.scale),
+ delta_reference,
+ )
+ super()._add_unit(delta_def)
def _is_multiplicative(self, u) -> bool:
if u in self._units:
@@ -135,7 +148,6 @@ class NonMultiplicativeRegistry(PlainRegistry):
return None
def _add_ref_of_log_or_offset_unit(self, offset_unit, all_units):
-
slct_unit = self._units[offset_unit]
if slct_unit.is_logarithmic or (not slct_unit.is_multiplicative):
# Extract reference unit
diff --git a/pint/facets/numpy/__init__.py b/pint/facets/numpy/__init__.py
index 9e0d4d5..aad9508 100644
--- a/pint/facets/numpy/__init__.py
+++ b/pint/facets/numpy/__init__.py
@@ -12,4 +12,4 @@ from __future__ import annotations
from .registry import NumpyRegistry
-__all__ = [NumpyRegistry]
+__all__ = ["NumpyRegistry"]
diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py
index 7143143..2a4421c 100644
--- a/pint/facets/numpy/numpy_func.py
+++ b/pint/facets/numpy/numpy_func.py
@@ -77,6 +77,8 @@ def convert_arg(arg, pre_calc_units):
Helper function for convert_to_consistent_units. pre_calc_units must be given as a
pint Unit or None.
"""
+ if isinstance(arg, bool):
+ return arg
if pre_calc_units is not None:
if _is_quantity(arg):
return arg.m_as(pre_calc_units)
@@ -101,7 +103,7 @@ def convert_to_consistent_units(*args, pre_calc_units=None, **kwargs):
If pre_calc_units is not None, takes the args and kwargs for a NumPy function and
converts any Quantity or Sequence of Quantities into the units of the first
- Quantity/Sequence of Quantities and returns the magnitudes. Other args/kwargs are
+ Quantity/Sequence of Quantities and returns the magnitudes. Other args/kwargs (except booleans) are
treated as dimensionless Quantities. If pre_calc_units is None, units are simply
stripped.
"""
@@ -419,6 +421,7 @@ matching_input_copy_units_output_ufuncs = [
"nextafter",
"trunc",
"absolute",
+ "positive",
"negative",
"maximum",
"minimum",
@@ -524,22 +527,16 @@ def _meshgrid(*xi, **kwargs):
@implements("full_like", "function")
-def _full_like(a, fill_value, dtype=None, order="K", subok=True, shape=None):
+def _full_like(a, fill_value, **kwargs):
# Make full_like by multiplying with array from ones_like in a
# non-multiplicative-unit-safe way
if hasattr(fill_value, "_REGISTRY"):
return fill_value._REGISTRY.Quantity(
- (
- np.ones_like(a, dtype=dtype, order=order, subok=subok, shape=shape)
- * fill_value.m
- ),
+ np.ones_like(a, **kwargs) * fill_value.m,
fill_value.units,
)
else:
- return (
- np.ones_like(a, dtype=dtype, order=order, subok=subok, shape=shape)
- * fill_value
- )
+ return np.ones_like(a, **kwargs) * fill_value
@implements("interp", "function")
@@ -552,6 +549,12 @@ def _interp(x, xp, fp, left=None, right=None, period=None):
@implements("where", "function")
def _where(condition, *args):
+ if not getattr(condition, "_is_multiplicative", True):
+ raise ValueError(
+ "Invalid units of the condition: Boolean value of Quantity with offset unit is ambiguous."
+ )
+
+ condition = getattr(condition, "magnitude", condition)
args, output_wrap = unwrap_and_wrap_consistent_units(*args)
return output_wrap(np.where(condition, *args))
@@ -787,6 +790,7 @@ for func_str, unit_arguments, wrap_output in [
("ptp", "a", True),
("ravel", "a", True),
("round_", "a", True),
+ ("round", "a", True),
("sort", "a", True),
("median", "a", True),
("nanmedian", "a", True),
@@ -807,8 +811,10 @@ for func_str, unit_arguments, wrap_output in [
("broadcast_to", ["array"], True),
("amax", ["a", "initial"], True),
("amin", ["a", "initial"], True),
+ ("max", ["a", "initial"], True),
+ ("min", ["a", "initial"], True),
("searchsorted", ["a", "v"], False),
- ("isclose", ["a", "b"], False),
+ ("isclose", ["a", "b", "atol"], False),
("nan_to_num", ["x", "nan", "posinf", "neginf"], True),
("clip", ["a", "a_min", "a_max"], True),
("append", ["arr", "values"], True),
@@ -818,9 +824,10 @@ for func_str, unit_arguments, wrap_output in [
("lib.stride_tricks.sliding_window_view", "x", True),
("rot90", "m", True),
("insert", ["arr", "values"], True),
+ ("delete", ["arr"], True),
("resize", "a", True),
("reshape", "a", True),
- ("allclose", ["a", "b"], False),
+ ("allclose", ["a", "b", "atol"], False),
("intersect1d", ["ar1", "ar2"], True),
]:
implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output)
@@ -877,7 +884,14 @@ for func_str in ["cumprod", "cumproduct", "nancumprod"]:
implement_single_dimensionless_argument_func(func_str)
# Handle single-argument consistent unit functions
-for func_str in ["block", "hstack", "vstack", "dstack", "column_stack"]:
+for func_str in [
+ "block",
+ "hstack",
+ "vstack",
+ "dstack",
+ "column_stack",
+ "broadcast_arrays",
+]:
implement_func(
"function", func_str, input_units="all_consistent", output_unit="match_input"
)
@@ -894,7 +908,6 @@ for func_str in [
"argsort",
"argmin",
"argmax",
- "alen",
"ndim",
"nanargmax",
"nanargmin",
diff --git a/pint/facets/numpy/quantity.py b/pint/facets/numpy/quantity.py
index 2436100..0d335cd 100644
--- a/pint/facets/numpy/quantity.py
+++ b/pint/facets/numpy/quantity.py
@@ -111,7 +111,6 @@ class NumpyQuantity:
return _to_magnitude(self._magnitude, force_ndarray=True)
def clip(self, min=None, max=None, out=None, **kwargs):
-
if min is not None:
if isinstance(min, self.__class__):
min = min.to(self).magnitude
diff --git a/pint/facets/numpy/registry.py b/pint/facets/numpy/registry.py
index 8ae6088..fa4768f 100644
--- a/pint/facets/numpy/registry.py
+++ b/pint/facets/numpy/registry.py
@@ -15,6 +15,5 @@ from .unit import NumpyUnit
class NumpyRegistry(PlainRegistry):
-
_quantity_class = NumpyQuantity
_unit_class = NumpyUnit
diff --git a/pint/facets/numpy/unit.py b/pint/facets/numpy/unit.py
index fc94853..0b5007f 100644
--- a/pint/facets/numpy/unit.py
+++ b/pint/facets/numpy/unit.py
@@ -12,7 +12,6 @@ from ...compat import is_upcast_type
class NumpyUnit:
-
__array_priority__ = 17
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
diff --git a/pint/facets/plain/__init__.py b/pint/facets/plain/__init__.py
index 5066932..211d017 100644
--- a/pint/facets/plain/__init__.py
+++ b/pint/facets/plain/__init__.py
@@ -1,6 +1,6 @@
"""
pint.facets.plain
- ~~~~~~~~~~~~~~~~
+ ~~~~~~~~~~~~~~~~~
Base implementation for registry, units and quantities.
@@ -18,18 +18,17 @@ from .definitions import (
ScaleConverter,
UnitDefinition,
)
-from .objects import PlainQuantity, PlainUnit, UnitsContainer
+from .objects import PlainQuantity, PlainUnit
from .registry import PlainRegistry
__all__ = [
- PlainUnit,
- PlainQuantity,
- PlainRegistry,
- AliasDefinition,
- DefaultsDefinition,
- DimensionDefinition,
- PrefixDefinition,
- ScaleConverter,
- UnitDefinition,
- UnitsContainer,
+ "PlainUnit",
+ "PlainQuantity",
+ "PlainRegistry",
+ "AliasDefinition",
+ "DefaultsDefinition",
+ "DimensionDefinition",
+ "PrefixDefinition",
+ "ScaleConverter",
+ "UnitDefinition",
]
diff --git a/pint/facets/plain/definitions.py b/pint/facets/plain/definitions.py
index c1d1d9a..11a3095 100644
--- a/pint/facets/plain/definitions.py
+++ b/pint/facets/plain/definitions.py
@@ -8,259 +8,267 @@
from __future__ import annotations
+import itertools
+import numbers
+import typing as ty
from dataclasses import dataclass
-from typing import Iterable, Optional, Tuple, Union
+from functools import cached_property
+from typing import Callable, Optional
+from ... import errors
from ...converters import Converter
-from ...definitions import Definition, PreprocessedDefinition
-from ...errors import DefinitionSyntaxError
-from ...util import ParserHelper, SourceIterator, UnitsContainer, _is_dim
+from ...util import UnitsContainer
-class _NotNumeric(Exception):
+class NotNumeric(Exception):
"""Internal exception. Do not expose outside Pint"""
def __init__(self, value):
self.value = value
-def numeric_parse(s: str, non_int_type: type = float):
- """Try parse a string into a number (without using eval).
-
- Parameters
- ----------
- s : str
- non_int_type : type
-
- Returns
- -------
- Number
-
- Raises
- ------
- _NotNumeric
- If the string cannot be parsed as a number.
- """
- ph = ParserHelper.from_string(s, non_int_type)
-
- if len(ph):
- raise _NotNumeric(s)
-
- return ph.scale
+########################
+# Convenience functions
+########################
@dataclass(frozen=True)
-class PrefixDefinition(Definition):
- """Definition of a prefix::
-
- <prefix>- = <amount> [= <symbol>] [= <alias>] [ = <alias> ] [...]
+class Equality:
+ """An equality statement contains a left and right hand separated
+ by and equal (=) sign.
- Example::
+ lhs = rhs
- deca- = 1e+1 = da- = deka-
+ lhs and rhs are space stripped.
"""
- @classmethod
- def accept_to_parse(cls, preprocessed: PreprocessedDefinition):
- return preprocessed.name.endswith("-")
+ lhs: str
+ rhs: str
- @classmethod
- def from_string(
- cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
- ) -> PrefixDefinition:
- if isinstance(definition, str):
- definition = PreprocessedDefinition.from_string(definition)
- aliases = tuple(alias.strip("-") for alias in definition.aliases)
- if definition.symbol:
- symbol = definition.symbol.strip("-")
- else:
- symbol = definition.symbol
-
- try:
- converter = ScaleConverter(numeric_parse(definition.value, non_int_type))
- except _NotNumeric as ex:
- raise ValueError(
- f"Prefix definition ('{definition.name}') must contain only numbers, not {ex.value}"
- )
+@dataclass(frozen=True)
+class CommentDefinition:
+ """A comment"""
- return cls(definition.name.rstrip("-"), symbol, aliases, converter)
+ comment: str
@dataclass(frozen=True)
-class UnitDefinition(Definition, default=True):
- """Definition of a unit::
-
- <canonical name> = <relation to another unit or dimension> [= <symbol>] [= <alias>] [ = <alias> ] [...]
+class DefaultsDefinition:
+ """Directive to store default values."""
- Example::
+ group: ty.Optional[str]
+ system: ty.Optional[str]
- millennium = 1e3 * year = _ = millennia
+ def items(self):
+ if self.group is not None:
+ yield "group", self.group
+ if self.system is not None:
+ yield "system", self.system
- Parameters
- ----------
- reference : UnitsContainer
- Reference units.
- is_base : bool
- Indicates if it is a plain unit.
- """
+@dataclass(frozen=True)
+class PrefixDefinition(errors.WithDefErr):
+ """Definition of a prefix."""
+
+ #: name of the prefix
+ name: str
+ #: scaling value for this prefix
+ value: numbers.Number
+ #: canonical symbol
+ defined_symbol: Optional[str] = ""
+ #: additional names for the same prefix
+ aliases: ty.Tuple[str, ...] = ()
+
+ @property
+ def symbol(self) -> str:
+ return self.defined_symbol or self.name
+
+ @property
+ def has_symbol(self) -> bool:
+ return bool(self.defined_symbol)
+
+ @cached_property
+ def converter(self):
+ return Converter.from_arguments(scale=self.value)
+
+ def __post_init__(self):
+ if not errors.is_valid_prefix_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_PREFIX_NAME)
+
+ if self.defined_symbol and not errors.is_valid_prefix_symbol(self.name):
+ raise self.def_err(
+ f"the symbol {self.defined_symbol} " + errors.MSG_INVALID_PREFIX_SYMBOL
+ )
- reference: Optional[UnitsContainer] = None
- is_base: bool = False
+ for alias in self.aliases:
+ if not errors.is_valid_prefix_alias(alias):
+ raise self.def_err(
+ f"the alias {alias} " + errors.MSG_INVALID_PREFIX_ALIAS
+ )
- @classmethod
- def from_string(
- cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
- ) -> "UnitDefinition":
- if isinstance(definition, str):
- definition = PreprocessedDefinition.from_string(definition)
- if ";" in definition.value:
- [converter, modifiers] = definition.value.split(";", 1)
+@dataclass(frozen=True)
+class UnitDefinition(errors.WithDefErr):
+ """Definition of a unit."""
+
+ #: canonical name of the unit
+ name: str
+ #: canonical symbol
+ defined_symbol: ty.Optional[str]
+ #: additional names for the same unit
+ aliases: ty.Tuple[str, ...]
+ #: A functiont that converts a value in these units into the reference units
+ converter: ty.Optional[ty.Union[Callable, Converter]]
+ #: Reference units.
+ reference: ty.Optional[UnitsContainer]
+
+ def __post_init__(self):
+ if not errors.is_valid_unit_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_UNIT_NAME)
+
+ if not any(map(errors.is_dim, self.reference.keys())):
+ invalid = tuple(
+ itertools.filterfalse(errors.is_valid_unit_name, self.reference.keys())
+ )
+ if invalid:
+ raise self.def_err(
+ f"refers to {', '.join(invalid)} that "
+ + errors.MSG_INVALID_UNIT_NAME
+ )
+ is_base = False
- try:
- modifiers = dict(
- (key.strip(), numeric_parse(value, non_int_type))
- for key, value in (part.split(":") for part in modifiers.split(";"))
+ elif all(map(errors.is_dim, self.reference.keys())):
+ invalid = tuple(
+ itertools.filterfalse(
+ errors.is_valid_dimension_name, self.reference.keys()
)
- except _NotNumeric as ex:
- raise ValueError(
- f"Unit definition ('{definition.name}') must contain only numbers in modifier, not {ex.value}"
+ )
+ if invalid:
+ raise self.def_err(
+ f"refers to {', '.join(invalid)} that "
+ + errors.MSG_INVALID_DIMENSION_NAME
)
- else:
- converter = definition.value
- modifiers = {}
-
- converter = ParserHelper.from_string(converter, non_int_type)
-
- if not any(_is_dim(key) for key in converter.keys()):
- is_base = False
- elif all(_is_dim(key) for key in converter.keys()):
is_base = True
+ scale = getattr(self.converter, "scale", 1)
+ if scale != 1:
+ return self.def_err(
+ "Base unit definitions cannot have a scale different to 1. "
+ f"(`{scale}` found)"
+ )
else:
- raise DefinitionSyntaxError(
+ raise self.def_err(
"Cannot mix dimensions and units in the same definition. "
"Base units must be referenced only to dimensions. "
"Derived units must be referenced only to units."
)
- reference = UnitsContainer(converter)
-
- try:
- converter = Converter.from_arguments(scale=converter.scale, **modifiers)
- except Exception as ex:
- raise DefinitionSyntaxError(
- "Unable to assign a converter to the unit"
- ) from ex
-
- return cls(
- definition.name,
- definition.symbol,
- definition.aliases,
- converter,
- reference,
- is_base,
- )
+ super.__setattr__(self, "_is_base", is_base)
-@dataclass(frozen=True)
-class DimensionDefinition(Definition):
- """Definition of a dimension::
-
- [dimension name] = <relation to other dimensions>
+ if self.defined_symbol and not errors.is_valid_unit_symbol(self.name):
+ raise self.def_err(
+ f"the symbol {self.defined_symbol} " + errors.MSG_INVALID_UNIT_SYMBOL
+ )
- Example::
+ for alias in self.aliases:
+ if not errors.is_valid_unit_alias(alias):
+ raise self.def_err(
+ f"the alias {alias} " + errors.MSG_INVALID_UNIT_ALIAS
+ )
- [density] = [mass] / [volume]
- """
+ @property
+ def is_base(self) -> bool:
+ """Indicates if it is a base unit."""
+ return self._is_base
- reference: Optional[UnitsContainer] = None
- is_base: bool = False
+ @property
+ def is_multiplicative(self) -> bool:
+ return self.converter.is_multiplicative
- @classmethod
- def accept_to_parse(cls, preprocessed: PreprocessedDefinition):
- return preprocessed.name.startswith("[")
+ @property
+ def is_logarithmic(self) -> bool:
+ return self.converter.is_logarithmic
- @classmethod
- def from_string(
- cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
- ) -> DimensionDefinition:
- if isinstance(definition, str):
- definition = PreprocessedDefinition.from_string(definition)
+ @property
+ def symbol(self) -> str:
+ return self.defined_symbol or self.name
- converter = ParserHelper.from_string(definition.value, non_int_type)
+ @property
+ def has_symbol(self) -> bool:
+ return bool(self.defined_symbol)
- if not converter:
- is_base = True
- elif all(_is_dim(key) for key in converter.keys()):
- is_base = False
- else:
- raise DefinitionSyntaxError(
- "Base dimensions must be referenced to None. "
- "Derived dimensions must only be referenced "
- "to dimensions."
- )
- reference = UnitsContainer(converter, non_int_type=non_int_type)
-
- return cls(
- definition.name,
- definition.symbol,
- definition.aliases,
- converter,
- reference,
- is_base,
- )
+@dataclass(frozen=True)
+class DimensionDefinition(errors.WithDefErr):
+ """Definition of a root dimension"""
-class AliasDefinition(Definition):
- """Additional alias(es) for an already existing unit::
+ #: name of the dimension
+ name: str
- @alias <canonical name or previous alias> = <alias> [ = <alias> ] [...]
+ @property
+ def is_base(self):
+ return True
- Example::
+ def __post_init__(self):
+ if not errors.is_valid_dimension_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_DIMENSION_NAME)
- @alias meter = my_meter
- """
- def __init__(self, name: str, aliases: Iterable[str]) -> None:
- super().__init__(
- name=name, defined_symbol=None, aliases=aliases, converter=None
- )
+@dataclass(frozen=True)
+class DerivedDimensionDefinition(DimensionDefinition):
+ """Definition of a derived dimension."""
- @classmethod
- def from_string(
- cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
- ) -> AliasDefinition:
+ #: reference dimensions.
+ reference: UnitsContainer
- if isinstance(definition, str):
- definition = PreprocessedDefinition.from_string(definition)
+ @property
+ def is_base(self):
+ return False
- name = definition.name[len("@alias ") :].lstrip()
- return AliasDefinition(name, tuple(definition.rhs_parts))
+ def __post_init__(self):
+ if not errors.is_valid_dimension_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_DIMENSION_NAME)
+ if not all(map(errors.is_dim, self.reference.keys())):
+ return self.def_err(
+ "derived dimensions must only reference other dimensions"
+ )
-@dataclass(frozen=True)
-class DefaultsDefinition:
- """Definition for the @default directive"""
+ invalid = tuple(
+ itertools.filterfalse(errors.is_valid_dimension_name, self.reference.keys())
+ )
- content: Tuple[Tuple[str, str], ...]
+ if invalid:
+ raise self.def_err(
+ f"refers to {', '.join(invalid)} that "
+ + errors.MSG_INVALID_DIMENSION_NAME
+ )
- @classmethod
- def from_lines(cls, lines, non_int_type=float) -> DefaultsDefinition:
- source_iterator = SourceIterator(lines)
- next(source_iterator)
- out = []
- for lineno, part in source_iterator:
- k, v = part.split("=")
- out.append((k.strip(), v.strip()))
- return DefaultsDefinition(tuple(out))
+@dataclass(frozen=True)
+class AliasDefinition(errors.WithDefErr):
+ """Additional alias(es) for an already existing unit."""
+
+ #: name of the already existing unit
+ name: str
+ #: aditional names for the same unit
+ aliases: ty.Tuple[str, ...]
+
+ def __post_init__(self):
+ if not errors.is_valid_unit_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_UNIT_NAME)
+
+ for alias in self.aliases:
+ if not errors.is_valid_unit_alias(alias):
+ raise self.def_err(
+ f"the alias {alias} " + errors.MSG_INVALID_UNIT_ALIAS
+ )
@dataclass(frozen=True)
class ScaleConverter(Converter):
- """A linear transformation."""
+ """A linear transformation without offset."""
scale: float
diff --git a/pint/facets/plain/quantity.py b/pint/facets/plain/quantity.py
index 343c3b8..f4608c7 100644
--- a/pint/facets/plain/quantity.py
+++ b/pint/facets/plain/quantity.py
@@ -62,7 +62,7 @@ from .definitions import UnitDefinition
if TYPE_CHECKING:
from ..context import Context
- from .unit import Unit
+ from .unit import PlainUnit as Unit
from .unit import UnitsContainer as UnitsContainerT
if HAS_NUMPY:
@@ -150,6 +150,12 @@ class PlainQuantity(PrettyIPython, SharedRegistryObject, Generic[_MagnitudeType]
_magnitude: _MagnitudeType
@property
+ def ndim(self) -> int:
+ if isinstance(self.magnitude, numbers.Number):
+ return 0
+ return self.magnitude.ndim
+
+ @property
def force_ndarray(self) -> bool:
return self._REGISTRY.force_ndarray
@@ -624,9 +630,9 @@ class PlainQuantity(PrettyIPython, SharedRegistryObject, Generic[_MagnitudeType]
>>> import pint
>>> ureg = pint.UnitRegistry()
>>> (200e-9*ureg.s).to_compact()
- <PlainQuantity(200.0, 'nanosecond')>
+ <Quantity(200.0, 'nanosecond')>
>>> (1e-2*ureg('kg m/s^2')).to_compact('N')
- <PlainQuantity(10.0, 'millinewton')>
+ <Quantity(10.0, 'millinewton')>
"""
if not isinstance(self.magnitude, numbers.Number):
@@ -1226,7 +1232,6 @@ class PlainQuantity(PrettyIPython, SharedRegistryObject, Generic[_MagnitudeType]
no_offset_units_self = len(offset_units_self)
if not self._check(other):
-
if not self._ok_for_muldiv(no_offset_units_self):
raise OffsetUnitCalculusError(self._units, getattr(other, "units", ""))
if len(offset_units_self) == 1:
@@ -1296,7 +1301,6 @@ class PlainQuantity(PrettyIPython, SharedRegistryObject, Generic[_MagnitudeType]
no_offset_units_self = len(offset_units_self)
if not self._check(other):
-
if not self._ok_for_muldiv(no_offset_units_self):
raise OffsetUnitCalculusError(self._units, getattr(other, "units", ""))
if len(offset_units_self) == 1:
diff --git a/pint/facets/plain/registry.py b/pint/facets/plain/registry.py
index 7f2c9a7..94cc5ff 100644
--- a/pint/facets/plain/registry.py
+++ b/pint/facets/plain/registry.py
@@ -10,12 +10,12 @@ from __future__ import annotations
import copy
import functools
+import inspect
import itertools
import locale
import pathlib
import re
from collections import defaultdict
-from dataclasses import dataclass
from decimal import Decimal
from fractions import Fraction
from numbers import Number
@@ -39,21 +39,14 @@ from typing import (
if TYPE_CHECKING:
from ..context import Context
- from pint import Quantity, Unit
+ from ..._typing import Quantity, Unit
-from ... import parser
from ..._typing import QuantityOrUnitLike, UnitLike
from ..._vendor import appdirs
from ...compat import HAS_BABEL, babel_parse, tokenizer
-from ...definitions import Definition
-from ...errors import (
- DefinitionSyntaxError,
- DimensionalityError,
- RedefinitionError,
- UndefinedUnitError,
-)
+from ...errors import DimensionalityError, RedefinitionError, UndefinedUnitError
from ...pint_eval import build_eval_tree
-from ...util import ParserHelper, SourceIterator
+from ...util import ParserHelper
from ...util import UnitsContainer
from ...util import UnitsContainer as UnitsContainerT
from ...util import (
@@ -68,15 +61,16 @@ from ...util import (
)
from .definitions import (
AliasDefinition,
+ CommentDefinition,
+ DefaultsDefinition,
+ DerivedDimensionDefinition,
DimensionDefinition,
PrefixDefinition,
- ScaleConverter,
UnitDefinition,
)
from .objects import PlainQuantity, PlainUnit
if TYPE_CHECKING:
-
if HAS_BABEL:
import babel
@@ -89,24 +83,6 @@ T = TypeVar("T")
_BLOCK_RE = re.compile(r"[ (]")
-@dataclass(frozen=True)
-class DefaultsDefinition:
- """Definition for the @default directive"""
-
- content: Tuple[Tuple[str, str], ...]
-
- @classmethod
- def from_lines(cls, lines, non_int_type=float) -> DefaultsDefinition:
- source_iterator = SourceIterator(lines)
- next(source_iterator)
- out = []
- for lineno, part in source_iterator:
- k, v = part.split("=")
- out.append((k.strip(), v.strip()))
-
- return DefaultsDefinition(tuple(out))
-
-
@functools.lru_cache()
def pattern_to_regex(pattern):
if hasattr(pattern, "finditer"):
@@ -212,6 +188,8 @@ class PlainRegistry(metaclass=RegistryMeta):
_quantity_class = PlainQuantity
_unit_class = PlainUnit
+ _def_parser = None
+
def __init__(
self,
filename="",
@@ -225,23 +203,34 @@ class PlainRegistry(metaclass=RegistryMeta):
case_sensitive: bool = True,
cache_folder: Union[str, pathlib.Path, None] = None,
separate_format_defaults: Optional[bool] = None,
+ mpl_formatter: str = "{:P}",
):
- #: Map context prefix to (loader function, parser function, single_line)
- #: type: Dict[str, Tuple[Callable[[Any], None]], Any]
- self._directives = {}
- self._register_directives()
+ #: Map a definition class to a adder methods.
+ self._adders = dict()
+ self._register_definition_adders()
self._init_dynamic_classes()
if cache_folder == ":auto:":
cache_folder = appdirs.user_cache_dir(appname="pint", appauthor=False)
+ cache_folder = pathlib.Path(cache_folder)
+
+ from ... import delegates # TODO: change thiss
if cache_folder is not None:
- self._diskcache = parser.build_disk_cache_class(non_int_type)(cache_folder)
+ self._diskcache = delegates.build_disk_cache_class(non_int_type)(
+ cache_folder
+ )
+
+ self._def_parser = delegates.txt_defparser.DefParser(
+ delegates.ParserConfig(non_int_type), diskcache=self._diskcache
+ )
self._filename = filename
self.force_ndarray = force_ndarray
self.force_ndarray_like = force_ndarray_like
self.preprocessors = preprocessors or []
+ # use a default preprocessor to support "%"
+ self.preprocessors.insert(0, lambda string: string.replace("%", " percent "))
#: mode used to fill in the format defaults
self.separate_format_defaults = separate_format_defaults
@@ -255,8 +244,11 @@ class PlainRegistry(metaclass=RegistryMeta):
#: Default locale identifier string, used when calling format_babel without explicit locale.
self.set_fmt_locale(fmt_locale)
+ #: sets the formatter used when plotting with matplotlib
+ self.mpl_formatter = mpl_formatter
+
#: Numerical type used for non integer values.
- self.non_int_type = non_int_type
+ self._non_int_type = non_int_type
#: Default unit case sensitivity
self.case_sensitive = case_sensitive
@@ -266,7 +258,9 @@ class PlainRegistry(metaclass=RegistryMeta):
self._defaults: Dict[str, str] = {}
#: Map dimension name (string) to its definition (DimensionDefinition).
- self._dimensions: Dict[str, DimensionDefinition] = {}
+ self._dimensions: Dict[
+ str, Union[DimensionDefinition, DerivedDimensionDefinition]
+ ] = {}
#: Map unit name (string) to its definition (UnitDefinition).
#: Might contain prefixed units.
@@ -282,9 +276,7 @@ class PlainRegistry(metaclass=RegistryMeta):
self._units_casei: Dict[str, Set[str]] = defaultdict(set)
#: Map prefix name (string) to its definition (PrefixDefinition).
- self._prefixes: Dict[str, PrefixDefinition] = {
- "": PrefixDefinition("", "", (), 1)
- }
+ self._prefixes: Dict[str, PrefixDefinition] = {"": PrefixDefinition("", 1)}
#: Map suffix name (string) to canonical , and unit alias to canonical unit name
self._suffixes: Dict[str, str] = {"": "", "s": ""}
@@ -296,20 +288,23 @@ class PlainRegistry(metaclass=RegistryMeta):
def __init_subclass__(cls, **kwargs):
super().__init_subclass__()
- cls.Unit = build_dependent_class(cls, "Unit", "_unit_class")
- cls.Quantity = build_dependent_class(cls, "Quantity", "_quantity_class")
+ cls.Unit: Unit = build_dependent_class(cls, "Unit", "_unit_class")
+ cls.Quantity: Quantity = build_dependent_class(
+ cls, "Quantity", "_quantity_class"
+ )
def _init_dynamic_classes(self) -> None:
"""Generate subclasses on the fly and attach them to self"""
- self.Unit = create_class_with_registry(self, self.Unit)
- self.Quantity = create_class_with_registry(self, self.Quantity)
+ self.Unit: Unit = create_class_with_registry(self, self.Unit)
+ self.Quantity: Quantity = create_class_with_registry(self, self.Quantity)
def _after_init(self) -> None:
"""This should be called after all __init__"""
if self._filename == "":
- loaded_files = self.load_definitions("default_en.txt", True)
+ path = pathlib.Path(__file__).parent.parent.parent / "default_en.txt"
+ loaded_files = self.load_definitions(path, True)
elif self._filename is not None:
loaded_files = self.load_definitions(self._filename)
else:
@@ -318,19 +313,18 @@ class PlainRegistry(metaclass=RegistryMeta):
self._build_cache(loaded_files)
self._initialized = True
- def _register_directives(self) -> None:
- self._register_directive("@alias", self._load_alias, AliasDefinition)
- self._register_directive("@defaults", self._load_defaults, DefaultsDefinition)
-
- def _load_defaults(self, defaults_definition: DefaultsDefinition) -> None:
- """Loader for a @default section."""
-
- for k, v in defaults_definition.content:
- self._defaults[k] = v
+ def _register_adder(self, definition_class, adder_func):
+ """Register a block definition."""
+ self._adders[definition_class] = adder_func
- def _load_alias(self, alias_definition: AliasDefinition) -> None:
- """Loader for an @alias directive"""
- self._define_alias(alias_definition)
+ def _register_definition_adders(self) -> None:
+ self._register_adder(AliasDefinition, self._add_alias)
+ self._register_adder(DefaultsDefinition, self._add_defaults)
+ self._register_adder(CommentDefinition, lambda o: o)
+ self._register_adder(PrefixDefinition, self._add_prefix)
+ self._register_adder(UnitDefinition, self._add_unit)
+ self._register_adder(DimensionDefinition, self._add_dimension)
+ self._register_adder(DerivedDimensionDefinition, self._add_derived_dimension)
def __deepcopy__(self, memo) -> "PlainRegistry":
new = object.__new__(type(self))
@@ -408,7 +402,11 @@ class PlainRegistry(metaclass=RegistryMeta):
return self._diskcache.cache_folder
return None
- def define(self, definition: Union[str, Definition]) -> None:
+ @property
+ def non_int_type(self):
+ return self._non_int_type
+
+ def define(self, definition):
"""Add unit to the registry.
Parameters
@@ -418,164 +416,100 @@ class PlainRegistry(metaclass=RegistryMeta):
"""
if isinstance(definition, str):
- for line in definition.split("\n"):
- if line.startswith("@alias"):
- # TODO why alias can be defined like this but not other directives?
- self._define_alias(
- AliasDefinition.from_string(line, self.non_int_type)
- )
- else:
- self._define(Definition.from_string(line, self.non_int_type))
- else:
- self._define(definition)
+ parsed_project = self._def_parser.parse_string(definition)
- def _define(self, definition: Definition) -> Tuple[Definition, dict, dict]:
- """Add unit to the registry.
-
- This method defines only multiplicative units, converting any other type
- to `delta_` units.
-
- Parameters
- ----------
- definition : Definition
- a dimension, unit or prefix definition.
+ for definition in self._def_parser.iter_parsed_project(parsed_project):
+ self._helper_dispatch_adder(definition)
+ else:
+ self._helper_dispatch_adder(definition)
- Returns
- -------
- Definition, dict, dict
- Definition instance, case sensitive unit dict, case insensitive unit dict.
+ ############
+ # Adders
+ # - we first provide some helpers that deal with repetitive task.
+ # - then we define specific adder for each definition class. :-D
+ ############
+ def _helper_dispatch_adder(self, definition):
+ """Helper function to add a single definition,
+ choosing the appropiate method by class.
"""
-
- if isinstance(definition, DimensionDefinition):
- d, di = self._dimensions, None
-
- elif isinstance(definition, UnitDefinition):
- d, di = self._units, self._units_casei
-
- # For a plain units, we need to define the related dimension
- # (making sure there is only one to define)
- if definition.is_base:
- self._base_units.append(definition.name)
-
- for dimension in definition.reference.keys():
- if dimension in self._dimensions:
- if dimension != "[]":
- raise DefinitionSyntaxError(
- "Only one unit per dimension can be a plain unit"
- )
- continue
-
- self.define(
- DimensionDefinition(dimension, "", (), None, None, True)
- )
-
- elif isinstance(definition, PrefixDefinition):
- d, di = self._prefixes, None
-
+ for cls in inspect.getmro(definition.__class__):
+ if cls in self._adders:
+ adder_func = self._adders[cls]
+ break
else:
- raise TypeError("{} is not a valid definition.".format(definition))
-
- # define "delta_" units for units with an offset
- if getattr(definition.converter, "offset", 0) != 0:
-
- if definition.name.startswith("["):
- d_name = "[delta_" + definition.name[1:]
- else:
- d_name = "delta_" + definition.name
-
- if definition.symbol:
- d_symbol = "Δ" + definition.symbol
- else:
- d_symbol = None
-
- d_aliases = tuple("Δ" + alias for alias in definition.aliases) + tuple(
- "delta_" + alias for alias in definition.aliases
- )
-
- d_reference = self.UnitsContainer(
- {ref: value for ref, value in definition.reference.items()}
- )
-
- d_def = UnitDefinition(
- d_name,
- d_symbol,
- d_aliases,
- ScaleConverter(definition.converter.scale),
- d_reference,
- definition.is_base,
+ raise TypeError(
+ f"No loader function defined " f"for {definition.__class__.__name__}"
)
- else:
- d_def = definition
-
- self._define_adder(d_def, d, di)
- return definition, d, di
+ adder_func(definition)
- def _define_adder(self, definition, unit_dict, casei_unit_dict):
+ def _helper_adder(self, definition, target_dict, casei_target_dict):
"""Helper function to store a definition in the internal dictionaries.
It stores the definition under its name, symbol and aliases.
"""
- self._define_single_adder(
- definition.name, definition, unit_dict, casei_unit_dict
+ self._helper_single_adder(
+ definition.name, definition, target_dict, casei_target_dict
)
- if definition.has_symbol:
- self._define_single_adder(
- definition.symbol, definition, unit_dict, casei_unit_dict
+ if getattr(definition, "has_symbol", ""):
+ self._helper_single_adder(
+ definition.symbol, definition, target_dict, casei_target_dict
)
- for alias in definition.aliases:
+ for alias in getattr(definition, "aliases", ()):
if " " in alias:
logger.warn("Alias cannot contain a space: " + alias)
- self._define_single_adder(alias, definition, unit_dict, casei_unit_dict)
+ self._helper_single_adder(alias, definition, target_dict, casei_target_dict)
- def _define_single_adder(self, key, value, unit_dict, casei_unit_dict):
+ def _helper_single_adder(self, key, value, target_dict, casei_target_dict):
"""Helper function to store a definition in the internal dictionaries.
It warns or raise error on redefinition.
"""
- if key in unit_dict:
+ if key in target_dict:
if self._on_redefinition == "raise":
raise RedefinitionError(key, type(value))
elif self._on_redefinition == "warn":
logger.warning("Redefining '%s' (%s)" % (key, type(value)))
- unit_dict[key] = value
- if casei_unit_dict is not None:
- casei_unit_dict[key.lower()].add(key)
+ target_dict[key] = value
+ if casei_target_dict is not None:
+ casei_target_dict[key.lower()].add(key)
- def _define_alias(self, definition):
- if not isinstance(definition, AliasDefinition):
- raise TypeError(
- "Not a valid input type for _define_alias. "
- f"(expected: AliasDefinition, found: {type(definition)}"
- )
+ def _add_defaults(self, defaults_definition: DefaultsDefinition):
+ for k, v in defaults_definition.items():
+ self._defaults[k] = v
+ def _add_alias(self, definition: AliasDefinition):
unit_dict = self._units
unit = unit_dict[definition.name]
while not isinstance(unit, UnitDefinition):
unit = unit_dict[unit.name]
for alias in definition.aliases:
- self._define_single_adder(alias, unit, self._units, self._units_casei)
+ self._helper_single_adder(alias, unit, self._units, self._units_casei)
- def _register_directive(self, prefix: str, loaderfunc, definition_class):
- """Register a loader for a given @ directive.
+ def _add_dimension(self, definition: DimensionDefinition):
+ self._helper_adder(definition, self._dimensions, None)
- Parameters
- ----------
- prefix
- string identifying the section (e.g. @context).
- loaderfunc
- function to load the definition into the registry.
- definition_class
- a class that represents the directive content.
- """
- if prefix and prefix[0] == "@":
- self._directives[prefix] = (loaderfunc, definition_class)
- else:
- raise ValueError("Prefix directives must start with '@'")
+ def _add_derived_dimension(self, definition: DerivedDimensionDefinition):
+ for dim_name in definition.reference.keys():
+ if dim_name not in self._dimensions:
+ self._add_dimension(DimensionDefinition(dim_name))
+ self._helper_adder(definition, self._dimensions, None)
+
+ def _add_prefix(self, definition: PrefixDefinition):
+ self._helper_adder(definition, self._prefixes, None)
+
+ def _add_unit(self, definition: UnitDefinition):
+ if definition.is_base:
+ self._base_units.append(definition.name)
+ for dim_name in definition.reference.keys():
+ if dim_name not in self._dimensions:
+ self._add_dimension(DimensionDefinition(dim_name))
+
+ self._helper_adder(definition, self._units, self._units_casei)
def load_definitions(self, file, is_resource: bool = False):
"""Add units and prefixes defined in a definition text file.
@@ -589,50 +523,26 @@ class PlainRegistry(metaclass=RegistryMeta):
and therefore should be loaded from the package. (Default value = False)
"""
- loaders = {
- AliasDefinition: self._define,
- UnitDefinition: self._define,
- DimensionDefinition: self._define,
- PrefixDefinition: self._define,
- }
-
- p = parser.Parser(self.non_int_type, cache_folder=self._diskcache)
- for prefix, (loaderfunc, definition_class) in self._directives.items():
- loaders[definition_class] = loaderfunc
- p.register_class(prefix, definition_class)
-
- if isinstance(file, (str, pathlib.Path)):
- try:
- parsed_files = p.parse(file, is_resource)
- except Exception as ex:
- # TODO: Change this is in the future
- # this is kept for backwards compatibility
- msg = getattr(ex, "message", "") or str(ex)
- raise ValueError("While opening {}\n{}".format(file, msg))
+ if isinstance(file, (list, tuple)):
+ # TODO: this hack was to keep it backwards compatible.
+ parsed_project = self._def_parser.parse_string("\n".join(file))
else:
- parsed_files = parser.DefinitionFiles([p.parse_lines(file)])
+ parsed_project = self._def_parser.parse_file(file)
- for lineno, definition in parsed_files.iter_definitions():
- if definition.__class__ in p.handled_classes:
- continue
- loaderfunc = loaders.get(definition.__class__, None)
- if not loaderfunc:
- raise ValueError(
- f"No loader function defined "
- f"for {definition.__class__.__name__}"
- )
- loaderfunc(definition)
+ for definition in self._def_parser.iter_parsed_project(parsed_project):
+ self._helper_dispatch_adder(definition)
- return parsed_files
+ return parsed_project
def _build_cache(self, loaded_files=None) -> None:
"""Build a cache of dimensionality and plain units."""
- if loaded_files and self._diskcache and all(loaded_files):
- cache, cache_basename = self._diskcache.load(loaded_files, "build_cache")
+ diskcache = self._diskcache
+ if loaded_files and diskcache:
+ cache, cache_basename = diskcache.load(loaded_files, "build_cache")
if cache is None:
self._build_cache()
- self._diskcache.save(self._cache, loaded_files, "build_cache")
+ diskcache.save(self._cache, loaded_files, "build_cache")
return
self._cache = RegistryCache()
@@ -1035,7 +945,6 @@ class PlainRegistry(metaclass=RegistryMeta):
"""
if check_dimensionality:
-
src_dim = self._get_dimensionality(src)
dst_dim = self._get_dimensionality(dst)
@@ -1216,7 +1125,6 @@ class PlainRegistry(metaclass=RegistryMeta):
return ret
def _eval_token(self, token, case_sensitive=None, use_decimal=False, **values):
-
# TODO: remove this code when use_decimal is deprecated
if use_decimal:
raise DeprecationWarning(
@@ -1231,9 +1139,9 @@ class PlainRegistry(metaclass=RegistryMeta):
if token_text == "dimensionless":
return 1 * self.dimensionless
elif token_text.lower() in ("inf", "infinity"):
- return float("inf")
+ return self.non_int_type("inf")
elif token_text.lower() == "nan":
- return float("nan")
+ return self.non_int_type("nan")
elif token_text in values:
return self.Quantity(values[token_text])
else:
diff --git a/pint/facets/plain/unit.py b/pint/facets/plain/unit.py
index 5fb050b..b608c05 100644
--- a/pint/facets/plain/unit.py
+++ b/pint/facets/plain/unit.py
@@ -21,7 +21,7 @@ from ...util import PrettyIPython, SharedRegistryObject, UnitsContainer
from .definitions import UnitDefinition
if TYPE_CHECKING:
- from pint import Context
+ from ..context import Context
class PlainUnit(PrettyIPython, SharedRegistryObject):
diff --git a/pint/facets/system/__init__.py b/pint/facets/system/__init__.py
index 3e04507..e95098b 100644
--- a/pint/facets/system/__init__.py
+++ b/pint/facets/system/__init__.py
@@ -14,4 +14,4 @@ from .definitions import SystemDefinition
from .objects import System
from .registry import SystemRegistry
-__all__ = [SystemDefinition, System, SystemRegistry]
+__all__ = ["SystemDefinition", "System", "SystemRegistry"]
diff --git a/pint/facets/system/definitions.py b/pint/facets/system/definitions.py
index 368dd71..8243324 100644
--- a/pint/facets/system/definitions.py
+++ b/pint/facets/system/definitions.py
@@ -6,82 +6,76 @@
:license: BSD, see LICENSE for more details.
"""
-
from __future__ import annotations
-import re
+import typing as ty
from dataclasses import dataclass
-from typing import Tuple
-from ...util import SourceIterator
+from ... import errors
@dataclass(frozen=True)
-class SystemDefinition:
- """Definition of a System:
-
- @system <name> [using <group 1>, ..., <group N>]
- <rule 1>
- ...
- <rule N>
- @end
+class BaseUnitRule:
+ """A rule to define a base unit within a system."""
- The syntax for the rule is:
+ #: name of the unit to become base unit
+ #: (must exist in the registry)
+ new_unit_name: str
+ #: name of the unit to be kicked out to make room for the new base uni
+ #: If None, the current base unit with the same dimensionality will be used
+ old_unit_name: ty.Optional[str] = None
- new_unit_name : old_unit_name
+ # Instead of defining __post_init__ here,
+ # it will be added to the container class
+ # so that the name and a meaningfull class
+ # could be used.
- where:
- - old_unit_name: a root unit part which is going to be removed from the system.
- - new_unit_name: a non root unit which is going to replace the old_unit.
- If the new_unit_name and the old_unit_name, the later and the colon can be omitted.
- """
-
- #: Regex to match the header parts of a context.
- _header_re = re.compile(r"@system\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*")
+@dataclass(frozen=True)
+class SystemDefinition(errors.WithDefErr):
+ """Definition of a System."""
+ #: name of the system
name: str
- unit_replacements: Tuple[Tuple[int, str, str], ...]
- using_group_names: Tuple[str, ...]
+ #: unit groups that will be included within the system
+ using_group_names: ty.Tuple[str, ...]
+ #: rules to define new base unit within the system.
+ rules: ty.Tuple[BaseUnitRule, ...]
@classmethod
- def from_lines(cls, lines, non_int_type=float):
- lines = SourceIterator(lines)
-
- lineno, header = next(lines)
-
- r = cls._header_re.search(header)
-
- if r is None:
- raise ValueError("Invalid System header syntax '%s'" % header)
-
- name = r.groupdict()["name"].strip()
- groups = r.groupdict()["used_groups"]
-
- # If the systems has no group, it automatically uses the root group.
- if groups:
- group_names = tuple(a.strip() for a in groups.split(","))
- else:
- group_names = ("root",)
-
- unit_replacements = []
- for lineno, line in lines:
- line = line.strip()
-
- # We would identify a
- # - old_unit: a root unit part which is going to be removed from the system.
- # - new_unit: a non root unit which is going to replace the old_unit.
-
- if ":" in line:
- # The syntax is new_unit:old_unit
-
- new_unit, old_unit = line.split(":")
- new_unit, old_unit = new_unit.strip(), old_unit.strip()
-
- unit_replacements.append((lineno, new_unit, old_unit))
- else:
- # The syntax is new_unit
- # old_unit is inferred as the root unit with the same dimensionality.
- unit_replacements.append((lineno, line, None))
-
- return cls(name, tuple(unit_replacements), group_names)
+ def from_lines(cls, lines, non_int_type):
+ # TODO: this is to keep it backwards compatible
+ from ...delegates import ParserConfig, txt_defparser
+
+ cfg = ParserConfig(non_int_type)
+ parser = txt_defparser.DefParser(cfg, None)
+ pp = parser.parse_string("\n".join(lines) + "\n@end")
+ for definition in parser.iter_parsed_project(pp):
+ if isinstance(definition, cls):
+ return definition
+
+ @property
+ def unit_replacements(self) -> ty.Tuple[ty.Tuple[str, str], ...]:
+ return tuple((el.new_unit_name, el.old_unit_name) for el in self.rules)
+
+ def __post_init__(self):
+ if not errors.is_valid_system_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_SYSTEM_NAME)
+
+ for k in self.using_group_names:
+ if not errors.is_valid_group_name(k):
+ raise self.def_err(
+ f"refers to '{k}' that " + errors.MSG_INVALID_GROUP_NAME
+ )
+
+ for ndx, rule in enumerate(self.rules, 1):
+ if not errors.is_valid_unit_name(rule.new_unit_name):
+ raise self.def_err(
+ f"rule #{ndx} refers to '{rule.new_unit_name}' that "
+ + errors.MSG_INVALID_UNIT_NAME
+ )
+ if rule.old_unit_name and not errors.is_valid_unit_name(rule.old_unit_name):
+ raise self.def_err(
+ f"rule #{ndx} refers to '{rule.old_unit_name}' that "
+ + errors.MSG_INVALID_UNIT_NAME
+ )
diff --git a/pint/facets/system/objects.py b/pint/facets/system/objects.py
index b81dfc5..829fb5c 100644
--- a/pint/facets/system/objects.py
+++ b/pint/facets/system/objects.py
@@ -119,16 +119,19 @@ class System(SharedRegistryObject):
return cls.from_definition(system_definition, get_root_func)
@classmethod
- def from_definition(cls, system_definition: SystemDefinition, get_root_func):
+ def from_definition(cls, system_definition: SystemDefinition, get_root_func=None):
+ if get_root_func is None:
+ # TODO: kept for backwards compatibility
+ get_root_func = cls._REGISTRY.get_root_units
base_unit_names = {}
derived_unit_names = []
- for lineno, new_unit, old_unit in system_definition.unit_replacements:
+ for new_unit, old_unit in system_definition.unit_replacements:
if old_unit is None:
old_unit_dict = to_units_container(get_root_func(new_unit)[1])
if len(old_unit_dict) != 1:
raise ValueError(
- "The new plain must be a root dimension if not discarded unit is specified."
+ "The new unit must be a root dimension if not discarded unit is specified."
)
old_unit, value = dict(old_unit_dict).popitem()
@@ -138,8 +141,8 @@ class System(SharedRegistryObject):
# The old unit MUST be a root unit, if not raise an error.
if old_unit != str(get_root_func(old_unit)[1]):
raise ValueError(
- "In `%s`, the unit at the right of the `:` (%s) must be a root unit."
- % (lineno, old_unit)
+ f"The old unit {old_unit} must be a root unit "
+ f"in order to be replaced by new unit {new_unit}"
)
# Here we find new_unit expanded in terms of root_units
diff --git a/pint/facets/system/registry.py b/pint/facets/system/registry.py
index ca60766..527440a 100644
--- a/pint/facets/system/registry.py
+++ b/pint/facets/system/registry.py
@@ -11,8 +11,10 @@ from __future__ import annotations
from numbers import Number
from typing import TYPE_CHECKING, Dict, FrozenSet, Tuple, Union
+from ... import errors
+
if TYPE_CHECKING:
- from pint import Quantity, Unit
+ from ..._typing import Quantity, Unit
from ..._typing import UnitLike
from ...util import UnitsContainer as UnitsContainerT
@@ -41,6 +43,9 @@ class SystemRegistry(GroupRegistry):
- Parse @group directive.
"""
+ # TODO: Change this to System: System to specify class
+ # and use introspection to get system class as a way
+ # to enjoy typing goodies
_system_class = System
def __init__(self, system=None, **kwargs):
@@ -77,13 +82,21 @@ class SystemRegistry(GroupRegistry):
"system", None
)
- def _register_directives(self) -> None:
- super()._register_directives()
- self._register_directive(
- "@system",
- lambda gd: self.System.from_definition(gd, self.get_root_units),
- SystemDefinition,
- )
+ def _register_definition_adders(self) -> None:
+ super()._register_definition_adders()
+ self._register_adder(SystemDefinition, self._add_system)
+
+ def _add_system(self, sd: SystemDefinition):
+ if sd.name in self._systems:
+ raise ValueError(f"System {sd.name} already present in registry")
+
+ try:
+ # As a System is a SharedRegistryObject
+ # it adds itself to the registry.
+ self.System.from_definition(sd)
+ except KeyError as e:
+ # TODO: fix this error message
+ raise errors.DefinitionError(f"unknown dimension {e} in context")
@property
def sys(self):
@@ -172,7 +185,6 @@ class SystemRegistry(GroupRegistry):
check_nonmult: bool = True,
system: Union[str, System, None] = None,
):
-
if system is None:
system = self._default_system
@@ -213,7 +225,6 @@ class SystemRegistry(GroupRegistry):
return base_factor, destination_units
def _get_compatible_units(self, input_units, group_or_system) -> FrozenSet[Unit]:
-
if group_or_system is None:
group_or_system = self._default_system
diff --git a/pint/formatting.py b/pint/formatting.py
index b8b3370..f450d5f 100644
--- a/pint/formatting.py
+++ b/pint/formatting.py
@@ -10,6 +10,7 @@
from __future__ import annotations
+import functools
import re
import warnings
from typing import Callable, Dict
@@ -157,7 +158,7 @@ def register_unit_format(name):
def wrapper(func):
if name in _FORMATTERS:
- raise ValueError(f"format {name:!r} already exists") # or warn instead
+ raise ValueError(f"format {name!r} already exists") # or warn instead
_FORMATTERS[name] = func
return wrapper
@@ -178,10 +179,26 @@ def format_pretty(unit, registry, **options):
)
+def latex_escape(string):
+ """
+ Prepend characters that have a special meaning in LaTeX with a backslash.
+ """
+ return functools.reduce(
+ lambda s, m: re.sub(m[0], m[1], s),
+ (
+ (r"[\\]", r"\\textbackslash "),
+ (r"[~]", r"\\textasciitilde "),
+ (r"[\^]", r"\\textasciicircum "),
+ (r"([&%$#_{}])", r"\\\1"),
+ ),
+ str(string),
+ )
+
+
@register_unit_format("L")
def format_latex(unit, registry, **options):
preprocessed = {
- r"\mathrm{{{}}}".format(u.replace("_", r"\_")): p for u, p in unit.items()
+ r"\mathrm{{{}}}".format(latex_escape(u)): p for u, p in unit.items()
}
formatted = formatter(
preprocessed.items(),
@@ -439,8 +456,9 @@ def siunitx_format_unit(units, registry):
lpick = lpos if power >= 0 else lneg
prefix = None
+ # TODO: fix this to be fore efficient and detect also aliases.
for p in registry._prefixes.values():
- p = str(p)
+ p = str(p.name)
if len(p) > 0 and unit.find(p) == 0:
prefix = p
unit = unit.replace(prefix, "", 1)
diff --git a/pint/matplotlib.py b/pint/matplotlib.py
index 3785c7d..ea88c70 100644
--- a/pint/matplotlib.py
+++ b/pint/matplotlib.py
@@ -21,7 +21,8 @@ class PintAxisInfo(matplotlib.units.AxisInfo):
def __init__(self, units):
"""Set the default label to the pretty-print of the unit."""
- super().__init__(label="{:P}".format(units))
+ formatter = units._REGISTRY.mpl_formatter
+ super().__init__(label=formatter.format(units))
class PintConverter(matplotlib.units.ConversionInterface):
diff --git a/pint/parser.py b/pint/parser.py
deleted file mode 100644
index e73e578..0000000
--- a/pint/parser.py
+++ /dev/null
@@ -1,374 +0,0 @@
-"""
- pint.parser
- ~~~~~~~~~~~
-
- Classes and methods to parse a definition text file into a DefinitionFile.
-
- :copyright: 2019 by Pint Authors, see AUTHORS for more details.
- :license: BSD, see LICENSE for more details.
-"""
-
-from __future__ import annotations
-
-import pathlib
-import re
-from dataclasses import dataclass, field
-from functools import cached_property
-from importlib import resources
-from io import StringIO
-from typing import Any, Callable, Dict, Generator, Iterable, Optional, Tuple
-
-from ._vendor import flexcache as fc
-from .definitions import Definition
-from .errors import DefinitionSyntaxError
-from .util import SourceIterator, logger
-
-_BLOCK_RE = re.compile(r"[ (]")
-
-ParserFuncT = Callable[[SourceIterator, type], Any]
-
-
-@dataclass(frozen=True)
-class DefinitionFile:
- """Represents a definition file after parsing."""
-
- # Fullpath of the original file, None if a text was provided
- filename: Optional[pathlib.Path]
- is_resource: bool
-
- # Modification time of the file or None.
- mtime: Optional[float]
-
- # SHA-1 hash
- content_hash: Optional[str]
-
- # collection of line number and corresponding definition.
- parsed_lines: Tuple[Tuple[int, Any], ...]
-
- def filter_by(self, *klass):
- yield from (
- (lineno, d) for lineno, d in self.parsed_lines if isinstance(d, klass)
- )
-
- @cached_property
- def errors(self):
- return tuple(self.filter_by(Exception))
-
- def has_errors(self):
- return bool(self.errors)
-
-
-class DefinitionFiles(tuple):
- """Wrapper class that allows handling a tuple containing DefinitionFile."""
-
- @staticmethod
- def _iter_definitions(
- pending_files: list[DefinitionFile],
- ) -> Generator[Tuple[int, Definition]]:
- """Internal method to iterate definitions.
-
- pending_files is a mutable list of definitions files
- and elements are being removed as they are yielded.
- """
- if not pending_files:
- return
- current_file = pending_files.pop(0)
- for lineno, definition in current_file.parsed_lines:
- if isinstance(definition, ImportDefinition):
- if not pending_files:
- raise ValueError(
- f"No more files while trying to import {definition.path}."
- )
-
- if not str(pending_files[0].filename).endswith(str(definition.path)):
- raise ValueError(
- "The order of the files do not match. "
- f"(expected: {definition.path}, "
- f"found {pending_files[0].filename})"
- )
-
- yield from DefinitionFiles._iter_definitions(pending_files)
- else:
- yield lineno, definition
-
- def iter_definitions(self):
- """Iter all definitions in the order they appear,
- going into the included files.
-
- Important: This assumes that the order of the imported files
- is the one that they will appear in the definitions.
- """
- yield from self._iter_definitions(list(self))
-
-
-def build_disk_cache_class(non_int_type: type):
- """Build disk cache class, taking into account the non_int_type."""
-
- @dataclass(frozen=True)
- class PintHeader(fc.InvalidateByExist, fc.NameByFields, fc.BasicPythonHeader):
-
- from . import __version__
-
- pint_version: str = __version__
- non_int_type: str = field(default_factory=lambda: non_int_type.__qualname__)
-
- class PathHeader(fc.NameByFileContent, PintHeader):
- pass
-
- class DefinitionFilesHeader(fc.NameByHashIter, PintHeader):
- @classmethod
- def from_definition_files(cls, dfs: DefinitionFiles, reader_id):
- return cls(tuple(df.content_hash for df in dfs), reader_id)
-
- class PintDiskCache(fc.DiskCache):
-
- _header_classes = {
- pathlib.Path: PathHeader,
- str: PathHeader.from_string,
- DefinitionFiles: DefinitionFilesHeader.from_definition_files,
- }
-
- return PintDiskCache
-
-
-@dataclass(frozen=True)
-class ImportDefinition:
- """Definition for the @import directive"""
-
- path: pathlib.Path
-
- @classmethod
- def from_string(
- cls, definition: str, non_int_type: type = float
- ) -> ImportDefinition:
- return ImportDefinition(pathlib.Path(definition[7:].strip()))
-
-
-class Parser:
- """Class to parse a definition file into an intermediate object representation.
-
- non_int_type
- numerical type used for non integer values. (Default: float)
- raise_on_error
- if True, an exception will be raised as soon as a Definition Error it is found.
- if False, the exception will be added to the ParedDefinitionFile
- """
-
- #: Map context prefix to function
- _directives: Dict[str, ParserFuncT]
-
- _diskcache: fc.DiskCache
-
- handled_classes = (ImportDefinition,)
-
- def __init__(self, non_int_type=float, raise_on_error=True, cache_folder=None):
- self._directives = {}
- self._non_int_type = non_int_type
- self._raise_on_error = raise_on_error
- self.register_class("@import", ImportDefinition)
-
- if isinstance(cache_folder, (str, pathlib.Path)):
- self._diskcache = build_disk_cache_class(non_int_type)(cache_folder)
- else:
- self._diskcache = cache_folder
-
- def register_directive(
- self, prefix: str, parserfunc: ParserFuncT, single_line: bool
- ):
- """Register a parser for a given @ directive..
-
- Parameters
- ----------
- prefix
- string identifying the section (e.g. @context)
- parserfunc
- function that is able to parse a definition into a DefinitionObject
- single_line
- indicates that the directive spans in a single line, i.e. and @end is not required.
- """
- if prefix and prefix[0] == "@":
- if single_line:
- self._directives[prefix] = lambda si, non_int_type: parserfunc(
- si.last[1], non_int_type
- )
- else:
- self._directives[prefix] = lambda si, non_int_type: parserfunc(
- si.block_iter(), non_int_type
- )
- else:
- raise ValueError("Prefix directives must start with '@'")
-
- def register_class(self, prefix: str, klass):
- """Register a definition class for a directive and try to guess
- if it is a line or block directive from the signature.
- """
- if hasattr(klass, "from_string"):
- self.register_directive(prefix, klass.from_string, True)
- elif hasattr(klass, "from_lines"):
- self.register_directive(prefix, klass.from_lines, False)
- else:
- raise ValueError(
- f"While registering {prefix}, {klass} does not have `from_string` or from_lines` method"
- )
-
- def parse(self, file, is_resource: bool = False) -> DefinitionFiles:
- """Parse a file or resource into a collection of DefinitionFile that will
- include all other files imported.
-
- Parameters
- ----------
- file
- definitions or file containing definition.
- is_resource
- indicates that the file is a resource file
- and therefore should be loaded from the package.
- (Default value = False)
- """
-
- if is_resource:
- parsed = self.parse_single_resource(file)
- else:
- path = pathlib.Path(file)
- if self._diskcache is None:
- parsed = self.parse_single(path, None)
- else:
- parsed, content_hash = self._diskcache.load(
- path, self.parse_single, True
- )
-
- out = [parsed]
- for lineno, content in parsed.filter_by(ImportDefinition):
- if parsed.is_resource:
- path = content.path
- else:
- try:
- basedir = parsed.filename.parent
- except AttributeError:
- basedir = pathlib.Path.cwd()
- path = basedir.joinpath(content.path)
- out.extend(self.parse(path, parsed.is_resource))
- return DefinitionFiles(out)
-
- def parse_single_resource(self, resource_name: str) -> DefinitionFile:
- """Parse a resource in the package into a DefinitionFile.
-
- Imported files will appear as ImportDefinition objects and
- will not be followed.
-
- This method will try to load it first as a regular file
- (with a path and mtime) to allow caching.
- If this files (i.e. the resource is not filesystem file)
- it will use python importlib.resources.read_binary
- """
-
- with resources.path(__package__, resource_name) as p:
- filepath = p.resolve()
-
- if filepath.exists():
- if self._diskcache is None:
- return self.parse_single(filepath, None)
- else:
- definition_file, content_hash = self._diskcache.load(
- filepath, self.parse_single, True
- )
- return definition_file
-
- logger.debug("Cannot use_cache resource (yet) without a real path")
- return self._parse_single_resource(resource_name)
-
- def _parse_single_resource(self, resource_name: str) -> DefinitionFile:
- rbytes = resources.read_binary(__package__, resource_name)
- if self._diskcache:
- hdr = self._diskcache.PathHeader(rbytes)
- content_hash = self._diskcache.cache_stem_for(hdr)
- else:
- content_hash = None
-
- si = SourceIterator(
- StringIO(rbytes.decode("utf-8")), resource_name, is_resource=True
- )
- parsed_lines = tuple(self.yield_from_source_iterator(si))
- return DefinitionFile(
- filename=pathlib.Path(resource_name),
- is_resource=True,
- mtime=None,
- content_hash=content_hash,
- parsed_lines=parsed_lines,
- )
-
- def parse_single(
- self, filepath: pathlib.Path, content_hash: Optional[str]
- ) -> DefinitionFile:
- """Parse a filepath without nesting into dependent files.
-
- Imported files will appear as ImportDefinition objects and
- will not be followed.
-
- Parameters
- ----------
- filepath
- definitions or file containing definition.
- """
- with filepath.open(encoding="utf-8") as fp:
- si = SourceIterator(fp, filepath, is_resource=False)
- parsed_lines = tuple(self.yield_from_source_iterator(si))
-
- filename = filepath.resolve()
- mtime = filepath.stat().st_mtime
-
- return DefinitionFile(
- filename=filename,
- is_resource=False,
- mtime=mtime,
- content_hash=content_hash,
- parsed_lines=parsed_lines,
- )
-
- def parse_lines(self, lines: Iterable[str]) -> DefinitionFile:
- """Parse an iterable of strings into a dependent file"""
- si = SourceIterator(lines, None, False)
- parsed_lines = tuple(self.yield_from_source_iterator(si))
- df = DefinitionFile(None, False, None, "", parsed_lines=parsed_lines)
- if any(df.filter_by(ImportDefinition)):
- raise ValueError(
- "Cannot use the @import directive when parsing "
- "an iterable of strings."
- )
- return df
-
- def yield_from_source_iterator(
- self, source_iterator: SourceIterator
- ) -> Generator[Tuple[int, Any]]:
- """Iterates through the source iterator, yields line numbers and
- the coresponding parsed definition object.
-
- Parameters
- ----------
- source_iterator
- """
- for lineno, line in source_iterator:
- try:
- if line.startswith("@"):
- # Handle @ directives dispatching to the appropriate parsers
- parts = _BLOCK_RE.split(line)
-
- subparser = self._directives.get(parts[0], None)
-
- if subparser is None:
- raise DefinitionSyntaxError(
- "Unknown directive %s" % line, lineno=lineno
- )
-
- d = subparser(source_iterator, self._non_int_type)
- yield lineno, d
- else:
- yield lineno, Definition.from_string(line, self._non_int_type)
- except DefinitionSyntaxError as ex:
- if ex.lineno is None:
- ex.lineno = lineno
- if self._raise_on_error:
- raise ex
- yield lineno, ex
- except Exception as ex:
- logger.error("In line {}, cannot add '{}' {}".format(lineno, line, ex))
- raise ex
diff --git a/pint/pint-convert b/pint/pint_convert.py
index 600016b..b30bb94 100755
--- a/pint/pint-convert
+++ b/pint/pint_convert.py
@@ -170,4 +170,9 @@ def use_unc(num, fmt, prec_unc):
return max(0, min(prec_unc, unc))
-convert(args.fr, args.to)
+def main():
+ convert(args.fr, args.to)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/pint/registry.py b/pint/registry.py
index a5aa9b3..29d5c89 100644
--- a/pint/registry.py
+++ b/pint/registry.py
@@ -88,7 +88,6 @@ class UnitRegistry(
case_sensitive: bool = True,
cache_folder=None,
):
-
super().__init__(
filename=filename,
force_ndarray=force_ndarray,
diff --git a/pint/registry_helpers.py b/pint/registry_helpers.py
index 8517ff3..07b00ff 100644
--- a/pint/registry_helpers.py
+++ b/pint/registry_helpers.py
@@ -20,8 +20,7 @@ from .errors import DimensionalityError
from .util import UnitsContainer, to_units_container
if TYPE_CHECKING:
- from pint import Quantity, Unit
-
+ from ._typing import Quantity, Unit
from .registry import UnitRegistry
T = TypeVar("T")
@@ -72,7 +71,6 @@ def _to_units_container(a, registry=None):
def _parse_wrap_args(args, registry=None):
-
# Arguments which contain definitions
# (i.e. names that appear alone and for the first time)
defs_args = set()
@@ -143,7 +141,6 @@ def _parse_wrap_args(args, registry=None):
# third pass: convert other arguments
for ndx in unit_args_ndx:
-
if isinstance(values[ndx], ureg.Quantity):
new_values[ndx] = ureg._convert(
values[ndx]._magnitude, values[ndx]._units, args_as_uc[ndx][0]
@@ -256,7 +253,6 @@ def wraps(
ret = _to_units_container(ret, ureg)
def decorator(func: Callable[..., T]) -> Callable[..., Quantity[T]]:
-
count_params = len(signature(func).parameters)
if len(args) != count_params:
raise TypeError(
@@ -273,7 +269,6 @@ def wraps(
@functools.wraps(func, assigned=assigned, updated=updated)
def wrapper(*values, **kw) -> Quantity[T]:
-
values, kw = _apply_defaults(func, values, kw)
# In principle, the values are used as is
@@ -339,7 +334,6 @@ def check(
]
def decorator(func):
-
count_params = len(signature(func).parameters)
if len(dimensions) != count_params:
raise TypeError(
@@ -359,7 +353,6 @@ def check(
list_args, empty = _apply_defaults(func, args, kwargs)
for dim, value in zip(dimensions, list_args):
-
if dim is None:
continue
diff --git a/pint/testsuite/baseline/test_basic_plot.png b/pint/testsuite/baseline/test_basic_plot.png
index 63be609..b0c4d18 100644
--- a/pint/testsuite/baseline/test_basic_plot.png
+++ b/pint/testsuite/baseline/test_basic_plot.png
Binary files differ
diff --git a/pint/testsuite/baseline/test_plot_with_non_default_format.png b/pint/testsuite/baseline/test_plot_with_non_default_format.png
new file mode 100644
index 0000000..1cb5b18
--- /dev/null
+++ b/pint/testsuite/baseline/test_plot_with_non_default_format.png
Binary files differ
diff --git a/pint/testsuite/baseline/test_plot_with_set_units.png b/pint/testsuite/baseline/test_plot_with_set_units.png
index 5fd3ce0..a59924c 100644
--- a/pint/testsuite/baseline/test_plot_with_set_units.png
+++ b/pint/testsuite/baseline/test_plot_with_set_units.png
Binary files differ
diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py
index 570b287..4c560fb 100644
--- a/pint/testsuite/helpers.py
+++ b/pint/testsuite/helpers.py
@@ -1,9 +1,9 @@
import doctest
import pickle
import re
-from distutils.version import LooseVersion
import pytest
+from packaging.version import parse as version_parse
from pint.testing import assert_allclose as assert_quantity_almost_equal # noqa: F401
from pint.testing import assert_equal as assert_quantity_equal # noqa: F401
@@ -114,7 +114,7 @@ def requires_numpy_previous_than(version):
if not HAS_NUMPY:
return pytest.mark.skip("Requires NumPy")
return pytest.mark.skipif(
- not LooseVersion(NUMPY_VER) < LooseVersion(version),
+ not version_parse(NUMPY_VER) < version_parse(version),
reason="Requires NumPy < %s" % version,
)
@@ -123,7 +123,7 @@ def requires_numpy_at_least(version):
if not HAS_NUMPY:
return pytest.mark.skip("Requires NumPy")
return pytest.mark.skipif(
- not LooseVersion(NUMPY_VER) >= LooseVersion(version),
+ not version_parse(NUMPY_VER) >= version_parse(version),
reason="Requires NumPy >= %s" % version,
)
diff --git a/pint/testsuite/test_compat_downcast.py b/pint/testsuite/test_compat_downcast.py
index 8293580..ebb5907 100644
--- a/pint/testsuite/test_compat_downcast.py
+++ b/pint/testsuite/test_compat_downcast.py
@@ -109,7 +109,6 @@ def array(request):
def test_univariate_op_consistency(
local_registry, q_base, op, magnitude_op, unit_op, array
):
-
q = local_registry.Quantity(array, "meter")
res = op(local_registry, q)
assert np.all(
@@ -130,7 +129,6 @@ def test_univariate_op_consistency(
],
)
def test_bivariate_op_consistency(local_registry, q_base, op, unit, array):
-
# This is to avoid having a ureg built at the module level.
unit = unit(local_registry)
diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py
index d8f5d50..c7551e4 100644
--- a/pint/testsuite/test_contexts.py
+++ b/pint/testsuite/test_contexts.py
@@ -323,7 +323,6 @@ class TestContexts:
q.to("Hz")
def test_context_with_arg(self, func_registry):
-
ureg = func_registry
add_arg_ctxs(ureg)
@@ -352,7 +351,6 @@ class TestContexts:
q.to("Hz")
def test_enable_context_with_arg(self, func_registry):
-
ureg = func_registry
add_arg_ctxs(ureg)
@@ -386,7 +384,6 @@ class TestContexts:
ureg.disable_contexts(1)
def test_context_with_arg_def(self, func_registry):
-
ureg = func_registry
add_argdef_ctxs(ureg)
@@ -427,7 +424,6 @@ class TestContexts:
q.to("Hz")
def test_context_with_sharedarg_def(self, func_registry):
-
ureg = func_registry
add_sharedargdef_ctxs(ureg)
@@ -499,7 +495,6 @@ class TestContexts:
helpers.assert_quantity_equal(x.to("s"), ureg("1 s"))
def _test_ctx(self, ctx, ureg):
-
q = 500 * ureg.meter
s = (ureg.speed_of_light / q).to("Hz")
@@ -563,7 +558,6 @@ class TestContexts:
],
)
def test_parse_simple(self, func_registry, source, name, aliases, defaults):
-
a = Context.__keytransform__(
UnitsContainer({"[time]": -1}), UnitsContainer({"[length]": 1})
)
@@ -579,7 +573,6 @@ class TestContexts:
self._test_ctx(c, func_registry)
def test_parse_auto_inverse(self, func_registry):
-
a = Context.__keytransform__(
UnitsContainer({"[time]": -1.0}), UnitsContainer({"[length]": 1.0})
)
@@ -638,7 +631,6 @@ class TestContexts:
Context.from_lines(s)
def test_warnings(self, caplog, func_registry):
-
ureg = func_registry
with caplog.at_level(logging.DEBUG, "pint"):
@@ -783,6 +775,7 @@ def test_redefine(subtests):
# Note how we're redefining a symbol, not the plain name, as a
# function of another name
b = 5 f
+ @end
""".splitlines()
)
# Units that are somehow directly or indirectly defined as a function of the
@@ -933,7 +926,7 @@ def test_err_change_base_unit():
def test_err_to_base_unit():
- expected = "Can't define plain units within a context"
+ expected = ".*can't define plain units within a context"
with pytest.raises(DefinitionSyntaxError, match=expected):
Context.from_lines(["@context c", "x = [d]"])
@@ -980,19 +973,17 @@ def test_err_cyclic_dependency():
def test_err_dimension_redefinition():
- expected = re.escape("Expected <unit> = <converter>; got [d1] = [d2] * [d3]")
- with pytest.raises(DefinitionSyntaxError, match=expected):
+ with pytest.raises(DefinitionSyntaxError):
Context.from_lines(["@context c", "[d1] = [d2] * [d3]"])
def test_err_prefix_redefinition():
- expected = re.escape("Expected <unit> = <converter>; got [d1] = [d2] * [d3]")
- with pytest.raises(DefinitionSyntaxError, match=expected):
+ with pytest.raises(DefinitionSyntaxError):
Context.from_lines(["@context c", "[d1] = [d2] * [d3]"])
def test_err_redefine_alias(subtests):
- expected = "Can't change a unit's symbol or aliases within a context"
+ expected = ".*can't change a unit's symbol or aliases within a context"
for s in ("foo = bar = f", "foo = bar = _ = baz"):
with subtests.test(s):
with pytest.raises(DefinitionSyntaxError, match=expected):
diff --git a/pint/testsuite/test_dask.py b/pint/testsuite/test_dask.py
index 69c80fe..f4dee6a 100644
--- a/pint/testsuite/test_dask.py
+++ b/pint/testsuite/test_dask.py
@@ -149,6 +149,8 @@ def test_compute_persist_equivalent(local_registry, dask_array, numpy_array):
assert np.all(res_compute == res_persist)
assert res_compute.units == res_persist.units == units_
+ assert type(res_compute) == local_registry.Quantity
+ assert type(res_persist) == local_registry.Quantity
@pytest.mark.parametrize("method", ["compute", "persist", "visualize"])
diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py
index 8f5becd..2618c6e 100644
--- a/pint/testsuite/test_definitions.py
+++ b/pint/testsuite/test_definitions.py
@@ -24,7 +24,6 @@ class TestDefinition:
Definition.from_string("[x] = [time] * meter")
def test_prefix_definition(self):
-
with pytest.raises(ValueError):
Definition.from_string("m- = 1e-3 k")
@@ -35,7 +34,6 @@ class TestDefinition:
assert x.aliases == ()
assert x.converter.to_reference(1000) == 1
assert x.converter.from_reference(0.001) == 1
- assert str(x) == "m"
x = Definition.from_string("kilo- = 1e-3 = k-")
assert isinstance(x, PrefixDefinition)
@@ -100,7 +98,6 @@ class TestDefinition:
)
def test_log_unit_definition(self):
-
x = Definition.from_string(
"decibelmilliwatt = 1e-3 watt; logbase: 10; logfactor: 10 = dBm"
)
@@ -161,7 +158,7 @@ class TestDefinition:
assert x.reference == UnitsContainer()
def test_dimension_definition(self):
- x = DimensionDefinition("[time]", "", (), None, is_base=True)
+ x = DimensionDefinition("[time]")
assert x.is_base
assert x.name == "[time]"
@@ -170,7 +167,7 @@ class TestDefinition:
assert x.reference == UnitsContainer({"[length]": 1, "[time]": -1})
def test_alias_definition(self):
- x = AliasDefinition.from_string("@alias meter = metro = metr")
+ x = Definition.from_string("@alias meter = metro = metr")
assert isinstance(x, AliasDefinition)
assert x.name == "meter"
assert x.aliases == ("metro", "metr")
diff --git a/pint/testsuite/test_diskcache.py b/pint/testsuite/test_diskcache.py
index 22f5e8f..399f9f7 100644
--- a/pint/testsuite/test_diskcache.py
+++ b/pint/testsuite/test_diskcache.py
@@ -5,8 +5,8 @@ import time
import pytest
import pint
+from pint._vendor import flexparser as fp
from pint.facets.plain import UnitDefinition
-from pint.parser import DefinitionFile
FS_SLEEP = 0.010
@@ -53,11 +53,11 @@ def test_decimal(tmp_path, float_cache_filename):
for p in files:
with p.open(mode="rb") as fi:
obj = pickle.load(fi)
- if not isinstance(obj, DefinitionFile):
+ if not isinstance(obj, fp.ParsedSource):
continue
- for lineno, adef in obj.filter_by(UnitDefinition):
- if adef.name == "pi":
- assert isinstance(adef.converter.scale, decimal.Decimal)
+ for definition in obj.parsed_source.filter_by(UnitDefinition):
+ if definition.name == "pi":
+ assert isinstance(definition.converter.scale, decimal.Decimal)
return
assert False
diff --git a/pint/testsuite/test_errors.py b/pint/testsuite/test_errors.py
index cc19bef..6a42eec 100644
--- a/pint/testsuite/test_errors.py
+++ b/pint/testsuite/test_errors.py
@@ -21,42 +21,10 @@ class TestErrors:
ex = DefinitionSyntaxError("foo")
assert str(ex) == "foo"
- # filename and lineno can be attached after init
- ex.filename = "a.txt"
- ex.lineno = 123
- assert str(ex) == "While opening a.txt, in line 123: foo"
-
- ex = DefinitionSyntaxError("foo", lineno=123)
- assert str(ex) == "In line 123: foo"
-
- ex = DefinitionSyntaxError("foo", filename="a.txt")
- assert str(ex) == "While opening a.txt: foo"
-
- ex = DefinitionSyntaxError("foo", filename="a.txt", lineno=123)
- assert str(ex) == "While opening a.txt, in line 123: foo"
-
def test_redefinition_error(self):
ex = RedefinitionError("foo", "bar")
assert str(ex) == "Cannot redefine 'foo' (bar)"
- # filename and lineno can be attached after init
- ex.filename = "a.txt"
- ex.lineno = 123
- assert (
- str(ex) == "While opening a.txt, in line 123: Cannot redefine 'foo' (bar)"
- )
-
- ex = RedefinitionError("foo", "bar", lineno=123)
- assert str(ex) == "In line 123: Cannot redefine 'foo' (bar)"
-
- ex = RedefinitionError("foo", "bar", filename="a.txt")
- assert str(ex) == "While opening a.txt: Cannot redefine 'foo' (bar)"
-
- ex = RedefinitionError("foo", "bar", filename="a.txt", lineno=123)
- assert (
- str(ex) == "While opening a.txt, in line 123: Cannot redefine 'foo' (bar)"
- )
-
with pytest.raises(PintError):
raise ex
@@ -149,7 +117,7 @@ class TestErrors:
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
for ex in [
- DefinitionSyntaxError("foo", filename="a.txt", lineno=123),
+ DefinitionSyntaxError("foo"),
RedefinitionError("foo", "bar"),
UndefinedUnitError("meter"),
DimensionalityError("a", "b", "c", "d", extra_msg=": msg"),
@@ -165,9 +133,11 @@ class TestErrors:
# assert False, ex.__reduce__()
ex2 = pickle.loads(pickle.dumps(ex, protocol))
+ print(ex)
+ print(ex2)
assert type(ex) is type(ex2)
- assert ex.args == ex2.args
- assert ex.__dict__ == ex2.__dict__
+ assert ex == ex
+ # assert ex.__dict__ == ex2.__dict__
assert str(ex) == str(ex2)
with pytest.raises(PintError):
diff --git a/pint/testsuite/test_formatting.py b/pint/testsuite/test_formatting.py
index d287948..48e770b 100644
--- a/pint/testsuite/test_formatting.py
+++ b/pint/testsuite/test_formatting.py
@@ -52,3 +52,18 @@ def test_split_format(format, default, flag, expected):
result = fmt.split_format(format, default, flag)
assert result == expected
+
+
+def test_register_unit_format(func_registry):
+ @fmt.register_unit_format("custom")
+ def format_custom(unit, registry, **options):
+ return "<formatted unit>"
+
+ quantity = 1.0 * func_registry.meter
+ assert f"{quantity:custom}" == "1.0 <formatted unit>"
+
+ with pytest.raises(ValueError, match="format 'custom' already exists"):
+
+ @fmt.register_unit_format("custom")
+ def format_custom_redefined(unit, registry, **options):
+ return "<overwritten>"
diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py
index 51d33b1..8f16f81 100644
--- a/pint/testsuite/test_issues.py
+++ b/pint/testsuite/test_issues.py
@@ -1,4 +1,5 @@
import copy
+import decimal
import math
import pprint
@@ -7,13 +8,13 @@ import pytest
from pint import Context, DimensionalityError, UnitRegistry, get_application_registry
from pint.compat import np
from pint.facets.plain.unit import UnitsContainer
+from pint.testing import assert_equal
from pint.testsuite import QuantityTestCase, helpers
from pint.util import ParserHelper
# TODO: do not subclass from QuantityTestCase
class TestIssues(QuantityTestCase):
-
kwargs = dict(autoconvert_offset_to_baseunit=False)
@pytest.mark.xfail
@@ -248,7 +249,6 @@ class TestIssues(QuantityTestCase):
assert dis.value == acc.value * tim.value**2 / 2
def test_issue85(self, module_registry):
-
T = 4.0 * module_registry.kelvin
m = 1.0 * module_registry.amu
va = 2.0 * module_registry.k * T / m
@@ -261,7 +261,6 @@ class TestIssues(QuantityTestCase):
helpers.assert_quantity_almost_equal(va.to_base_units(), vb.to_base_units())
def test_issue86(self, module_registry):
-
module_registry.autoconvert_offset_to_baseunit = True
def parts(q):
@@ -333,7 +332,6 @@ class TestIssues(QuantityTestCase):
helpers.assert_quantity_almost_equal(z, 5.1 * module_registry.meter)
def test_issue104(self, module_registry):
-
x = [
module_registry("1 meter"),
module_registry("1 meter"),
@@ -360,7 +358,6 @@ class TestIssues(QuantityTestCase):
helpers.assert_quantity_almost_equal(y[0], module_registry.Quantity(1, "meter"))
def test_issue105(self, module_registry):
-
func = module_registry.parse_unit_name
val = list(func("meter"))
assert list(func("METER")) == []
@@ -472,7 +469,6 @@ class TestIssues(QuantityTestCase):
@helpers.requires_numpy
def test_issue483(self, module_registry):
-
a = np.asarray([1, 2, 3])
q = [1, 2, 3] * module_registry.dimensionless
p = (q**q).m
@@ -855,6 +851,29 @@ class TestIssues(QuantityTestCase):
np.array((0.04, 0.09)),
)
+ def test_issue1277(self, module_registry):
+ ureg = module_registry
+ assert ureg("%") == ureg("percent")
+ assert ureg("%") == ureg.percent
+ assert ureg("ppm") == ureg.ppm
+
+ a = ureg.Quantity("10 %")
+ b = ureg.Quantity("100 ppm")
+ c = ureg.Quantity("0.5")
+
+ assert f"{a}" == "10 percent"
+ assert f"{a:~}" == "10 %"
+ assert f"{b}" == "100 ppm"
+ assert f"{b:~}" == "100 ppm"
+
+ assert_equal(a, 0.1)
+ assert_equal(1000 * b, a)
+ assert_equal(c, 5 * a)
+
+ assert_equal((1 * ureg.meter) / (1 * ureg.kilometer), 0.1 * ureg.percent)
+ assert c.to("percent").m == 50
+ # assert c.to("%").m == 50 # TODO: fails.
+
@helpers.requires_uncertainties()
def test_issue_1300(self):
module_registry = UnitRegistry()
@@ -928,7 +947,7 @@ def test_issue1498(tmp_path):
f"""
foo = [FOO]
- @import {str(def2)}
+ @import {def2.name}
"""
)
@@ -941,9 +960,8 @@ def test_issue1498(tmp_path):
"""
)
- # Succeeds with pint 0.18; fails with pint 0.19
ureg1 = UnitRegistry()
- ureg1.load_definitions(def1) # ← FAILS
+ ureg1.load_definitions(def1)
assert 12.0 == ureg1("1.2 foo").to("kg", "BAR").magnitude
@@ -1009,3 +1027,49 @@ def test_issue1498b(tmp_path):
ureg1.load_definitions(def0) # ← FAILS
assert 12.0 == ureg1("1.2 foo").to("kg", "BAR").magnitude
+
+
+def test_backcompat_speed_velocity(func_registry):
+ get = func_registry.get_dimensionality
+ assert get("[velocity]") == UnitsContainer({"[length]": 1, "[time]": -1})
+ assert get("[speed]") == UnitsContainer({"[length]": 1, "[time]": -1})
+
+
+def test_issue1433(func_registry):
+ assert func_registry.Quantity("1 micron") == func_registry.Quantity("1 micrometer")
+
+
+def test_issue1527():
+ ureg = UnitRegistry(non_int_type=decimal.Decimal)
+ x = ureg.parse_expression("2 microliter milligram/liter")
+ assert x.magnitude.as_tuple()[1] == (2,)
+ assert x.to_compact().as_tuple()[1] == (2,)
+ assert x.to_base_units().as_tuple()[1] == (2,)
+ assert x.to("ng").as_tuple()[1] == (2,)
+
+
+def test_issue1621():
+ ureg = UnitRegistry(non_int_type=decimal.Decimal)
+ digits = ureg.Quantity("5.0 mV/m").to_base_units().magnitude.as_tuple()[1]
+ assert digits == (5, 0)
+
+
+def test_issue1631():
+ import pint
+
+ # Test registry subclassing
+ class MyRegistry(pint.UnitRegistry):
+ pass
+
+ assert MyRegistry.Quantity is pint.UnitRegistry.Quantity
+ assert MyRegistry.Unit is pint.UnitRegistry.Unit
+
+ ureg = MyRegistry()
+
+ u = ureg.meter
+ assert isinstance(u, ureg.Unit)
+ assert isinstance(u, pint.Unit)
+
+ q = 2 * ureg.meter
+ assert isinstance(q, ureg.Quantity)
+ assert isinstance(q, pint.Quantity)
diff --git a/pint/testsuite/test_log_units.py b/pint/testsuite/test_log_units.py
index f9dfe77..2a048f6 100644
--- a/pint/testsuite/test_log_units.py
+++ b/pint/testsuite/test_log_units.py
@@ -16,7 +16,6 @@ def module_registry_auto_offset():
# TODO: do not subclass from QuantityTestCase
class TestLogarithmicQuantity(QuantityTestCase):
def test_log_quantity_creation(self, caplog):
-
# Following Quantity Creation Pattern
for args in (
(4.2, "dBm"),
diff --git a/pint/testsuite/test_matplotlib.py b/pint/testsuite/test_matplotlib.py
index 25f3172..0735721 100644
--- a/pint/testsuite/test_matplotlib.py
+++ b/pint/testsuite/test_matplotlib.py
@@ -46,3 +46,21 @@ def test_plot_with_set_units(local_registry):
ax.axvline(120 * local_registry.minutes, color="tab:green")
return fig
+
+
+@pytest.mark.mpl_image_compare(tolerance=0, remove_text=True)
+def test_plot_with_non_default_format(local_registry):
+ local_registry.mpl_formatter = "{:~P}"
+
+ y = np.linspace(0, 30) * local_registry.miles
+ x = np.linspace(0, 5) * local_registry.hours
+
+ fig, ax = plt.subplots()
+ ax.yaxis.set_units(local_registry.inches)
+ ax.xaxis.set_units(local_registry.seconds)
+
+ ax.plot(x, y, "tab:blue")
+ ax.axhline(26400 * local_registry.feet, color="tab:red")
+ ax.axvline(120 * local_registry.minutes, color="tab:green")
+
+ return fig
diff --git a/pint/testsuite/test_measurement.py b/pint/testsuite/test_measurement.py
index 926b4d6..b78ca0e 100644
--- a/pint/testsuite/test_measurement.py
+++ b/pint/testsuite/test_measurement.py
@@ -193,7 +193,6 @@ class TestMeasurement(QuantityTestCase):
v.plus_minus(u, relative=True)
def test_propagate_linear(self):
-
v1, u1 = self.Q_(8.0, "s"), self.Q_(0.7, "s")
v2, u2 = self.Q_(5.0, "s"), self.Q_(0.6, "s")
v2, u3 = self.Q_(-5.0, "s"), self.Q_(0.6, "s")
@@ -241,7 +240,6 @@ class TestMeasurement(QuantityTestCase):
assert r.value.units == ml.value.units
def test_propagate_product(self):
-
v1, u1 = self.Q_(8.0, "s"), self.Q_(0.7, "s")
v2, u2 = self.Q_(5.0, "s"), self.Q_(0.6, "s")
v2, u3 = self.Q_(-5.0, "s"), self.Q_(0.6, "s")
diff --git a/pint/testsuite/test_non_int.py b/pint/testsuite/test_non_int.py
index 409d604..66637e1 100644
--- a/pint/testsuite/test_non_int.py
+++ b/pint/testsuite/test_non_int.py
@@ -12,40 +12,36 @@ from pint.facets.plain.unit import UnitsContainer
from pint.testsuite import QuantityTestCase, helpers
-class FakeWrapper:
- # Used in test_upcast_type_rejection_on_creation
- def __init__(self, q):
- self.q = q
-
-
# TODO: do not subclass from QuantityTestCase
-class NonIntTypeQuantityTestCase(QuantityTestCase):
+class NonIntTypeTestCase(QuantityTestCase):
def assert_quantity_almost_equal(
self, first, second, rtol="1e-07", atol="0", msg=None
):
-
if isinstance(first, self.Q_):
- assert isinstance(first.m, (self.NON_INT_TYPE, int))
+ assert isinstance(first.m, (self.kwargs["non_int_type"], int))
else:
- assert isinstance(first, (self.NON_INT_TYPE, int))
+ assert isinstance(first, (self.kwargs["non_int_type"], int))
if isinstance(second, self.Q_):
- assert isinstance(second.m, (self.NON_INT_TYPE, int))
+ assert isinstance(second.m, (self.kwargs["non_int_type"], int))
else:
- assert isinstance(second, (self.NON_INT_TYPE, int))
- super().assert_quantity_almost_equal(
- first, second, self.NON_INT_TYPE(rtol), self.NON_INT_TYPE(atol), msg
+ assert isinstance(second, (self.kwargs["non_int_type"], int))
+ helpers.assert_quantity_almost_equal(
+ first,
+ second,
+ self.kwargs["non_int_type"](rtol),
+ self.kwargs["non_int_type"](atol),
+ msg,
)
def QP_(self, value, units):
assert isinstance(value, str)
- return self.Q_(self.NON_INT_TYPE(value), units)
+ return self.Q_(self.kwargs["non_int_type"](value), units)
-class _TestBasic:
- def test_quantity_creation(self):
-
- value = self.NON_INT_TYPE("4.2")
+class _TestBasic(NonIntTypeTestCase):
+ def test_quantity_creation(self, caplog):
+ value = self.kwargs["non_int_type"]("4.2")
for args in (
(value, "meter"),
@@ -69,11 +65,36 @@ class _TestBasic:
assert x.magnitude == value
assert x.units == UnitsContainer()
- with self.capture_log() as buffer:
- assert value * self.ureg.meter == self.Q_(
- value, self.NON_INT_TYPE("2") * self.ureg.meter
- )
- assert len(buffer) == 1
+ caplog.clear()
+ assert value * self.ureg.meter == self.Q_(
+ value, self.kwargs["non_int_type"]("2") * self.ureg.meter
+ )
+ assert len(caplog.records) == 1
+ assert (
+ caplog.records[0].message
+ == "Creating new PlainQuantity using a non unity PlainQuantity as units."
+ )
+
+ def test_nan_creation(self):
+ if self.SUPPORTS_NAN:
+ value = self.kwargs["non_int_type"]("nan")
+
+ for args in (
+ (value, "meter"),
+ (value, UnitsContainer(meter=1)),
+ (value, self.ureg.meter),
+ ("NaN*meter",),
+ ("nan/meter**(-1)",),
+ (self.Q_(value, "meter"),),
+ ):
+ x = self.Q_(*args)
+ assert math.isnan(x.magnitude)
+ assert type(x.magnitude) == self.kwargs["non_int_type"]
+ assert x.units == self.ureg.UnitsContainer(meter=1)
+
+ else:
+ with pytest.raises(ValueError):
+ self.Q_("NaN meters")
def test_quantity_comparison(self):
x = self.QP_("4.2", "meter")
@@ -109,7 +130,8 @@ class _TestBasic:
def test_quantity_comparison_convert(self):
assert self.QP_("1000", "millimeter") == self.QP_("1", "meter")
assert self.QP_("1000", "millimeter/min") == self.Q_(
- self.NON_INT_TYPE("1000") / self.NON_INT_TYPE("60"), "millimeter/s"
+ self.kwargs["non_int_type"]("1000") / self.kwargs["non_int_type"]("60"),
+ "millimeter/s",
)
def test_quantity_hash(self):
@@ -123,44 +145,56 @@ class _TestBasic:
assert hash(y * z) == hash(1.0)
# Dimensionless equality from a different unit registry
- ureg2 = UnitRegistry(force_ndarray=self.FORCE_NDARRAY)
- y2 = ureg2.Quantity(self.NON_INT_TYPE("2"), "second")
- z2 = ureg2.Quantity(self.NON_INT_TYPE("0.5"), "hertz")
+ ureg2 = UnitRegistry()
+ y2 = ureg2.Quantity(self.kwargs["non_int_type"]("2"), "second")
+ z2 = ureg2.Quantity(self.kwargs["non_int_type"]("0.5"), "hertz")
assert hash(y * z) == hash(y2 * z2)
def test_to_base_units(self):
x = self.Q_("1*inch")
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
x.to_base_units(), self.QP_("0.0254", "meter")
)
x = self.Q_("1*inch*inch")
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
x.to_base_units(),
self.Q_(
- self.NON_INT_TYPE("0.0254") ** self.NON_INT_TYPE("2.0"), "meter*meter"
+ self.kwargs["non_int_type"]("0.0254")
+ ** self.kwargs["non_int_type"]("2.0"),
+ "meter*meter",
),
)
x = self.Q_("1*inch/minute")
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
x.to_base_units(),
self.Q_(
- self.NON_INT_TYPE("0.0254") / self.NON_INT_TYPE("60"), "meter/second"
+ self.kwargs["non_int_type"]("0.0254")
+ / self.kwargs["non_int_type"]("60"),
+ "meter/second",
),
)
def test_convert(self):
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.Q_("2 inch").to("meter"),
- self.Q_(self.NON_INT_TYPE("2") * self.NON_INT_TYPE("0.0254"), "meter"),
+ self.Q_(
+ self.kwargs["non_int_type"]("2")
+ * self.kwargs["non_int_type"]("0.0254"),
+ "meter",
+ ),
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.Q_("2 meter").to("inch"),
- self.Q_(self.NON_INT_TYPE("2") / self.NON_INT_TYPE("0.0254"), "inch"),
+ self.Q_(
+ self.kwargs["non_int_type"]("2")
+ / self.kwargs["non_int_type"]("0.0254"),
+ "inch",
+ ),
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.Q_("2 sidereal_year").to("second"), self.QP_("63116297.5325", "second")
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.Q_("2.54 centimeter/second").to("inch/second"),
self.Q_("1 inch/second"),
)
@@ -174,35 +208,41 @@ class _TestBasic:
meter = self.ureg.meter
# from quantity
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
meter.from_(x),
- self.Q_(self.NON_INT_TYPE("2") * self.NON_INT_TYPE("0.0254"), "meter"),
+ self.Q_(
+ self.kwargs["non_int_type"]("2")
+ * self.kwargs["non_int_type"]("0.0254"),
+ "meter",
+ ),
)
- helpers.assert_quantity_almost_equal(
- meter.m_from(x), self.NON_INT_TYPE("2") * self.NON_INT_TYPE("0.0254")
+ self.assert_quantity_almost_equal(
+ meter.m_from(x),
+ self.kwargs["non_int_type"]("2") * self.kwargs["non_int_type"]("0.0254"),
)
# from unit
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
meter.from_(self.ureg.inch), self.QP_("0.0254", "meter")
)
- helpers.assert_quantity_almost_equal(
- meter.m_from(self.ureg.inch), self.NON_INT_TYPE("0.0254")
+ self.assert_quantity_almost_equal(
+ meter.m_from(self.ureg.inch), self.kwargs["non_int_type"]("0.0254")
)
# from number
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
meter.from_(2, strict=False), self.QP_("2", "meter")
)
- helpers.assert_quantity_almost_equal(
- meter.m_from(self.NON_INT_TYPE("2"), strict=False), self.NON_INT_TYPE("2")
+ self.assert_quantity_almost_equal(
+ meter.m_from(self.kwargs["non_int_type"]("2"), strict=False),
+ self.kwargs["non_int_type"]("2"),
)
# from number (strict mode)
with pytest.raises(ValueError):
- meter.from_(self.NON_INT_TYPE("2"))
+ meter.from_(self.kwargs["non_int_type"]("2"))
with pytest.raises(ValueError):
- meter.m_from(self.NON_INT_TYPE("2"))
+ meter.m_from(self.kwargs["non_int_type"]("2"))
def test_context_attr(self):
assert self.ureg.meter == self.QP_("1", "meter")
@@ -212,7 +252,7 @@ class _TestBasic:
assert self.QP_("2", "cm") == self.QP_("2", "centimeter")
def test_dimensionless_units(self):
- twopi = self.NON_INT_TYPE("2") * self.ureg.pi
+ twopi = self.kwargs["non_int_type"]("2") * self.ureg.pi
assert (
round(abs(self.QP_("360", "degree").to("radian").magnitude - twopi), 7) == 0
)
@@ -230,121 +270,121 @@ class _TestBasic:
assert 7 // self.QP_("360", "degree") == 1
def test_offset(self):
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("0", "kelvin").to("kelvin"), self.QP_("0", "kelvin")
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("0", "degC").to("kelvin"), self.QP_("273.15", "kelvin")
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("0", "degF").to("kelvin"),
self.QP_("255.372222", "kelvin"),
rtol=0.01,
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("100", "kelvin").to("kelvin"), self.QP_("100", "kelvin")
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("100", "degC").to("kelvin"), self.QP_("373.15", "kelvin")
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("100", "degF").to("kelvin"),
self.QP_("310.92777777", "kelvin"),
rtol=0.01,
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("0", "kelvin").to("degC"), self.QP_("-273.15", "degC")
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("100", "kelvin").to("degC"), self.QP_("-173.15", "degC")
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("0", "kelvin").to("degF"), self.QP_("-459.67", "degF"), rtol=0.01
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("100", "kelvin").to("degF"), self.QP_("-279.67", "degF"), rtol=0.01
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("32", "degF").to("degC"), self.QP_("0", "degC"), atol=0.01
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("100", "degC").to("degF"), self.QP_("212", "degF"), atol=0.01
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("54", "degF").to("degC"), self.QP_("12.2222", "degC"), atol=0.01
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("12", "degC").to("degF"), self.QP_("53.6", "degF"), atol=0.01
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("12", "kelvin").to("degC"), self.QP_("-261.15", "degC"), atol=0.01
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("12", "degC").to("kelvin"), self.QP_("285.15", "kelvin"), atol=0.01
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("12", "kelvin").to("degR"), self.QP_("21.6", "degR"), atol=0.01
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("12", "degR").to("kelvin"),
self.QP_("6.66666667", "kelvin"),
atol=0.01,
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("12", "degC").to("degR"), self.QP_("513.27", "degR"), atol=0.01
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("12", "degR").to("degC"),
self.QP_("-266.483333", "degC"),
atol=0.01,
)
def test_offset_delta(self):
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("0", "delta_degC").to("kelvin"), self.QP_("0", "kelvin")
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("0", "delta_degF").to("kelvin"), self.QP_("0", "kelvin"), rtol=0.01
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("100", "kelvin").to("delta_degC"), self.QP_("100", "delta_degC")
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("100", "kelvin").to("delta_degF"),
self.QP_("180", "delta_degF"),
rtol=0.01,
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("100", "delta_degF").to("kelvin"),
self.QP_("55.55555556", "kelvin"),
rtol=0.01,
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("100", "delta_degC").to("delta_degF"),
self.QP_("180", "delta_degF"),
rtol=0.01,
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("100", "delta_degF").to("delta_degC"),
self.QP_("55.55555556", "delta_degC"),
rtol=0.01,
)
- helpers.assert_quantity_almost_equal(
+ self.assert_quantity_almost_equal(
self.QP_("12.3", "delta_degC").to("delta_degF"),
self.QP_("22.14", "delta_degF"),
rtol=0.01,
)
- def test_pickle(self):
+ def test_pickle(self, subtests):
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
for magnitude, unit in (
("32", ""),
@@ -352,7 +392,7 @@ class _TestBasic:
("32", "m/s"),
("2.4", "m/s"),
):
- with self.subTest(protocol=protocol, magnitude=magnitude, unit=unit):
+ with subtests.test(protocol=protocol, magnitude=magnitude, unit=unit):
q1 = self.QP_(magnitude, unit)
q2 = pickle.loads(pickle.dumps(q1, protocol))
assert q1 == q2
@@ -365,10 +405,7 @@ class _TestBasic:
iter(x)
-class _TestQuantityBasicMath:
-
- FORCE_NDARRAY = False
-
+class _TestQuantityBasicMath(NonIntTypeTestCase):
def _test_inplace(self, operator, value1, value2, expected_result, unit=None):
if isinstance(value1, str):
value1 = self.Q_(value1)
@@ -388,9 +425,9 @@ class _TestQuantityBasicMath:
id2 = id(value2)
value1 = operator(value1, value2)
value2_cpy = copy.copy(value2)
- helpers.assert_quantity_almost_equal(value1, expected_result)
+ self.assert_quantity_almost_equal(value1, expected_result)
assert id1 == id(value1)
- helpers.assert_quantity_almost_equal(value2, value2_cpy)
+ self.assert_quantity_almost_equal(value2, value2_cpy)
assert id2 == id(value2)
def _test_not_inplace(self, operator, value1, value2, expected_result, unit=None):
@@ -414,9 +451,9 @@ class _TestQuantityBasicMath:
result = operator(value1, value2)
- helpers.assert_quantity_almost_equal(expected_result, result)
- helpers.assert_quantity_almost_equal(value1, value1_cpy)
- helpers.assert_quantity_almost_equal(value2, value2_cpy)
+ self.assert_quantity_almost_equal(expected_result, result)
+ self.assert_quantity_almost_equal(value1, value1_cpy)
+ self.assert_quantity_almost_equal(value2, value2_cpy)
assert id(result) != id1
assert id(result) != id2
@@ -428,37 +465,43 @@ class _TestQuantityBasicMath:
func(op.add, x, x, self.Q_(unit + unit, "centimeter"))
func(
- op.add, x, y, self.Q_(unit + self.NON_INT_TYPE("2.54") * unit, "centimeter")
+ op.add,
+ x,
+ y,
+ self.Q_(unit + self.kwargs["non_int_type"]("2.54") * unit, "centimeter"),
)
func(
op.add,
y,
x,
- self.Q_(unit + unit / (self.NON_INT_TYPE("2.54") * unit), "inch"),
+ self.Q_(unit + unit / (self.kwargs["non_int_type"]("2.54") * unit), "inch"),
)
func(op.add, a, unit, self.Q_(unit + unit, None))
with pytest.raises(DimensionalityError):
- op.add(self.NON_INT_TYPE("10"), x)
+ op.add(self.kwargs["non_int_type"]("10"), x)
with pytest.raises(DimensionalityError):
- op.add(x, self.NON_INT_TYPE("10"))
+ op.add(x, self.kwargs["non_int_type"]("10"))
with pytest.raises(DimensionalityError):
op.add(x, z)
func(op.sub, x, x, self.Q_(unit - unit, "centimeter"))
func(
- op.sub, x, y, self.Q_(unit - self.NON_INT_TYPE("2.54") * unit, "centimeter")
+ op.sub,
+ x,
+ y,
+ self.Q_(unit - self.kwargs["non_int_type"]("2.54") * unit, "centimeter"),
)
func(
op.sub,
y,
x,
- self.Q_(unit - unit / (self.NON_INT_TYPE("2.54") * unit), "inch"),
+ self.Q_(unit - unit / (self.kwargs["non_int_type"]("2.54") * unit), "inch"),
)
func(op.sub, a, unit, self.Q_(unit - unit, None))
with pytest.raises(DimensionalityError):
- op.sub(self.NON_INT_TYPE("10"), x)
+ op.sub(self.kwargs["non_int_type"]("10"), x)
with pytest.raises(DimensionalityError):
- op.sub(x, self.NON_INT_TYPE("10"))
+ op.sub(x, self.kwargs["non_int_type"]("10"))
with pytest.raises(DimensionalityError):
op.sub(x, z)
@@ -473,45 +516,86 @@ class _TestQuantityBasicMath:
op.iadd,
x,
y,
- self.Q_(unit + self.NON_INT_TYPE("2.54") * unit, "centimeter"),
+ self.Q_(unit + self.kwargs["non_int_type"]("2.54") * unit, "centimeter"),
+ )
+ func(
+ op.iadd,
+ y,
+ x,
+ self.Q_(unit + unit / self.kwargs["non_int_type"]("2.54"), "inch"),
)
- func(op.iadd, y, x, self.Q_(unit + unit / self.NON_INT_TYPE("2.54"), "inch"))
func(op.iadd, a, unit, self.Q_(unit + unit, None))
with pytest.raises(DimensionalityError):
- op.iadd(self.NON_INT_TYPE("10"), x)
+ op.iadd(self.kwargs["non_int_type"]("10"), x)
with pytest.raises(DimensionalityError):
- op.iadd(x, self.NON_INT_TYPE("10"))
+ op.iadd(x, self.kwargs["non_int_type"]("10"))
with pytest.raises(DimensionalityError):
op.iadd(x, z)
func(op.isub, x, x, self.Q_(unit - unit, "centimeter"))
- func(op.isub, x, y, self.Q_(unit - self.NON_INT_TYPE("2.54"), "centimeter"))
- func(op.isub, y, x, self.Q_(unit - unit / self.NON_INT_TYPE("2.54"), "inch"))
+ func(
+ op.isub,
+ x,
+ y,
+ self.Q_(unit - self.kwargs["non_int_type"]("2.54"), "centimeter"),
+ )
+ func(
+ op.isub,
+ y,
+ x,
+ self.Q_(unit - unit / self.kwargs["non_int_type"]("2.54"), "inch"),
+ )
func(op.isub, a, unit, self.Q_(unit - unit, None))
with pytest.raises(DimensionalityError):
- op.sub(self.NON_INT_TYPE("10"), x)
+ op.sub(self.kwargs["non_int_type"]("10"), x)
with pytest.raises(DimensionalityError):
- op.sub(x, self.NON_INT_TYPE("10"))
+ op.sub(x, self.kwargs["non_int_type"]("10"))
with pytest.raises(DimensionalityError):
op.sub(x, z)
def _test_quantity_mul_div(self, unit, func):
- func(op.mul, unit * self.NON_INT_TYPE("10"), "4.2*meter", "42*meter", unit)
- func(op.mul, "4.2*meter", unit * self.NON_INT_TYPE("10"), "42*meter", unit)
+ func(
+ op.mul,
+ unit * self.kwargs["non_int_type"]("10"),
+ "4.2*meter",
+ "42*meter",
+ unit,
+ )
+ func(
+ op.mul,
+ "4.2*meter",
+ unit * self.kwargs["non_int_type"]("10"),
+ "42*meter",
+ unit,
+ )
func(op.mul, "4.2*meter", "10*inch", "42*meter*inch", unit)
- func(op.truediv, unit * self.NON_INT_TYPE("42"), "4.2*meter", "10/meter", unit)
func(
- op.truediv, "4.2*meter", unit * self.NON_INT_TYPE("10"), "0.42*meter", unit
+ op.truediv,
+ unit * self.kwargs["non_int_type"]("42"),
+ "4.2*meter",
+ "10/meter",
+ unit,
+ )
+ func(
+ op.truediv,
+ "4.2*meter",
+ unit * self.kwargs["non_int_type"]("10"),
+ "0.42*meter",
+ unit,
)
func(op.truediv, "4.2*meter", "10*inch", "0.42*meter/inch", unit)
def _test_quantity_imul_idiv(self, unit, func):
# func(op.imul, 10.0, '4.2*meter', '42*meter')
- func(op.imul, "4.2*meter", self.NON_INT_TYPE("10"), "42*meter", unit)
+ func(op.imul, "4.2*meter", self.kwargs["non_int_type"]("10"), "42*meter", unit)
func(op.imul, "4.2*meter", "10*inch", "42*meter*inch", unit)
# func(op.truediv, 42, '4.2*meter', '10/meter')
func(
- op.itruediv, "4.2*meter", unit * self.NON_INT_TYPE("10"), "0.42*meter", unit
+ op.itruediv,
+ "4.2*meter",
+ unit * self.kwargs["non_int_type"]("10"),
+ "0.42*meter",
+ unit,
)
func(op.itruediv, "4.2*meter", "10*inch", "0.42*meter/inch", unit)
@@ -521,23 +605,25 @@ class _TestQuantityBasicMath:
with pytest.raises(DimensionalityError):
op.floordiv(a, b)
with pytest.raises(DimensionalityError):
- op.floordiv(self.NON_INT_TYPE("3"), b)
+ op.floordiv(self.kwargs["non_int_type"]("3"), b)
with pytest.raises(DimensionalityError):
- op.floordiv(a, self.NON_INT_TYPE("3"))
+ op.floordiv(a, self.kwargs["non_int_type"]("3"))
with pytest.raises(DimensionalityError):
op.ifloordiv(a, b)
with pytest.raises(DimensionalityError):
- op.ifloordiv(self.NON_INT_TYPE("3"), b)
+ op.ifloordiv(self.kwargs["non_int_type"]("3"), b)
with pytest.raises(DimensionalityError):
- op.ifloordiv(a, self.NON_INT_TYPE("3"))
+ op.ifloordiv(a, self.kwargs["non_int_type"]("3"))
func(
op.floordiv,
- unit * self.NON_INT_TYPE("10"),
+ unit * self.kwargs["non_int_type"]("10"),
"4.2*meter/meter",
- self.NON_INT_TYPE("2"),
+ self.kwargs["non_int_type"]("2"),
unit,
)
- func(op.floordiv, "10*meter", "4.2*inch", self.NON_INT_TYPE("93"), unit)
+ func(
+ op.floordiv, "10*meter", "4.2*inch", self.kwargs["non_int_type"]("93"), unit
+ )
def _test_quantity_mod(self, unit, func):
a = self.Q_("10*meter")
@@ -556,21 +642,27 @@ class _TestQuantityBasicMath:
op.imod(a, 3)
func(
op.mod,
- unit * self.NON_INT_TYPE("10"),
+ unit * self.kwargs["non_int_type"]("10"),
"4.2*meter/meter",
- self.NON_INT_TYPE("1.6"),
+ self.kwargs["non_int_type"]("1.6"),
unit,
)
def _test_quantity_ifloordiv(self, unit, func):
func(
op.ifloordiv,
- self.NON_INT_TYPE("10"),
+ self.kwargs["non_int_type"]("10"),
"4.2*meter/meter",
- self.NON_INT_TYPE("2"),
+ self.kwargs["non_int_type"]("2"),
+ unit,
+ )
+ func(
+ op.ifloordiv,
+ "10*meter",
+ "4.2*inch",
+ self.kwargs["non_int_type"]("93"),
unit,
)
- func(op.ifloordiv, "10*meter", "4.2*inch", self.NON_INT_TYPE("93"), unit)
def _test_quantity_divmod_one(self, a, b):
if isinstance(a, str):
@@ -639,8 +731,7 @@ class _TestQuantityBasicMath:
# self._test_quantity_ifloordiv(unit, ifunc)
def test_quantity_abs_round(self):
-
- value = self.NON_INT_TYPE("4.2")
+ value = self.kwargs["non_int_type"]("4.2")
x = self.Q_(-value, "meter")
y = self.Q_(value, "meter")
@@ -665,12 +756,13 @@ class _TestQuantityBasicMath:
fun(z)
def test_not_inplace(self):
- self._test_numeric(self.NON_INT_TYPE("1.0"), self._test_not_inplace)
+ self._test_numeric(self.kwargs["non_int_type"]("1.0"), self._test_not_inplace)
-class _TestOffsetUnitMath:
+class _TestOffsetUnitMath(NonIntTypeTestCase):
@classmethod
def setup_class(cls):
+ super().setup_class()
cls.ureg.autoconvert_offset_to_baseunit = False
cls.ureg.default_as_delta = True
@@ -714,20 +806,22 @@ class _TestOffsetUnitMath:
((("100", "delta_degF"), ("10", "delta_degF")), ("110", "delta_degF")),
]
- @pytest.mark.parametrize(("input", "expected_output"), additions)
- def test_addition(self, input_tuple, expected):
+ @pytest.mark.parametrize(("input_tuple", "expected_output"), additions)
+ def test_addition(self, input_tuple, expected_output):
self.ureg.autoconvert_offset_to_baseunit = False
qin1, qin2 = input_tuple
q1, q2 = self.QP_(*qin1), self.QP_(*qin2)
# update input tuple with new values to have correct values on failure
input_tuple = q1, q2
- if expected == "error":
+ if expected_output == "error":
with pytest.raises(OffsetUnitCalculusError):
op.add(q1, q2)
else:
- expected = self.QP_(*expected)
- assert op.add(q1, q2).units == expected.units
- helpers.assert_quantity_almost_equal(op.add(q1, q2), expected, atol="0.01")
+ expected_output = self.QP_(*expected_output)
+ assert op.add(q1, q2).units == expected_output.units
+ self.assert_quantity_almost_equal(
+ op.add(q1, q2), expected_output, atol="0.01"
+ )
subtractions = [
((("100", "kelvin"), ("10", "kelvin")), ("90", "kelvin")),
@@ -768,19 +862,21 @@ class _TestOffsetUnitMath:
((("100", "delta_degF"), ("10", "delta_degF")), ("90", "delta_degF")),
]
- @pytest.mark.parametrize(("input", "expected_output"), subtractions)
- def test_subtraction(self, input_tuple, expected):
+ @pytest.mark.parametrize(("input_tuple", "expected_output"), subtractions)
+ def test_subtraction(self, input_tuple, expected_output):
self.ureg.autoconvert_offset_to_baseunit = False
qin1, qin2 = input_tuple
q1, q2 = self.QP_(*qin1), self.QP_(*qin2)
input_tuple = q1, q2
- if expected == "error":
+ if expected_output == "error":
with pytest.raises(OffsetUnitCalculusError):
op.sub(q1, q2)
else:
- expected = self.QP_(*expected)
- assert op.sub(q1, q2).units == expected.units
- helpers.assert_quantity_almost_equal(op.sub(q1, q2), expected, atol=0.01)
+ expected_output = self.QP_(*expected_output)
+ assert op.sub(q1, q2).units == expected_output.units
+ self.assert_quantity_almost_equal(
+ op.sub(q1, q2), expected_output, atol=0.01
+ )
multiplications = [
((("100", "kelvin"), ("10", "kelvin")), ("1000", "kelvin**2")),
@@ -827,19 +923,21 @@ class _TestOffsetUnitMath:
((("100", "delta_degF"), ("10", "delta_degF")), ("1000", "delta_degF**2")),
]
- @pytest.mark.parametrize(("input", "expected_output"), multiplications)
- def test_multiplication(self, input_tuple, expected):
+ @pytest.mark.parametrize(("input_tuple", "expected_output"), multiplications)
+ def test_multiplication(self, input_tuple, expected_output):
self.ureg.autoconvert_offset_to_baseunit = False
qin1, qin2 = input_tuple
q1, q2 = self.QP_(*qin1), self.QP_(*qin2)
input_tuple = q1, q2
- if expected == "error":
+ if expected_output == "error":
with pytest.raises(OffsetUnitCalculusError):
op.mul(q1, q2)
else:
- expected = self.QP_(*expected)
- assert op.mul(q1, q2).units == expected.units
- helpers.assert_quantity_almost_equal(op.mul(q1, q2), expected, atol=0.01)
+ expected_output = self.QP_(*expected_output)
+ assert op.mul(q1, q2).units == expected_output.units
+ self.assert_quantity_almost_equal(
+ op.mul(q1, q2), expected_output, atol=0.01
+ )
divisions = [
((("100", "kelvin"), ("10", "kelvin")), ("10", "")),
@@ -886,20 +984,20 @@ class _TestOffsetUnitMath:
((("100", "delta_degF"), ("10", "delta_degF")), ("10", "")),
]
- @pytest.mark.parametrize(("input", "expected_output"), divisions)
- def test_truedivision(self, input_tuple, expected):
+ @pytest.mark.parametrize(("input_tuple", "expected_output"), divisions)
+ def test_truedivision(self, input_tuple, expected_output):
self.ureg.autoconvert_offset_to_baseunit = False
qin1, qin2 = input_tuple
q1, q2 = self.QP_(*qin1), self.QP_(*qin2)
input_tuple = q1, q2
- if expected == "error":
+ if expected_output == "error":
with pytest.raises(OffsetUnitCalculusError):
op.truediv(q1, q2)
else:
- expected = self.QP_(*expected)
- assert op.truediv(q1, q2).units == expected.units
- helpers.assert_quantity_almost_equal(
- op.truediv(q1, q2), expected, atol=0.01
+ expected_output = self.QP_(*expected_output)
+ assert op.truediv(q1, q2).units == expected_output.units
+ self.assert_quantity_almost_equal(
+ op.truediv(q1, q2), expected_output, atol=0.01
)
multiplications_with_autoconvert_to_baseunit = [
@@ -926,20 +1024,22 @@ class _TestOffsetUnitMath:
]
@pytest.mark.parametrize(
- ("input", "expected_output"), multiplications_with_autoconvert_to_baseunit
+ ("input_tuple", "expected_output"), multiplications_with_autoconvert_to_baseunit
)
- def test_multiplication_with_autoconvert(self, input_tuple, expected):
+ def test_multiplication_with_autoconvert(self, input_tuple, expected_output):
self.ureg.autoconvert_offset_to_baseunit = True
qin1, qin2 = input_tuple
q1, q2 = self.QP_(*qin1), self.QP_(*qin2)
input_tuple = q1, q2
- if expected == "error":
+ if expected_output == "error":
with pytest.raises(OffsetUnitCalculusError):
op.mul(q1, q2)
else:
- expected = self.QP_(*expected)
- assert op.mul(q1, q2).units == expected.units
- helpers.assert_quantity_almost_equal(op.mul(q1, q2), expected, atol=0.01)
+ expected_output = self.QP_(*expected_output)
+ assert op.mul(q1, q2).units == expected_output.units
+ self.assert_quantity_almost_equal(
+ op.mul(q1, q2), expected_output, atol=0.01
+ )
multiplications_with_scalar = [
((("10", "kelvin"), "2"), ("20.0", "kelvin")),
@@ -951,23 +1051,25 @@ class _TestOffsetUnitMath:
((("10", "degC**-2"), "2"), "error"),
]
- @pytest.mark.parametrize(("input", "expected_output"), multiplications_with_scalar)
- def test_multiplication_with_scalar(self, input_tuple, expected):
+ @pytest.mark.parametrize(
+ ("input_tuple", "expected_output"), multiplications_with_scalar
+ )
+ def test_multiplication_with_scalar(self, input_tuple, expected_output):
self.ureg.default_as_delta = False
in1, in2 = input_tuple
if type(in1) is tuple:
- in1, in2 = self.QP_(*in1), self.NON_INT_TYPE(in2)
+ in1, in2 = self.QP_(*in1), self.kwargs["non_int_type"](in2)
else:
in1, in2 = in1, self.QP_(*in2)
input_tuple = in1, in2 # update input_tuple for better tracebacks
- if expected == "error":
+ if expected_output == "error":
with pytest.raises(OffsetUnitCalculusError):
op.mul(in1, in2)
else:
- expected = self.QP_(*expected)
- assert op.mul(in1, in2).units == expected.units
- helpers.assert_quantity_almost_equal(
- op.mul(in1, in2), expected, atol="0.01"
+ expected_output = self.QP_(*expected_output)
+ assert op.mul(in1, in2).units == expected_output.units
+ self.assert_quantity_almost_equal(
+ op.mul(in1, in2), expected_output, atol="0.01"
)
divisions_with_scalar = [ # without / with autoconvert to plain unit
@@ -982,25 +1084,25 @@ class _TestOffsetUnitMath:
(("2", ("10", "degC**-2")), ["error", "error"]),
]
- @pytest.mark.parametrize(("input", "expected_output"), divisions_with_scalar)
- def test_division_with_scalar(self, input_tuple, expected):
+ @pytest.mark.parametrize(("input_tuple", "expected_output"), divisions_with_scalar)
+ def test_division_with_scalar(self, input_tuple, expected_output):
self.ureg.default_as_delta = False
in1, in2 = input_tuple
if type(in1) is tuple:
- in1, in2 = self.QP_(*in1), self.NON_INT_TYPE(in2)
+ in1, in2 = self.QP_(*in1), self.kwargs["non_int_type"](in2)
else:
- in1, in2 = self.NON_INT_TYPE(in1), self.QP_(*in2)
+ in1, in2 = self.kwargs["non_int_type"](in1), self.QP_(*in2)
input_tuple = in1, in2 # update input_tuple for better tracebacks
- expected_copy = expected[:]
+ expected_copy = expected_output[:]
for i, mode in enumerate([False, True]):
self.ureg.autoconvert_offset_to_baseunit = mode
if expected_copy[i] == "error":
with pytest.raises(OffsetUnitCalculusError):
op.truediv(in1, in2)
else:
- expected = self.QP_(*expected_copy[i])
- assert op.truediv(in1, in2).units == expected.units
- helpers.assert_quantity_almost_equal(op.truediv(in1, in2), expected)
+ expected_output = self.QP_(*expected_copy[i])
+ assert op.truediv(in1, in2).units == expected_output.units
+ self.assert_quantity_almost_equal(op.truediv(in1, in2), expected_output)
exponentiation = [ # results without / with autoconvert
((("10", "degC"), "1"), [("10", "degC"), ("10", "degC")]),
@@ -1024,18 +1126,18 @@ class _TestOffsetUnitMath:
# ),
]
- @pytest.mark.parametrize(("input", "expected_output"), exponentiation)
- def test_exponentiation(self, input_tuple, expected):
+ @pytest.mark.parametrize(("input_tuple", "expected_output"), exponentiation)
+ def test_exponentiation(self, input_tuple, expected_output):
self.ureg.default_as_delta = False
in1, in2 = input_tuple
if type(in1) is tuple and type(in2) is tuple:
in1, in2 = self.QP_(*in1), self.QP_(*in2)
- elif not type(in1) is tuple and type(in2) is tuple:
- in1, in2 = self.NON_INT_TYPE(in1), self.QP_(*in2)
+ elif type(in1) is not tuple and type(in2) is tuple:
+ in1, in2 = self.kwargs["non_int_type"](in1), self.QP_(*in2)
else:
- in1, in2 = self.QP_(*in1), self.NON_INT_TYPE(in2)
+ in1, in2 = self.QP_(*in1), self.kwargs["non_int_type"](in2)
input_tuple = in1, in2
- expected_copy = expected[:]
+ expected_copy = expected_output[:]
for i, mode in enumerate([False, True]):
self.ureg.autoconvert_offset_to_baseunit = mode
if expected_copy[i] == "error":
@@ -1043,65 +1145,47 @@ class _TestOffsetUnitMath:
op.pow(in1, in2)
else:
if type(expected_copy[i]) is tuple:
- expected = self.QP_(*expected_copy[i])
- assert op.pow(in1, in2).units == expected.units
+ expected_output = self.QP_(*expected_copy[i])
+ assert op.pow(in1, in2).units == expected_output.units
else:
- expected = expected_copy[i]
- helpers.assert_quantity_almost_equal(op.pow(in1, in2), expected)
-
+ expected_output = expected_copy[i]
+ self.assert_quantity_almost_equal(op.pow(in1, in2), expected_output)
-class NonIntTypeQuantityTestQuantityFloat(_TestBasic, NonIntTypeQuantityTestCase):
+class TestNonIntTypeQuantityFloat(_TestBasic):
kwargs = dict(non_int_type=float)
+ SUPPORTS_NAN = True
-class NonIntTypeQuantityTestQuantityBasicMathFloat(
- _TestQuantityBasicMath, NonIntTypeQuantityTestCase
-):
-
+class TestNonIntTypeQuantityBasicMathFloat(_TestQuantityBasicMath):
kwargs = dict(non_int_type=float)
-class NonIntTypeQuantityTestOffsetUnitMathFloat(
- _TestOffsetUnitMath, NonIntTypeQuantityTestCase
-):
-
+class TestNonIntTypeOffsetUnitMathFloat(_TestOffsetUnitMath):
kwargs = dict(non_int_type=float)
-class NonIntTypeQuantityTestQuantityDecimal(_TestBasic, NonIntTypeQuantityTestCase):
-
+class TestNonIntTypeQuantityDecimal(_TestBasic):
kwargs = dict(non_int_type=Decimal)
+ SUPPORTS_NAN = True
-class NonIntTypeQuantityTestQuantityBasicMathDecimal(
- _TestQuantityBasicMath, NonIntTypeQuantityTestCase
-):
-
+class TestNonIntTypeQuantityBasicMathDecimal(_TestQuantityBasicMath):
kwargs = dict(non_int_type=Decimal)
-class NonIntTypeQuantityTestOffsetUnitMathDecimal(
- _TestOffsetUnitMath, NonIntTypeQuantityTestCase
-):
-
+class TestNonIntTypeOffsetUnitMathDecimal(_TestOffsetUnitMath):
kwargs = dict(non_int_type=Decimal)
-class NonIntTypeQuantityTestQuantityFraction(_TestBasic, NonIntTypeQuantityTestCase):
-
+class TestNonIntTypeQuantityFraction(_TestBasic):
kwargs = dict(non_int_type=Fraction)
+ SUPPORTS_NAN = False
-class NonIntTypeQuantityTestQuantityBasicMathFraction(
- _TestQuantityBasicMath, NonIntTypeQuantityTestCase
-):
-
+class TestNonIntTypeQuantityBasicMathFraction(_TestQuantityBasicMath):
kwargs = dict(non_int_type=Fraction)
-class NonIntTypeQuantityTestOffsetUnitMathFraction(
- _TestOffsetUnitMath, NonIntTypeQuantityTestCase
-):
-
+class TestNonIntTypeOffsetUnitMathFraction(_TestOffsetUnitMath):
kwargs = dict(non_int_type=Fraction)
diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py
index f2ddaf0..080c60a 100644
--- a/pint/testsuite/test_numpy.py
+++ b/pint/testsuite/test_numpy.py
@@ -82,7 +82,7 @@ class TestNumpyArrayManipulation(TestNumpyMethods):
# TODO
# https://www.numpy.org/devdocs/reference/routines.array-manipulation.html
# copyto
- # broadcast , broadcast_arrays
+ # broadcast
# asarray asanyarray asmatrix asfarray asfortranarray ascontiguousarray asarray_chkfinite asscalar require
# Changing array shape
@@ -222,7 +222,6 @@ class TestNumpyArrayManipulation(TestNumpyMethods):
def test_block_column_stack(self, subtests):
for func in (np.block, np.column_stack):
with subtests.test(func=func):
-
helpers.assert_quantity_equal(
func([self.q[:, 0], self.q[:, 1]]),
self.Q_(func([self.q[:, 0].m, self.q[:, 1].m]), self.ureg.m),
@@ -271,6 +270,22 @@ class TestNumpyArrayManipulation(TestNumpyMethods):
def test_item(self):
helpers.assert_quantity_equal(self.Q_([[0]], "m").item(), 0 * self.ureg.m)
+ def test_broadcast_arrays(self):
+ x = self.Q_(np.array([[1, 2, 3]]), "m")
+ y = self.Q_(np.array([[4], [5]]), "nm")
+ result = np.broadcast_arrays(x, y)
+ expected = self.Q_(
+ [
+ [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]],
+ [[4e-09, 4e-09, 4e-09], [5e-09, 5e-09, 5e-09]],
+ ],
+ "m",
+ )
+ helpers.assert_quantity_equal(result, expected)
+
+ result = np.broadcast_arrays(x, y, subok=True)
+ helpers.assert_quantity_equal(result, expected)
+
class TestNumpyMathematicalFunctions(TestNumpyMethods):
# https://www.numpy.org/devdocs/reference/routines.math.html
@@ -790,7 +805,7 @@ class TestNumpyUnclassified(TestNumpyMethods):
np.around(1.0275 * self.ureg.m, decimals=2), 1.03 * self.ureg.m
)
helpers.assert_quantity_equal(
- np.round_(1.0275 * self.ureg.m, decimals=2), 1.03 * self.ureg.m
+ np.round(1.0275 * self.ureg.m, decimals=2), 1.03 * self.ureg.m
)
def test_trace(self):
@@ -1034,7 +1049,7 @@ class TestNumpyUnclassified(TestNumpyMethods):
np.isclose(self.q, q2), np.array([[False, True], [True, False]])
)
self.assertNDArrayEqual(
- np.isclose(self.q, q2, atol=1e-5, rtol=1e-7),
+ np.isclose(self.q, q2, atol=1e-5 * self.ureg.mm, rtol=1e-7),
np.array([[False, True], [True, False]]),
)
@@ -1103,6 +1118,18 @@ class TestNumpyUnclassified(TestNumpyMethods):
0 * self.ureg.J,
)
+ helpers.assert_quantity_equal(
+ np.where([-1, 0, 1] * self.ureg.m, [1, 2, 1] * self.ureg.s, np.nan),
+ [1, np.nan, 1] * self.ureg.s,
+ )
+ with pytest.raises(
+ ValueError,
+ match=".*Boolean value of Quantity with offset unit is ambiguous",
+ ):
+ np.where(
+ self.ureg.Quantity([-1, 0, 1], "degC"), [1, 2, 1] * self.ureg.s, np.nan
+ )
+
@helpers.requires_array_function_protocol()
def test_fabs(self):
helpers.assert_quantity_equal(
@@ -1194,6 +1221,24 @@ class TestNumpyUnclassified(TestNumpyMethods):
np.array([[1, 0, 2], [3, 0, 4]]) * self.ureg.m,
)
+ @helpers.requires_array_function_protocol()
+ def test_delete(self):
+ q = self.Q_(np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]), "m")
+ helpers.assert_quantity_equal(
+ np.delete(q, 1, axis=0),
+ np.array([[1, 2, 3, 4], [9, 10, 11, 12]]) * self.ureg.m,
+ )
+
+ helpers.assert_quantity_equal(
+ np.delete(q, np.s_[::2], 1),
+ np.array([[2, 4], [6, 8], [10, 12]]) * self.ureg.m,
+ )
+
+ helpers.assert_quantity_equal(
+ np.delete(q, [1, 3, 5], None),
+ np.array([1, 3, 5, 7, 8, 9, 10, 11, 12]) * self.ureg.m,
+ )
+
def test_ndarray_downcast(self):
with pytest.warns(UnitStrippedWarning):
np.asarray(self.q)
@@ -1327,6 +1372,16 @@ class TestNumpyUnclassified(TestNumpyMethods):
assert not np.allclose(
[1e10, 1e-8] * self.ureg.m, [1.00001e10, 1e-9] * self.ureg.mm
)
+ assert np.allclose(
+ [1e10, 1e-8] * self.ureg.m,
+ [1.00001e10, 1e-9] * self.ureg.m,
+ atol=1e-8 * self.ureg.m,
+ )
+
+ with pytest.raises(DimensionalityError):
+ assert np.allclose(
+ [1e10, 1e-8] * self.ureg.m, [1.00001e10, 1e-9] * self.ureg.m, atol=1e-8
+ )
@helpers.requires_array_function_protocol()
def test_intersect1d(self):
diff --git a/pint/testsuite/test_pint_eval.py b/pint/testsuite/test_pint_eval.py
index bed8105..b5b94f0 100644
--- a/pint/testsuite/test_pint_eval.py
+++ b/pint/testsuite/test_pint_eval.py
@@ -2,10 +2,13 @@ import pytest
from pint.compat import tokenizer
from pint.pint_eval import build_eval_tree
+from pint.util import string_preprocessor
class TestPintEval:
- def _test_one(self, input_text, parsed):
+ def _test_one(self, input_text, parsed, preprocess=False):
+ if preprocess:
+ input_text = string_preprocessor(input_text)
assert build_eval_tree(tokenizer(input_text)).to_string() == parsed
@pytest.mark.parametrize(
@@ -13,6 +16,7 @@ class TestPintEval:
(
("3", "3"),
("1 + 2", "(1 + 2)"),
+ ("1 - 2", "(1 - 2)"),
("2 * 3 + 4", "((2 * 3) + 4)"), # order of operations
("2 * (3 + 4)", "(2 * (3 + 4))"), # parentheses
(
@@ -71,4 +75,70 @@ class TestPintEval:
),
)
def test_build_eval_tree(self, input_text, parsed):
- self._test_one(input_text, parsed)
+ self._test_one(input_text, parsed, preprocess=False)
+
+ @pytest.mark.parametrize(
+ ("input_text", "parsed"),
+ (
+ ("3", "3"),
+ ("1 + 2", "(1 + 2)"),
+ ("1 - 2", "(1 - 2)"),
+ ("2 * 3 + 4", "((2 * 3) + 4)"), # order of operations
+ ("2 * (3 + 4)", "(2 * (3 + 4))"), # parentheses
+ (
+ "1 + 2 * 3 ** (4 + 3 / 5)",
+ "(1 + (2 * (3 ** (4 + (3 / 5)))))",
+ ), # more order of operations
+ (
+ "1 * ((3 + 4) * 5)",
+ "(1 * ((3 + 4) * 5))",
+ ), # nested parentheses at beginning
+ ("1 * (5 * (3 + 4))", "(1 * (5 * (3 + 4)))"), # nested parentheses at end
+ (
+ "1 * (5 * (3 + 4) / 6)",
+ "(1 * ((5 * (3 + 4)) / 6))",
+ ), # nested parentheses in middle
+ ("-1", "(- 1)"), # unary
+ ("3 * -1", "(3 * (- 1))"), # unary
+ ("3 * --1", "(3 * (- (- 1)))"), # double unary
+ ("3 * -(2 + 4)", "(3 * (- (2 + 4)))"), # parenthetical unary
+ ("3 * -((2 + 4))", "(3 * (- (2 + 4)))"), # parenthetical unary
+ # implicit op
+ ("3 4", "(3 * 4)"),
+ # implicit op, then parentheses
+ ("3 (2 + 4)", "(3 * (2 + 4))"),
+ # parentheses, then implicit
+ ("(3 ** 4 ) 5", "((3 ** 4) * 5)"),
+ # implicit op, then exponentiation
+ ("3 4 ** 5", "(3 * (4 ** 5))"),
+ # implicit op, then addition
+ ("3 4 + 5", "((3 * 4) + 5)"),
+ # power followed by implicit
+ ("3 ** 4 5", "((3 ** 4) * 5)"),
+ # implicit with parentheses
+ ("3 (4 ** 5)", "(3 * (4 ** 5))"),
+ # exponent with e
+ ("3e-1", "3e-1"),
+ # multiple units with exponents
+ ("kg ** 1 * s ** 2", "((kg ** 1) * (s ** 2))"),
+ # multiple units with neg exponents
+ ("kg ** -1 * s ** -2", "((kg ** (- 1)) * (s ** (- 2)))"),
+ # multiple units with neg exponents
+ ("kg^-1 * s^-2", "((kg ** (- 1)) * (s ** (- 2)))"),
+ # multiple units with neg exponents, implicit op
+ ("kg^-1 s^-2", "((kg ** (- 1)) * (s ** (- 2)))"),
+ # nested power
+ ("2 ^ 3 ^ 2", "(2 ** (3 ** 2))"),
+ # nested power
+ ("gram * second / meter ** 2", "((gram * second) / (meter ** 2))"),
+ # nested power
+ ("gram / meter ** 2 / second", "((gram / (meter ** 2)) / second)"),
+ # units should behave like numbers, so we don't need a bunch of extra tests for them
+ # implicit op, then addition
+ ("3 kg + 5", "((3 * kg) + 5)"),
+ ("(5 % 2) m", "((5 % 2) * m)"), # mod operator
+ ("(5 // 2) m", "((5 // 2) * m)"), # floordiv operator
+ ),
+ )
+ def test_preprocessed_eval_tree(self, input_text, parsed):
+ self._test_one(input_text, parsed, preprocess=True)
diff --git a/pint/testsuite/test_pitheorem.py b/pint/testsuite/test_pitheorem.py
index a495882..9893f50 100644
--- a/pint/testsuite/test_pitheorem.py
+++ b/pint/testsuite/test_pitheorem.py
@@ -8,7 +8,6 @@ from pint.testsuite import QuantityTestCase
# TODO: do not subclass from QuantityTestCase
class TestPiTheorem(QuantityTestCase):
def test_simple(self, caplog):
-
# simple movement
with caplog.at_level(logging.DEBUG):
assert pi_theorem({"V": "m/s", "T": "s", "L": "m"}) == [
diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py
index e51ec11..8fb712a 100644
--- a/pint/testsuite/test_quantity.py
+++ b/pint/testsuite/test_quantity.py
@@ -29,7 +29,6 @@ class FakeWrapper:
# TODO: do not subclass from QuantityTestCase
class TestQuantity(QuantityTestCase):
-
kwargs = dict(autoconvert_offset_to_baseunit=False)
def test_quantity_creation(self, caplog):
@@ -410,7 +409,6 @@ class TestQuantity(QuantityTestCase):
@helpers.requires_numpy
def test_convert_numpy(self):
-
# Conversions with single units take a different codepath than
# Conversions with more than one unit.
src_dst1 = UnitsContainer(meter=1), UnitsContainer(inch=1)
@@ -658,7 +656,13 @@ class TestQuantity(QuantityTestCase):
with pytest.raises(ValueError):
self.Q_(1, "m").__array__()
- @patch("pint.compat.upcast_types", [FakeWrapper])
+ @patch(
+ "pint.compat.upcast_type_names", ("pint.testsuite.test_quantity.FakeWrapper",)
+ )
+ @patch(
+ "pint.compat.upcast_type_map",
+ {"pint.testsuite.test_quantity.FakeWrapper": FakeWrapper},
+ )
def test_upcast_type_rejection_on_creation(self):
with pytest.raises(TypeError):
self.Q_(FakeWrapper(42), "m")
@@ -1038,7 +1042,6 @@ class TestQuantityBasicMath(QuantityTestCase):
self._test_numeric(np.ones((1, 3)), self._test_inplace)
def test_quantity_abs_round(self):
-
x = self.Q_(-4.2, "meter")
y = self.Q_(4.2, "meter")
@@ -1169,7 +1172,7 @@ class TestDimensions(QuantityTestCase):
assert get(UnitsContainer({"[time]": 1})) == UnitsContainer({"[time]": 1})
assert get("seconds") == UnitsContainer({"[time]": 1})
assert get(UnitsContainer({"seconds": 1})) == UnitsContainer({"[time]": 1})
- assert get("[speed]") == UnitsContainer({"[length]": 1, "[time]": -1})
+ assert get("[velocity]") == UnitsContainer({"[length]": 1, "[time]": -1})
assert get("[acceleration]") == UnitsContainer({"[length]": 1, "[time]": -2})
def test_dimensionality(self):
@@ -1190,14 +1193,14 @@ class TestDimensions(QuantityTestCase):
def test_inclusion(self):
dim = self.Q_(42, "meter").dimensionality
assert "[length]" in dim
- assert not ("[time]" in dim)
+ assert "[time]" not in dim
dim = (self.Q_(42, "meter") / self.Q_(11, "second")).dimensionality
assert "[length]" in dim
assert "[time]" in dim
dim = self.Q_(20.785, "J/(mol)").dimensionality
for dimension in ("[length]", "[mass]", "[substance]", "[time]"):
assert dimension in dim
- assert not ("[angle]" in dim)
+ assert "[angle]" not in dim
class TestQuantityWithDefaultRegistry(TestQuantity):
@@ -1694,7 +1697,7 @@ class TestOffsetUnitMath(QuantityTestCase):
in1, in2 = input_tuple
if type(in1) is tuple and type(in2) is tuple:
in1, in2 = self.Q_(*in1), self.Q_(*in2)
- elif not type(in1) is tuple and type(in2) is tuple:
+ elif type(in1) is not tuple and type(in2) is tuple:
in2 = self.Q_(*in2)
else:
in1 = self.Q_(*in1)
@@ -1734,7 +1737,7 @@ class TestOffsetUnitMath(QuantityTestCase):
(q1v, q1u), (q2v, q2u) = in1, in2
in1 = self.Q_(*(np.array([q1v] * 2, dtype=float), q1u))
in2 = self.Q_(q2v, q2u)
- elif not type(in1) is tuple and type(in2) is tuple:
+ elif type(in1) is not tuple and type(in2) is tuple:
in2 = self.Q_(*in2)
else:
in1 = self.Q_(*in1)
diff --git a/pint/testsuite/test_umath.py b/pint/testsuite/test_umath.py
index a3e69c7..6f32ab5 100644
--- a/pint/testsuite/test_umath.py
+++ b/pint/testsuite/test_umath.py
@@ -279,34 +279,43 @@ class TestMathUfuncs(TestUFuncs):
http://docs.scipy.org/doc/numpy/reference/ufuncs.html#math-operations
- add(x1, x2[, out]) Add arguments element-wise.
- subtract(x1, x2[, out]) Subtract arguments, element-wise.
- multiply(x1, x2[, out]) Multiply arguments element-wise.
- divide(x1, x2[, out]) Divide arguments element-wise.
- logaddexp(x1, x2[, out]) Logarithm of the sum of exponentiations of the inputs.
- logaddexp2(x1, x2[, out]) Logarithm of the sum of exponentiations of the inputs in plain-2.
- true_divide(x1, x2[, out]) Returns a true division of the inputs, element-wise.
- floor_divide(x1, x2[, out]) Return the largest integer smaller or equal to the division of the inputs.
- negative(x[, out]) Returns an array with the negative of each element of the original array.
- power(x1, x2[, out]) First array elements raised to powers from second array, element-wise. NOT IMPLEMENTED
- remainder(x1, x2[, out]) Return element-wise remainder of division.
- mod(x1, x2[, out]) Return element-wise remainder of division.
- fmod(x1, x2[, out]) Return the element-wise remainder of division.
- absolute(x[, out]) Calculate the absolute value element-wise.
- rint(x[, out]) Round elements of the array to the nearest integer.
- sign(x[, out]) Returns an element-wise indication of the sign of a number.
- conj(x[, out]) Return the complex conjugate, element-wise.
- exp(x[, out]) Calculate the exponential of all elements in the input array.
- exp2(x[, out]) Calculate 2**p for all p in the input array.
- log(x[, out]) Natural logarithm, element-wise.
- log2(x[, out]) Base-2 logarithm of x.
- log10(x[, out]) Return the plain 10 logarithm of the input array, element-wise.
- expm1(x[, out]) Calculate exp(x) - 1 for all elements in the array.
- log1p(x[, out]) Return the natural logarithm of one plus the input array, element-wise.
- sqrt(x[, out]) Return the positive square-root of an array, element-wise.
- square(x[, out]) Return the element-wise square of the input.
- reciprocal(x[, out]) Return the reciprocal of the argument, element-wise.
- ones_like(x[, out]) Returns an array of ones with the same shape and type as a given array.
+ add(x1, x2, /[, out, where, casting, order, ...] Add arguments element-wise.
+ subtract(x1, x2, /[, out, where, casting, ...] Subtract arguments, element-wise.
+ multiply(x1, x2, /[, out, where, casting, ...] Multiply arguments element-wise.
+ matmul(x1, x2, /[, out, casting, order, ...] Matrix product of two arrays.
+ divide(x1, x2, /[, out, where, casting, ...] Divide arguments element-wise.
+ logaddexp(x1, x2, /[, out, where, casting, ...] Logarithm of the sum of exponentiations of the inputs.
+ logaddexp2(x1, x2, /[, out, where, casting, ...] Logarithm of the sum of exponentiations of the inputs in base-2.
+ true_divide(x1, x2, /[, out, where, ...] Divide arguments element-wise.
+ floor_divide(x1, x2, /[, out, where, ...] Return the largest integer smaller or equal to the division of the inputs.
+ negative(x, /[, out, where, casting, order, ...] Numerical negative, element-wise.
+ positive(x, /[, out, where, casting, order, ...] Numerical positive, element-wise.
+ power(x1, x2, /[, out, where, casting, ...] First array elements raised to powers from second array, element-wise.
+ float_power(x1, x2, /[, out, where, ...] First array elements raised to powers from second array, element-wise.
+ remainder(x1, x2, /[, out, where, casting, ...] Returns the element-wise remainder of division.
+ mod(x1, x2, /[, out, where, casting, order, ...] Returns the element-wise remainder of division.
+ fmod(x1, x2, /[, out, where, casting, ...] Returns the element-wise remainder of division.
+ divmod(x1, x2[, out1, out2], / [[, out, ...] Return element-wise quotient and remainder simultaneously.
+ absolute(x, /[, out, where, casting, order, ...] Calculate the absolute value element-wise.
+ fabs(x, /[, out, where, casting, order, ...] Compute the absolute values element-wise.
+ rint(x, /[, out, where, casting, order, ...] Round elements of the array to the nearest integer.
+ sign(x, /[, out, where, casting, order, ...] Returns an element-wise indication of the sign of a number.
+ heaviside(x1, x2, /[, out, where, casting, ...] Compute the Heaviside step function.
+ conj(x, /[, out, where, casting, order, ...] Return the complex conjugate, element-wise.
+ conjugate(x, /[, out, where, casting, ...] Return the complex conjugate, element-wise.
+ exp(x, /[, out, where, casting, order, ...] Calculate the exponential of all elements in the input array.
+ exp2(x, /[, out, where, casting, order, ...] Calculate 2**p for all p in the input array.
+ log(x, /[, out, where, casting, order, ...] Natural logarithm, element-wise.
+ log2(x, /[, out, where, casting, order, ...] Base-2 logarithm of x.
+ log10(x, /[, out, where, casting, order, ...] Return the base 10 logarithm of the input array, element-wise.
+ expm1(x, /[, out, where, casting, order, ...] Calculate exp(x) - 1 for all elements in the array.
+ log1p(x, /[, out, where, casting, order, ...] Return the natural logarithm of one plus the input array, element-wise.
+ sqrt(x, /[, out, where, casting, order, ...] Return the non-negative square-root of an array, element-wise.
+ square(x, /[, out, where, casting, order, ...] Return the element-wise square of the input.
+ cbrt(x, /[, out, where, casting, order, ...] Return the cube-root of an array, element-wise.
+ reciprocal(x, /[, out, where, casting, ...] Return the reciprocal of the argument, element-wise.
+ gcd(x1, x2, /[, out, where, casting, order, ...] Returns the greatest common divisor of |x1| and |x2|
+ lcm(x1, x2, /[, out, where, casting, order, ...] Returns the lowest common multiple of |x1| and |x2|
Parameters
----------
@@ -364,6 +373,9 @@ class TestMathUfuncs(TestUFuncs):
def test_negative(self):
self._test1(np.negative, (self.qless, self.q1), ())
+ def test_positive(self):
+ self._test1(np.positive, (self.qless, self.q1), ())
+
def test_remainder(self):
self._test2(
np.remainder,
diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py
index b3a6219..98a4fcc 100644
--- a/pint/testsuite/test_unit.py
+++ b/pint/testsuite/test_unit.py
@@ -7,12 +7,7 @@ from contextlib import nullcontext as does_not_raise
import pytest
-from pint import (
- DefinitionSyntaxError,
- DimensionalityError,
- RedefinitionError,
- UndefinedUnitError,
-)
+from pint import DimensionalityError, RedefinitionError, UndefinedUnitError, errors
from pint.compat import np
from pint.registry import LazyRegistry, UnitRegistry
from pint.testsuite import QuantityTestCase, assert_no_warnings, helpers
@@ -59,6 +54,33 @@ class TestUnit(QuantityTestCase):
with subtests.test(spec):
assert spec.format(x) == result
+ def test_latex_escaping(self, subtests):
+ ureg = UnitRegistry()
+ ureg.define(r"percent = 1e-2 = %")
+ x = ureg.Unit(UnitsContainer(percent=1))
+ for spec, result in {
+ "L": r"\mathrm{percent}",
+ "L~": r"\mathrm{\%}",
+ "Lx": r"\si[]{\percent}",
+ "Lx~": r"\si[]{\%}",
+ }.items():
+ with subtests.test(spec):
+ ureg.default_format = spec
+ assert f"{x}" == result, f"Failed for {spec}, {result}"
+ # no '#' here as it's a comment char when define()ing new units
+ ureg.define(r"weirdunit = 1 = \~_^&%$_{}")
+ x = ureg.Unit(UnitsContainer(weirdunit=1))
+ for spec, result in {
+ "L": r"\mathrm{weirdunit}",
+ "L~": r"\mathrm{\textbackslash \textasciitilde \_\textasciicircum \&\%\$\_\{\}}",
+ "Lx": r"\si[]{\weirdunit}",
+ # TODO: Currently makes \si[]{\\~_^&%$_{}} (broken). What do we even want this to be?
+ # "Lx~": r"\si[]{\textbackslash \textasciitilde \_\textasciicircum \&\%\$\_\{\}}",
+ }.items():
+ with subtests.test(spec):
+ ureg.default_format = spec
+ assert f"{x}" == result, f"Failed for {spec}, {result}"
+
def test_unit_default_formatting(self, subtests):
ureg = UnitRegistry()
x = ureg.Unit(UnitsContainer(meter=2, kilogram=1, second=-1))
@@ -217,7 +239,6 @@ class TestUnit(QuantityTestCase):
assert not (self.U_("byte") != self.U_("byte"))
def test_unit_cmp(self):
-
x = self.U_("m")
assert x < self.U_("km")
assert x > self.U_("mm")
@@ -227,17 +248,14 @@ class TestUnit(QuantityTestCase):
assert y < 1e6
def test_dimensionality(self):
-
x = self.U_("m")
assert x.dimensionality == UnitsContainer({"[length]": 1})
def test_dimensionless(self):
-
assert self.U_("m/mm").dimensionless
assert not self.U_("m").dimensionless
def test_unit_casting(self):
-
assert int(self.U_("m/mm")) == 1000
assert float(self.U_("mm/m")) == 1e-3
assert complex(self.U_("mm/mm")) == 1 + 0j
@@ -260,9 +278,9 @@ class TestRegistry(QuantityTestCase):
cls.ureg.autoconvert_offset_to_baseunit = False
def test_base(self):
- ureg = UnitRegistry(None)
+ ureg = UnitRegistry(None, on_redefinition="raise")
ureg.define("meter = [length]")
- with pytest.raises(DefinitionSyntaxError):
+ with pytest.raises(errors.RedefinitionError):
ureg.define("meter = [length]")
with pytest.raises(TypeError):
ureg.define(list())
@@ -282,7 +300,7 @@ class TestRegistry(QuantityTestCase):
ureg1 = UnitRegistry()
ureg2 = UnitRegistry(data)
assert dir(ureg1) == dir(ureg2)
- with pytest.raises(ValueError):
+ with pytest.raises(FileNotFoundError):
UnitRegistry(None).load_definitions("notexisting")
def test_default_format(self):
@@ -375,6 +393,16 @@ class TestRegistry(QuantityTestCase):
1, UnitsContainer(meter=37, second=-4.321)
)
+ def test_parse_pretty_degrees(self):
+ for exp in ["1Δ°C", "1 Δ°C", "ΔdegC", "delta_°C"]:
+ assert self.ureg.parse_expression(exp) == self.Q_(
+ 1, UnitsContainer(delta_degree_Celsius=1)
+ )
+ assert self.ureg.parse_expression("")
+ assert self.ureg.parse_expression("mol °K") == self.Q_(
+ 1, UnitsContainer(mol=1, kelvin=1)
+ )
+
def test_parse_factor(self):
assert self.ureg.parse_expression("42*meter") == self.Q_(
42, UnitsContainer(meter=1.0)
@@ -566,7 +594,6 @@ class TestRegistry(QuantityTestCase):
assert h3(3, 1) == (3, 1)
def test_wrap_referencing(self):
-
ureg = self.ureg
def gfunc(x, y):
@@ -630,7 +657,7 @@ class TestRegistry(QuantityTestCase):
assert g0(6, 2) == 3
assert g0(6 * ureg.parsec, 2) == 3 * ureg.parsec
- g1 = ureg.check("[speed]", "[time]")(gfunc)
+ g1 = ureg.check("[velocity]", "[time]")(gfunc)
with pytest.raises(DimensionalityError):
g1(3.0, 1)
with pytest.raises(DimensionalityError):
@@ -643,9 +670,9 @@ class TestRegistry(QuantityTestCase):
)
with pytest.raises(TypeError):
- ureg.check("[speed]")(gfunc)
+ ureg.check("[velocity]")(gfunc)
with pytest.raises(TypeError):
- ureg.check("[speed]", "[time]", "[mass]")(gfunc)
+ ureg.check("[velocity]", "[time]", "[mass]")(gfunc)
def test_to_ref_vs_to(self):
self.ureg.autoconvert_offset_to_baseunit = True
@@ -668,7 +695,7 @@ class TestRegistry(QuantityTestCase):
with caplog.at_level(logging.DEBUG):
d("meter = [fruits]")
d("kilo- = 1000")
- d("[speed] = [vegetables]")
+ d("[velocity] = [vegetables]")
# aliases
d("bla = 3.2 meter = inch")
@@ -777,7 +804,6 @@ class TestRegistry(QuantityTestCase):
class TestCaseInsensitiveRegistry(QuantityTestCase):
-
kwargs = dict(case_sensitive=False)
def test_case_sensitivity(self):
@@ -819,7 +845,6 @@ class TestCompatibleUnits(QuantityTestCase):
self._test(self.ureg.kelvin)
def test_context_sp(self):
-
gd = self.ureg.get_dimensionality
# length, frequency, energy
@@ -875,14 +900,14 @@ class TestRegistryWithDefaultRegistry(TestRegistry):
def test_redefinition(self):
d = self.ureg.define
- with pytest.raises(DefinitionSyntaxError):
+ with pytest.raises(RedefinitionError):
d("meter = [time]")
with pytest.raises(RedefinitionError):
d("meter = [newdim]")
with pytest.raises(RedefinitionError):
d("kilo- = 1000")
with pytest.raises(RedefinitionError):
- d("[speed] = [length]")
+ d("[velocity] = [length]")
# aliases
assert "inch" in self.ureg._units
@@ -894,7 +919,6 @@ class TestRegistryWithDefaultRegistry(TestRegistry):
# TODO: remove QuantityTestCase
class TestConvertWithOffset(QuantityTestCase):
-
# The dicts in convert_with_offset are used to create a UnitsContainer.
# We create UnitsContainer to avoid any auto-conversion of units.
convert_with_offset = [
diff --git a/pint/testsuite/test_util.py b/pint/testsuite/test_util.py
index d2eebe5..fd6494a 100644
--- a/pint/testsuite/test_util.py
+++ b/pint/testsuite/test_util.py
@@ -305,7 +305,6 @@ class TestGraph:
class TestMatrix:
def test_matrix_to_string(self):
-
assert (
matrix_to_string([[1, 2], [3, 4]], row_headers=None, col_headers=None)
== "1\t2\n"
@@ -346,13 +345,11 @@ class TestMatrix:
)
def test_transpose(self):
-
assert transpose([[1, 2], [3, 4]]) == [[1, 3], [2, 4]]
class TestOtherUtils:
def test_iterable(self):
-
# Test with list, string, generator, and scalar
assert iterable([0, 1, 2, 3])
assert iterable("test")
@@ -360,7 +357,6 @@ class TestOtherUtils:
assert not iterable(0)
def test_sized(self):
-
# Test with list, string, generator, and scalar
assert sized([0, 1, 2, 3])
assert sized("test")
diff --git a/pint/util.py b/pint/util.py
index 54a7755..d5f3aab 100644
--- a/pint/util.py
+++ b/pint/util.py
@@ -10,6 +10,7 @@
from __future__ import annotations
+import functools
import inspect
import logging
import math
@@ -29,9 +30,8 @@ from .formatting import format_unit
from .pint_eval import build_eval_tree
if TYPE_CHECKING:
- from pint import Quantity, UnitRegistry
-
- from ._typing import UnitLike
+ from ._typing import Quantity, UnitLike
+ from .registry import UnitRegistry
logger = logging.getLogger(__name__)
logger.addHandler(NullHandler())
@@ -565,12 +565,11 @@ class ParserHelper(UnitsContainer):
if non_int_type is float:
return cls(1, [(input_word, 1)], non_int_type=non_int_type)
else:
- ONE = non_int_type("1.0")
+ ONE = non_int_type("1")
return cls(ONE, [(input_word, ONE)], non_int_type=non_int_type)
@classmethod
def eval_token(cls, token, use_decimal=False, non_int_type=float):
-
# TODO: remove this code when use_decimal is deprecated
if use_decimal:
raise DeprecationWarning(
@@ -641,7 +640,7 @@ class ParserHelper(UnitsContainer):
for k in list(ret):
if k.lower() == "nan":
del ret._d[k]
- ret.scale = math.nan
+ ret.scale = non_int_type(math.nan)
return ret
@@ -753,7 +752,7 @@ class ParserHelper(UnitsContainer):
#: List of regex substitution pairs.
_subs_re_list = [
- ("\N{DEGREE SIGN}", " degree"),
+ ("\N{DEGREE SIGN}", "degree"),
(r"([\w\.\-\+\*\\\^])\s+", r"\1 "), # merge multiple spaces
(r"({}) squared", r"\1**2"), # Handle square and cube
(r"({}) cubed", r"\1**3"),
@@ -764,7 +763,7 @@ _subs_re_list = [
r"\b([0-9]+\.?[0-9]*)(?=[e|E][a-zA-Z]|[a-df-zA-DF-Z])",
r"\1*",
), # Handle numberLetter for multiplication
- (r"([\w\.\-])\s+(?=\w)", r"\1*"), # Handle space for multiplication
+ (r"([\w\.\)])\s+(?=[\w\(])", r"\1*"), # Handle space for multiplication
]
#: Compiles the regex and replace {} by a regex that matches an identifier.
@@ -980,80 +979,6 @@ def getattr_maybe_raise(self, item):
raise AttributeError("%r object has no attribute %r" % (self, item))
-class SourceIterator:
- """Iterator to facilitate reading the definition files.
-
- Accepts any sequence (like a list of lines, a file or another SourceIterator)
-
- The iterator yields the line number and line (skipping comments and empty lines)
- and stripping white spaces.
-
- for lineno, line in SourceIterator(sequence):
- # do something here
-
- """
-
- def __new__(cls, sequence, filename=None, is_resource=False):
- if isinstance(sequence, SourceIterator):
- return sequence
-
- obj = object.__new__(cls)
-
- if sequence is not None:
- obj.internal = enumerate(sequence, 1)
- obj.last = (None, None)
- obj.filename = filename or getattr(sequence, "name", None)
- obj.is_resource = is_resource
-
- return obj
-
- def __iter__(self):
- return self
-
- def __next__(self):
- line = ""
- while not line or line.startswith("#"):
- lineno, line = next(self.internal)
- line = line.split("#", 1)[0].strip()
-
- self.last = lineno, line
- return lineno, line
-
- next = __next__
-
- def block_iter(self):
- """Iterate block including header."""
- return BlockIterator(self)
-
-
-class BlockIterator(SourceIterator):
- """Like SourceIterator but stops when it finds '@end'
- It also raises an error if another '@' directive is found inside.
- """
-
- def __new__(cls, line_iterator):
- obj = SourceIterator.__new__(cls, None)
- obj.internal = line_iterator.internal
- obj.last = line_iterator.last
- obj.done_last = False
- return obj
-
- def __next__(self):
- if not self.done_last:
- self.done_last = True
- return self.last
-
- lineno, line = SourceIterator.__next__(self)
- if line.startswith("@end"):
- raise StopIteration
- elif line.startswith("@"):
- raise DefinitionSyntaxError("cannot nest @ directives", lineno=lineno)
-
- return lineno, line
-
- next = __next__
-
-
def iterable(y) -> bool:
"""Check whether or not an object can be iterated over.
@@ -1095,6 +1020,13 @@ def sized(y) -> bool:
return True
+@functools.lru_cache(
+ maxsize=None
+) # TODO: replace with cache when Python 3.8 is dropped.
+def _build_type(class_name: str, bases):
+ return type(class_name, bases, dict())
+
+
def build_dependent_class(registry_class, class_name: str, attribute_name: str) -> Type:
"""Creates a class specifically for the given registry that
subclass all the classes named by the registry bases in a
@@ -1110,9 +1042,10 @@ def build_dependent_class(registry_class, class_name: str, attribute_name: str)
for base in inspect.getmro(registry_class)
if attribute_name in base.__dict__
)
- bases = dict.fromkeys(bases, None)
- newcls = type(class_name, tuple(bases.keys()), dict())
- return newcls
+ bases = tuple(dict.fromkeys(bases, None).keys())
+ if len(bases) == 1 and bases[0].__name__ == class_name:
+ return bases[0]
+ return _build_type(class_name, bases)
def create_class_with_registry(registry, base_class) -> Type:
diff --git a/pyproject.toml b/pyproject.toml
index 771af68..72b6560 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,5 +1,89 @@
+[project]
+name = "Pint"
+authors = [
+ {name="Hernan E. Grecco", email="hernan.grecco@gmail.com"}
+]
+license = {text = "BSD"}
+description = "Physical quantities module"
+readme = "README.rst"
+maintainers = [
+ {name="Hernan E. Grecco", email="hernan.grecco@gmail.com"},
+ {name="Jules Chéron", email="julescheron@gmail.com"}
+]
+keywords = ["physical", "quantities", "unit", "conversion", "science"]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Science/Research",
+ "License :: OSI Approved :: BSD License",
+ "Operating System :: MacOS :: MacOS X",
+ "Operating System :: Microsoft :: Windows",
+ "Operating System :: POSIX",
+ "Programming Language :: Python",
+ "Topic :: Scientific/Engineering",
+ "Topic :: Software Development :: Libraries",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11"
+]
+requires-python = ">=3.8"
+dynamic = ["version"]
+
+[tool.setuptools.package-data]
+pint = [
+ "default_en.txt",
+ "constants_en.txt",
+ "py.typed"]
+
+
+[project.optional-dependencies]
+test = [
+ "pytest",
+ "pytest-mpl",
+ "pytest-cov",
+ "pytest-subtests"
+]
+numpy = ["numpy >= 1.19.5"]
+uncertainties = ["uncertainties >= 3.1.6"]
+babel = ["babel <= 2.8"]
+pandas = ["pint-pandas >= 0.3"]
+xarray = ["xarray"]
+dask = ["dask"]
+mip = ["mip >= 1.13"]
+
+[project.urls]
+Homepage = "https://github.com/hgrecco/pint"
+Documentation = "https://pint.readthedocs.io/"
+
+[project.scripts]
+pint-convert = "pint.pint_convert:main"
+
+[tool.setuptools]
+packages = ["pint"]
+
[build-system]
-requires = ["setuptools>=41", "wheel", "setuptools_scm[toml]>=3.4.3"]
+requires = ["setuptools>=61", "wheel", "setuptools_scm[toml]>=3.4.3"]
build-backend = "setuptools.build_meta"
[tool.setuptools_scm]
+
+[tool.ruff.isort]
+required-imports = ["from __future__ import annotations"]
+known-first-party= ["pint"]
+
+
+[tool.ruff]
+ignore = [
+ # whitespace before ':' - doesn't work well with black
+ # "E203",
+ "E402",
+ # line too long - let black worry about that
+ "E501",
+ # do not assign a lambda expression, use a def
+ "E731",
+ # line break before binary operator
+ # "W503"
+]
+extend-exclude = ["build"]
+line-length=88
diff --git a/requirements_docs.txt b/requirements_docs.txt
index 7e90fd2..683292c 100644
--- a/requirements_docs.txt
+++ b/requirements_docs.txt
@@ -15,3 +15,6 @@ dask[complete]
setuptools>=41.2
Serialize
pygments>=2.4
+sphinx-book-theme==0.3.3
+sphinx_copybutton
+sphinx_design
diff --git a/setup.cfg b/setup.cfg
deleted file mode 100644
index 831599b..0000000
--- a/setup.cfg
+++ /dev/null
@@ -1,73 +0,0 @@
-[metadata]
-name = Pint
-author = Hernan E. Grecco
-author_email = hernan.grecco@gmail.com
-license = BSD
-description = Physical quantities module
-long_description = file: README.rst
-keywords = physical, quantities, unit, conversion, science
-url = https://github.com/hgrecco/pint
-classifiers =
- Development Status :: 4 - Beta
- Intended Audience :: Developers
- Intended Audience :: Science/Research
- License :: OSI Approved :: BSD License
- Operating System :: MacOS :: MacOS X
- Operating System :: Microsoft :: Windows
- Operating System :: POSIX
- Programming Language :: Python
- Topic :: Scientific/Engineering
- Topic :: Software Development :: Libraries
- Programming Language :: Python :: 3.8
- Programming Language :: Python :: 3.9
- Programming Language :: Python :: 3.10
-
-[options]
-packages = pint
-zip_safe = True
-include_package_data = True
-python_requires = >=3.8
-setup_requires = setuptools; setuptools_scm
-scripts = pint/pint-convert
-
-[options.extras_require]
-numpy = numpy >= 1.19.5
-uncertainties = uncertainties >= 3.1.6
-mip = mip >= 1.13
-test =
- pytest
- pytest-mpl
- pytest-cov
- pytest-subtests
-
-[options.package_data]
-pint = default_en.txt; constants_en.txt; py.typed
-
-[build-system]
-requires = ["setuptools", "setuptools_scm", "wheel"]
-
-[flake8]
-ignore=
- # whitespace before ':' - doesn't work well with black
- E203
- E402
- # line too long - let black worry about that
- E501
- # do not assign a lambda expression, use a def
- E731
- # line break before binary operator
- W503
-exclude=
- build
-
-[isort]
-default_section=THIRDPARTY
-known_first_party=pint
-multi_line_output=3
-include_trailing_comma=True
-force_grid_wrap=0
-use_parentheses=True
-line_length=88
-
-[zest.releaser]
-python-file-with-version = version.py
diff --git a/setup.py b/setup.py
deleted file mode 100644
index f4f9665..0000000
--- a/setup.py
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/usr/bin/env python3
-from setuptools import setup
-
-if __name__ == "__main__":
- setup()
diff --git a/version.py b/version.py
index 9926a1f..c9114dd 100644
--- a/version.py
+++ b/version.py
@@ -2,5 +2,5 @@
# flake8: noqa
# fmt: off
-__version__ = '0.20.dev0'
+__version__ = '0.21.dev0'
# fmt: on