diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f1beef5cc5..04a1279316 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13.0-rc.3"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml index a2621ecc26..3751578eac 100644 --- a/.github/workflows/spellcheck.yml +++ b/.github/workflows/spellcheck.yml @@ -27,4 +27,4 @@ jobs: - name: Check spelling run: | codespell docs/*.rst --ignore-words docs/codespell-ignore-words.txt - codespell datasette -S datasette/static --ignore-words docs/codespell-ignore-words.txt + codespell datasette -S datasette/static -S datasette/vendored --ignore-words docs/codespell-ignore-words.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4c4b7534ba..f7ee1cbe17 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13.0-rc.3"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} diff --git a/datasette/vendored/__init__.py b/datasette/vendored/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/datasette/vendored/pint/LICENSE b/datasette/vendored/pint/LICENSE new file mode 100644 index 0000000000..49aec52e35 --- /dev/null +++ b/datasette/vendored/pint/LICENSE @@ -0,0 +1,33 @@ +Copyright (c) 2012 by Hernan E. Grecco and contributors. See AUTHORS +for more details. + +Some rights reserved. + +Redistribution and use in source and binary forms of the software as well +as documentation, 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. + +THIS SOFTWARE AND DOCUMENTATION 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 +SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/datasette/vendored/pint/__init__.py b/datasette/vendored/pint/__init__.py new file mode 100644 index 0000000000..f717fc6e74 --- /dev/null +++ b/datasette/vendored/pint/__init__.py @@ -0,0 +1,144 @@ +""" + pint + ~~~~ + + Pint is Python module/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. + + :copyright: 2016 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from importlib.metadata import version + +from .delegates.formatter._format_helpers import formatter +from .errors import ( # noqa: F401 + DefinitionSyntaxError, + DimensionalityError, + LogarithmicUnitCalculusError, + OffsetUnitCalculusError, + PintError, + RedefinitionError, + UndefinedUnitError, + UnitStrippedWarning, +) +from .formatting import register_unit_format +from .registry import ApplicationRegistry, LazyRegistry, UnitRegistry +from .util import logger, pi_theorem # noqa: F401 + +# Default Quantity, Unit and Measurement are the ones +# build in the default registry. +Quantity = UnitRegistry.Quantity +Unit = UnitRegistry.Unit +Measurement = UnitRegistry.Measurement +Context = UnitRegistry.Context +Group = UnitRegistry.Group + +try: # pragma: no cover + __version__ = version("pint") +except Exception: # pragma: no cover + # we seem to have a local copy not installed without setuptools + # so the reported version will be unknown + __version__ = "unknown" + + +#: A Registry with the default units and constants. +_DEFAULT_REGISTRY = LazyRegistry() + +#: Registry used for unpickling operations. +application_registry = ApplicationRegistry(_DEFAULT_REGISTRY) + + +def _unpickle(cls, *args): + """Rebuild object upon unpickling. + All units must exist in the application registry. + + Parameters + ---------- + cls : Quantity, Magnitude, or Unit + *args + + Returns + ------- + object of type cls + + """ + from datasette.vendored.pint.util import UnitsContainer + + for arg in args: + # Prefixed units are defined within the registry + # on parsing (which does not happen here). + # We need to make sure that this happens before using. + if isinstance(arg, UnitsContainer): + for name in arg: + application_registry.parse_units(name) + + return cls(*args) + + +def _unpickle_quantity(cls, *args): + """Rebuild quantity upon unpickling using the application registry.""" + return _unpickle(application_registry.Quantity, *args) + + +def _unpickle_unit(cls, *args): + """Rebuild unit upon unpickling using the application registry.""" + return _unpickle(application_registry.Unit, *args) + + +def _unpickle_measurement(cls, *args): + """Rebuild measurement upon unpickling using the application registry.""" + return _unpickle(application_registry.Measurement, *args) + + +def set_application_registry(registry): + """Set the application registry, which is used for unpickling operations + and when invoking pint.Quantity or pint.Unit directly. + + Parameters + ---------- + registry : pint.UnitRegistry + """ + application_registry.set(registry) + + +def get_application_registry(): + """Return the application registry. If :func:`set_application_registry` was never + invoked, return a registry built using :file:`defaults_en.txt` embedded in the pint + package. + + Returns + ------- + pint.UnitRegistry + """ + return application_registry + + +# Enumerate all user-facing objects +# Hint to intersphinx that, when building objects.inv, these objects must be registered +# under the top-level module and not in their original submodules +__all__ = ( + "Measurement", + "Quantity", + "Unit", + "UnitRegistry", + "PintError", + "DefinitionSyntaxError", + "LogarithmicUnitCalculusError", + "DimensionalityError", + "OffsetUnitCalculusError", + "RedefinitionError", + "UndefinedUnitError", + "UnitStrippedWarning", + "formatter", + "get_application_registry", + "set_application_registry", + "register_unit_format", + "pi_theorem", + "__version__", + "Context", +) diff --git a/datasette/vendored/pint/_typing.py b/datasette/vendored/pint/_typing.py new file mode 100644 index 0000000000..778145d6e3 --- /dev/null +++ b/datasette/vendored/pint/_typing.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +from collections.abc import Callable +from decimal import Decimal +from fractions import Fraction +from typing import TYPE_CHECKING, Any, Protocol, TypeVar, Union + +from .compat import Never, TypeAlias + +if TYPE_CHECKING: + from .facets.plain import PlainQuantity as Quantity + from .facets.plain import PlainUnit as Unit + from .util import UnitsContainer + + +HAS_NUMPY = False +if TYPE_CHECKING: + from .compat import HAS_NUMPY + +if HAS_NUMPY: + from .compat import np + + Scalar: TypeAlias = Union[float, int, Decimal, Fraction, np.number[Any]] + Array = np.ndarray[Any, Any] +else: + Scalar: TypeAlias = Union[float, int, Decimal, Fraction] + Array: TypeAlias = Never + +# TODO: Change when Python 3.10 becomes minimal version. +Magnitude = Union[Scalar, Array] + +UnitLike = Union[str, dict[str, Scalar], "UnitsContainer", "Unit"] + +QuantityOrUnitLike = Union["Quantity", UnitLike] + +Shape = tuple[int, ...] + +S = TypeVar("S") + +FuncType = Callable[..., Any] +F = TypeVar("F", bound=FuncType) + + +# TODO: Improve or delete types +QuantityArgument = Any + +T = TypeVar("T") + + +class Handler(Protocol): + def __getitem__(self, item: type[T]) -> Callable[[T], None]: ... diff --git a/datasette/vendored/pint/babel_names.py b/datasette/vendored/pint/babel_names.py new file mode 100644 index 0000000000..408ef8f8cc --- /dev/null +++ b/datasette/vendored/pint/babel_names.py @@ -0,0 +1,146 @@ +""" + pint.babel + ~~~~~~~~~~ + + :copyright: 2016 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from .compat import HAS_BABEL + +_babel_units: dict[str, str] = dict( + standard_gravity="acceleration-g-force", + millibar="pressure-millibar", + metric_ton="mass-metric-ton", + megawatt="power-megawatt", + degF="temperature-fahrenheit", + dietary_calorie="energy-foodcalorie", + millisecond="duration-millisecond", + mph="speed-mile-per-hour", + acre_foot="volume-acre-foot", + mebibit="digital-megabit", + gibibit="digital-gigabit", + tebibit="digital-terabit", + mebibyte="digital-megabyte", + kibibyte="digital-kilobyte", + mm_Hg="pressure-millimeter-of-mercury", + month="duration-month", + kilocalorie="energy-kilocalorie", + cubic_mile="volume-cubic-mile", + arcsecond="angle-arc-second", + byte="digital-byte", + metric_cup="volume-cup-metric", + kilojoule="energy-kilojoule", + meter_per_second_squared="acceleration-meter-per-second-squared", + pint="volume-pint", + square_centimeter="area-square-centimeter", + in_Hg="pressure-inch-hg", + milliampere="electric-milliampere", + arcminute="angle-arc-minute", + MPG="consumption-mile-per-gallon", + hertz="frequency-hertz", + day="duration-day", + mps="speed-meter-per-second", + kilometer="length-kilometer", + square_yard="area-square-yard", + kelvin="temperature-kelvin", + kilogram="mass-kilogram", + kilohertz="frequency-kilohertz", + megahertz="frequency-megahertz", + meter="length-meter", + cubic_inch="volume-cubic-inch", + kilowatt_hour="energy-kilowatt-hour", + second="duration-second", + yard="length-yard", + light_year="length-light-year", + millimeter="length-millimeter", + metric_horsepower="power-horsepower", + gibibyte="digital-gigabyte", + # 'temperature-generic', + liter="volume-liter", + turn="angle-revolution", + microsecond="duration-microsecond", + pound="mass-pound", + ounce="mass-ounce", + calorie="energy-calorie", + centimeter="length-centimeter", + inch="length-inch", + centiliter="volume-centiliter", + troy_ounce="mass-ounce-troy", + gram="mass-gram", + kilowatt="power-kilowatt", + knot="speed-knot", + lux="light-lux", + hectoliter="volume-hectoliter", + microgram="mass-microgram", + degC="temperature-celsius", + tablespoon="volume-tablespoon", + cubic_yard="volume-cubic-yard", + square_foot="area-square-foot", + tebibyte="digital-terabyte", + square_inch="area-square-inch", + carat="mass-carat", + hectopascal="pressure-hectopascal", + gigawatt="power-gigawatt", + watt="power-watt", + micrometer="length-micrometer", + volt="electric-volt", + bit="digital-bit", + gigahertz="frequency-gigahertz", + teaspoon="volume-teaspoon", + ohm="electric-ohm", + joule="energy-joule", + cup="volume-cup", + square_mile="area-square-mile", + nautical_mile="length-nautical-mile", + square_meter="area-square-meter", + mile="length-mile", + acre="area-acre", + nanometer="length-nanometer", + hour="duration-hour", + astronomical_unit="length-astronomical-unit", + liter_per_100kilometers="consumption-liter-per-100kilometers", + megaliter="volume-megaliter", + ton="mass-ton", + hectare="area-hectare", + square_kilometer="area-square-kilometer", + kibibit="digital-kilobit", + mile_scandinavian="length-mile-scandinavian", + liter_per_kilometer="consumption-liter-per-kilometer", + century="duration-century", + cubic_foot="volume-cubic-foot", + deciliter="volume-deciliter", + # pint='volume-pint-metric', + cubic_meter="volume-cubic-meter", + cubic_kilometer="volume-cubic-kilometer", + quart="volume-quart", + cc="volume-cubic-centimeter", + pound_force_per_square_inch="pressure-pound-per-square-inch", + milligram="mass-milligram", + kph="speed-kilometer-per-hour", + minute="duration-minute", + parsec="length-parsec", + picometer="length-picometer", + degree="angle-degree", + milliwatt="power-milliwatt", + week="duration-week", + ampere="electric-ampere", + milliliter="volume-milliliter", + decimeter="length-decimeter", + fluid_ounce="volume-fluid-ounce", + nanosecond="duration-nanosecond", + foot="length-foot", + karat="proportion-karat", + year="duration-year", + gallon="volume-gallon", + radian="angle-radian", +) + +if not HAS_BABEL: + _babel_units = {} + +_babel_systems: dict[str, str] = dict(mks="metric", imperial="uksystem", US="ussystem") + +_babel_lengths: list[str] = ["narrow", "short", "long"] diff --git a/datasette/vendored/pint/compat.py b/datasette/vendored/pint/compat.py new file mode 100644 index 0000000000..e67f99ba8f --- /dev/null +++ b/datasette/vendored/pint/compat.py @@ -0,0 +1,394 @@ +""" + pint.compat + ~~~~~~~~~~~ + + Compatibility layer. + + :copyright: 2013 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import math +import sys +from collections.abc import Callable, Iterable, Mapping +from decimal import Decimal +from importlib import import_module +from numbers import Number +from typing import ( + Any, + NoReturn, +) + +if sys.version_info >= (3, 10): + from typing import TypeAlias # noqa +else: + from typing_extensions import TypeAlias # noqa + +if sys.version_info >= (3, 11): + from typing import Self # noqa +else: + from typing_extensions import Self # noqa + + +if sys.version_info >= (3, 11): + from typing import Never # noqa +else: + from typing_extensions import Never # noqa + + +if sys.version_info >= (3, 11): + from typing import Unpack # noqa +else: + from typing_extensions import Unpack # noqa + + +if sys.version_info >= (3, 13): + from warnings import deprecated # noqa +else: + from typing_extensions import deprecated # noqa + + +def missing_dependency( + package: str, display_name: str | None = None +) -> Callable[..., NoReturn]: + """Return a helper function that raises an exception when used. + + It provides a way delay a missing dependency exception until it is used. + """ + display_name = display_name or package + + def _inner(*args: Any, **kwargs: Any) -> NoReturn: + raise Exception( + "This feature requires %s. Please install it by running:\n" + "pip install %s" % (display_name, package) + ) + + return _inner + + +# TODO: remove this warning after v0.10 +class BehaviorChangeWarning(UserWarning): + pass + + +try: + from uncertainties import UFloat, ufloat + + unp = None + + HAS_UNCERTAINTIES = True +except ImportError: + UFloat = ufloat = unp = None + + HAS_UNCERTAINTIES = False + + +try: + import numpy as np + from numpy import datetime64 as np_datetime64 + from numpy import ndarray + + HAS_NUMPY = True + NUMPY_VER = np.__version__ + if HAS_UNCERTAINTIES: + from uncertainties import unumpy as unp + + NUMERIC_TYPES = (Number, Decimal, ndarray, np.number, UFloat) + else: + NUMERIC_TYPES = (Number, Decimal, ndarray, np.number) + + def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): + if isinstance(value, (dict, bool)) or value is None: + raise TypeError(f"Invalid magnitude for Quantity: {value!r}") + elif isinstance(value, str) and value == "": + raise ValueError("Quantity magnitude cannot be an empty string.") + elif isinstance(value, (list, tuple)): + return np.asarray(value) + elif HAS_UNCERTAINTIES: + from datasette.vendored.pint.facets.measurement.objects import Measurement + + if isinstance(value, Measurement): + return ufloat(value.value, value.error) + if force_ndarray or ( + force_ndarray_like and not is_duck_array_type(type(value)) + ): + return np.asarray(value) + return value + + def _test_array_function_protocol(): + # Test if the __array_function__ protocol is enabled + try: + + class FakeArray: + def __array_function__(self, *args, **kwargs): + return + + np.concatenate([FakeArray()]) + return True + except ValueError: + return False + + HAS_NUMPY_ARRAY_FUNCTION = _test_array_function_protocol() + + NP_NO_VALUE = np._NoValue + +except ImportError: + np = None + + class ndarray: + pass + + class np_datetime64: + pass + + HAS_NUMPY = False + NUMPY_VER = "0" + NUMERIC_TYPES = (Number, Decimal) + HAS_NUMPY_ARRAY_FUNCTION = False + NP_NO_VALUE = None + + def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): + if force_ndarray or force_ndarray_like: + raise ValueError( + "Cannot force to ndarray or ndarray-like when NumPy is not present." + ) + elif isinstance(value, (dict, bool)) or value is None: + raise TypeError(f"Invalid magnitude for Quantity: {value!r}") + elif isinstance(value, str) and value == "": + raise ValueError("Quantity magnitude cannot be an empty string.") + elif isinstance(value, (list, tuple)): + raise TypeError( + "lists and tuples are valid magnitudes for " + "Quantity only when NumPy is present." + ) + elif HAS_UNCERTAINTIES: + from datasette.vendored.pint.facets.measurement.objects import Measurement + + if isinstance(value, Measurement): + return ufloat(value.value, value.error) + return value + + +try: + from babel import Locale + from babel import units as babel_units + + babel_parse = Locale.parse + + HAS_BABEL = hasattr(babel_units, "format_unit") +except ImportError: + HAS_BABEL = False + + babel_parse = missing_dependency("Babel") # noqa: F811 # type:ignore + babel_units = babel_parse + +try: + import mip + + mip_model = mip.model + mip_Model = mip.Model + mip_INF = mip.INF + mip_INTEGER = mip.INTEGER + mip_xsum = mip.xsum + mip_OptimizationStatus = mip.OptimizationStatus + + HAS_MIP = True +except ImportError: + HAS_MIP = False + + mip_missing = missing_dependency("mip") + mip_model = mip_missing + mip_Model = mip_missing + mip_INF = mip_missing + mip_INTEGER = mip_missing + mip_xsum = mip_missing + mip_OptimizationStatus = mip_missing + +# Defines Logarithm and Exponential for Logarithmic Converter +if HAS_NUMPY: + from numpy import ( + exp, # noqa: F401 + log, # noqa: F401 + ) +else: + from math import ( + exp, # noqa: F401 + log, # noqa: F401 + ) + + +# Define location of pint.Quantity in NEP-13 type cast hierarchy by defining upcast +# types using guarded imports + +try: + from dask import array as dask_array + from dask.base import compute, persist, visualize +except ImportError: + compute, persist, visualize = None, None, None + dask_array = None + + +# TODO: merge with upcast_type_map + +#: List upcast type names +upcast_type_names = ( + "pint_pandas.pint_array.PintArray", + "xarray.core.dataarray.DataArray", + "xarray.core.dataset.Dataset", + "xarray.core.variable.Variable", + "pandas.core.series.Series", + "pandas.core.frame.DataFrame", + "pandas.Series", + "pandas.DataFrame", + "xarray.core.dataarray.DataArray", +) + +#: Map type name to the actual type (for upcast types). +upcast_type_map: Mapping[str, type | None] = {k: None for k in upcast_type_names} + + +def fully_qualified_name(t: type) -> str: + """Return the fully qualified name of a type.""" + module = t.__module__ + name = t.__qualname__ + + if module is None or module == "builtins": + return name + + return f"{module}.{name}" + + +def check_upcast_type(obj: type) -> bool: + """Check if the type object is an upcast type.""" + + # TODO: merge or unify name with is_upcast_type + + 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) + + 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 obj in upcast_type_map.values() + + +def is_upcast_type(other: type) -> bool: + """Check if the type object is an upcast type.""" + + # TODO: merge or unify name with check_upcast_type + + if other in upcast_type_map.values(): + return True + return check_upcast_type(other) + + +def is_duck_array_type(cls: type) -> bool: + """Check if the type object represents a (non-Quantity) duck array type.""" + # TODO (NEP 30): replace duck array check with hasattr(other, "__duckarray__") + return issubclass(cls, ndarray) or ( + not hasattr(cls, "_magnitude") + and not hasattr(cls, "_units") + and HAS_NUMPY_ARRAY_FUNCTION + and hasattr(cls, "__array_function__") + and hasattr(cls, "ndim") + and hasattr(cls, "dtype") + ) + + +def is_duck_array(obj: type) -> bool: + """Check if an object represents a (non-Quantity) duck array type.""" + return is_duck_array_type(type(obj)) + + +def eq(lhs: Any, rhs: Any, check_all: bool) -> bool | Iterable[bool]: + """Comparison of scalars and arrays. + + Parameters + ---------- + lhs + left-hand side + rhs + right-hand side + check_all + if True, reduce sequence to single bool; + return True if all the elements are equal. + + Returns + ------- + bool or array_like of bool + """ + out = lhs == rhs + if check_all and is_duck_array_type(type(out)): + return out.all() + return out + + +def isnan(obj: Any, check_all: bool) -> bool | Iterable[bool]: + """Test for NaN or NaT. + + Parameters + ---------- + obj + scalar or vector + check_all + if True, reduce sequence to single bool; + return True if any of the elements are NaN. + + Returns + ------- + bool or array_like of bool. + Always return False for non-numeric types. + """ + if is_duck_array_type(type(obj)): + if obj.dtype.kind in "ifc": + out = np.isnan(obj) + elif obj.dtype.kind in "Mm": + out = np.isnat(obj) + else: + if HAS_UNCERTAINTIES: + try: + out = unp.isnan(obj) + except TypeError: + # Not a numeric or UFloat type + out = np.full(obj.shape, False) + else: + # Not a numeric or datetime type + out = np.full(obj.shape, False) + return out.any() if check_all else out + if isinstance(obj, np_datetime64): + return np.isnat(obj) + elif HAS_UNCERTAINTIES and isinstance(obj, UFloat): + return unp.isnan(obj) + try: + return math.isnan(obj) + except TypeError: + return False + + +def zero_or_nan(obj: Any, check_all: bool) -> bool | Iterable[bool]: + """Test if obj is zero, NaN, or NaT. + + Parameters + ---------- + obj + scalar or vector + check_all + if True, reduce sequence to single bool; + return True if all the elements are zero, NaN, or NaT. + + Returns + ------- + bool or array_like of bool. + Always return False for non-numeric types. + """ + out = eq(obj, 0, False) + isnan(obj, False) + if check_all and is_duck_array_type(type(out)): + return out.all() + return out diff --git a/datasette/vendored/pint/constants_en.txt b/datasette/vendored/pint/constants_en.txt new file mode 100644 index 0000000000..9babc8fa2e --- /dev/null +++ b/datasette/vendored/pint/constants_en.txt @@ -0,0 +1,74 @@ +# Default Pint constants definition file +# Based on the International System of Units +# Language: english +# Source: https://physics.nist.gov/cuu/Constants/ +# https://physics.nist.gov/PhysRefData/XrayTrans/Html/search.html +# :copyright: 2013,2019 by Pint Authors, see AUTHORS for more details. + +#### MATHEMATICAL CONSTANTS #### +# As computed by Maxima with fpprec:50 + +pi = 3.1415926535897932384626433832795028841971693993751 = π # pi +tansec = 4.8481368111333441675396429478852851658848753880815e-6 # tangent of 1 arc-second ~ arc_second/radian +ln10 = 2.3025850929940456840179914546843642076011014886288 # natural logarithm of 10 +wien_x = 4.9651142317442763036987591313228939440555849867973 # solution to (x-5)*exp(x)+5 = 0 => x = W(5/exp(5))+5 +wien_u = 2.8214393721220788934031913302944851953458817440731 # solution to (u-3)*exp(u)+3 = 0 => u = W(3/exp(3))+3 +eulers_number = 2.71828182845904523536028747135266249775724709369995 + +#### DEFINED EXACT CONSTANTS #### + +speed_of_light = 299792458 m/s = c = c_0 # since 1983 +planck_constant = 6.62607015e-34 J s = ℎ # since May 2019 +elementary_charge = 1.602176634e-19 C = e # since May 2019 +avogadro_number = 6.02214076e23 # since May 2019 +boltzmann_constant = 1.380649e-23 J K^-1 = k = k_B # since May 2019 +standard_gravity = 9.80665 m/s^2 = g_0 = g0 = g_n = gravity # since 1901 +standard_atmosphere = 1.01325e5 Pa = atm = atmosphere # since 1954 +conventional_josephson_constant = 4.835979e14 Hz / V = K_J90 # since Jan 1990 +conventional_von_klitzing_constant = 2.5812807e4 ohm = R_K90 # since Jan 1990 + +#### DERIVED EXACT CONSTANTS #### +# Floating-point conversion may introduce inaccuracies + +zeta = c / (cm/s) = ζ +dirac_constant = ℎ / (2 * π) = ħ = hbar = atomic_unit_of_action = a_u_action +avogadro_constant = avogadro_number * mol^-1 = N_A +molar_gas_constant = k * N_A = R +faraday_constant = e * N_A +conductance_quantum = 2 * e ** 2 / ℎ = G_0 +magnetic_flux_quantum = ℎ / (2 * e) = Φ_0 = Phi_0 +josephson_constant = 2 * e / ℎ = K_J +von_klitzing_constant = ℎ / e ** 2 = R_K +stefan_boltzmann_constant = 2 / 15 * π ** 5 * k ** 4 / (ℎ ** 3 * c ** 2) = σ = sigma +first_radiation_constant = 2 * π * ℎ * c ** 2 = c_1 +second_radiation_constant = ℎ * c / k = c_2 +wien_wavelength_displacement_law_constant = ℎ * c / (k * wien_x) +wien_frequency_displacement_law_constant = wien_u * k / ℎ + +#### MEASURED CONSTANTS #### +# Recommended CODATA-2018 values +# To some extent, what is measured and what is derived is a bit arbitrary. +# The choice of measured constants is based on convenience and on available uncertainty. +# The uncertainty in the last significant digits is given in parentheses as a comment. + +newtonian_constant_of_gravitation = 6.67430e-11 m^3/(kg s^2) = _ = gravitational_constant # (15) +rydberg_constant = 1.0973731568160e7 * m^-1 = R_∞ = R_inf # (21) +electron_g_factor = -2.00231930436256 = g_e # (35) +atomic_mass_constant = 1.66053906660e-27 kg = m_u # (50) +electron_mass = 9.1093837015e-31 kg = m_e = atomic_unit_of_mass = a_u_mass # (28) +proton_mass = 1.67262192369e-27 kg = m_p # (51) +neutron_mass = 1.67492749804e-27 kg = m_n # (95) +lattice_spacing_of_Si = 1.920155716e-10 m = d_220 # (32) +K_alpha_Cu_d_220 = 0.80232719 # (22) +K_alpha_Mo_d_220 = 0.36940604 # (19) +K_alpha_W_d_220 = 0.108852175 # (98) + +#### DERIVED CONSTANTS #### + +fine_structure_constant = (2 * ℎ * R_inf / (m_e * c)) ** 0.5 = α = alpha +vacuum_permeability = 2 * α * ℎ / (e ** 2 * c) = µ_0 = mu_0 = mu0 = magnetic_constant +vacuum_permittivity = e ** 2 / (2 * α * ℎ * c) = ε_0 = epsilon_0 = eps_0 = eps0 = electric_constant +impedance_of_free_space = 2 * α * ℎ / e ** 2 = Z_0 = characteristic_impedance_of_vacuum +coulomb_constant = α * hbar * c / e ** 2 = k_C +classical_electron_radius = α * hbar / (m_e * c) = r_e +thomson_cross_section = 8 / 3 * π * r_e ** 2 = σ_e = sigma_e diff --git a/datasette/vendored/pint/context.py b/datasette/vendored/pint/context.py new file mode 100644 index 0000000000..6c74f6555d --- /dev/null +++ b/datasette/vendored/pint/context.py @@ -0,0 +1,22 @@ +""" + pint.context + ~~~~~~~~~~~~ + + Functions and classes related to context definitions and application. + + :copyright: 2016 by Pint Authors, see AUTHORS for more details.. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + pass + +#: Regex to match the header parts of a context. + +#: Regex to match variable names in an equation. + +# TODO: delete this file diff --git a/datasette/vendored/pint/converters.py b/datasette/vendored/pint/converters.py new file mode 100644 index 0000000000..fbe3b5fb06 --- /dev/null +++ b/datasette/vendored/pint/converters.py @@ -0,0 +1,75 @@ +""" + pint.converters + ~~~~~~~~~~~~~~~ + + Functions and classes related to unit conversions. + + :copyright: 2016 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from dataclasses import fields as dc_fields +from typing import Any, ClassVar + +from ._typing import Magnitude +from .compat import HAS_NUMPY, Self, exp, log # noqa: F401 + + +@dataclass(frozen=True) +class Converter: + """Base class for value converters.""" + + _subclasses: ClassVar[list[type[Converter]]] = [] + _param_names_to_subclass: ClassVar[dict[frozenset[str], type[Converter]]] = {} + + @property + def is_multiplicative(self) -> bool: + return True + + @property + def is_logarithmic(self) -> bool: + return False + + def to_reference(self, value: Magnitude, inplace: bool = False) -> Magnitude: + return value + + def from_reference(self, value: Magnitude, inplace: bool = False) -> Magnitude: + return value + + def __init_subclass__(cls, **kwargs: Any): + # Get constructor parameters + super().__init_subclass__(**kwargs) + cls._subclasses.append(cls) + + @classmethod + def get_field_names(cls, new_cls: type) -> frozenset[str]: + return frozenset(p.name for p in dc_fields(new_cls)) + + @classmethod + def preprocess_kwargs(cls, **kwargs: Any) -> dict[str, Any] | None: + return None + + @classmethod + def from_arguments(cls, **kwargs: Any) -> Converter: + kwk = frozenset(kwargs.keys()) + try: + new_cls = cls._param_names_to_subclass[kwk] + except KeyError: + for new_cls in cls._subclasses: + p_names = frozenset(p.name for p in dc_fields(new_cls)) + if p_names == kwk: + cls._param_names_to_subclass[kwk] = new_cls + break + else: + params = "(" + ", ".join(tuple(kwk)) + ")" + raise ValueError( + f"There is no class registered for parameters {params}" + ) + + kw = new_cls.preprocess_kwargs(**kwargs) + if kw is None: + return new_cls(**kwargs) + return cls.from_arguments(**kw) diff --git a/datasette/vendored/pint/default_en.txt b/datasette/vendored/pint/default_en.txt new file mode 100644 index 0000000000..4250a48cbc --- /dev/null +++ b/datasette/vendored/pint/default_en.txt @@ -0,0 +1,898 @@ +# Default Pint units definition file +# Based on the International System of Units +# Language: english +# :copyright: 2013,2019 by Pint Authors, see AUTHORS for more details. + +# Syntax +# ====== +# Units +# ----- +# = [= ] [= ] [ = ] [...] +# +# The canonical name and aliases should be expressed in singular form. +# Pint automatically deals with plurals built by adding 's' to the singular form; plural +# forms that don't follow this rule should be instead explicitly listed as aliases. +# +# If a unit has no symbol and one wants to define aliases, then the symbol should be +# conventionally set to _. +# +# Example: +# millennium = 1e3 * year = _ = millennia +# +# +# Prefixes +# -------- +# - = [= ] [= ] [ = ] [...] +# +# Example: +# deca- = 1e+1 = da- = deka- +# +# +# Derived dimensions +# ------------------ +# [dimension name] = +# +# Example: +# [density] = [mass] / [volume] +# +# Note that primary dimensions don't need to be declared; they can be +# defined for the first time in a unit definition. +# E.g. see below `meter = [length]` +# +# +# Additional aliases +# ------------------ +# @alias = [ = ] [...] +# +# Used to add aliases to already existing unit definitions. +# Particularly useful when one wants to enrich definitions +# from defaults_en.txt with custom aliases. +# +# Example: +# @alias meter = my_meter + +# See also: https://pint.readthedocs.io/en/latest/defining.html + +@defaults + group = international + system = mks +@end + + +#### PREFIXES #### + +# decimal prefixes +quecto- = 1e-30 = q- +ronto- = 1e-27 = r- +yocto- = 1e-24 = y- +zepto- = 1e-21 = z- +atto- = 1e-18 = a- +femto- = 1e-15 = f- +pico- = 1e-12 = p- +nano- = 1e-9 = n- +# The micro (U+00B5) and Greek mu (U+03BC) are both valid prefixes, +# and they often use the same glyph. +micro- = 1e-6 = µ- = μ- = u- = mu- = mc- +milli- = 1e-3 = m- +centi- = 1e-2 = c- +deci- = 1e-1 = d- +deca- = 1e+1 = da- = deka- +hecto- = 1e2 = h- +kilo- = 1e3 = k- +mega- = 1e6 = M- +giga- = 1e9 = G- +tera- = 1e12 = T- +peta- = 1e15 = P- +exa- = 1e18 = E- +zetta- = 1e21 = Z- +yotta- = 1e24 = Y- +ronna- = 1e27 = R- +quetta- = 1e30 = Q- + +# binary_prefixes +kibi- = 2**10 = Ki- +mebi- = 2**20 = Mi- +gibi- = 2**30 = Gi- +tebi- = 2**40 = Ti- +pebi- = 2**50 = Pi- +exbi- = 2**60 = Ei- +zebi- = 2**70 = Zi- +yobi- = 2**80 = Yi- + +# extra_prefixes +semi- = 0.5 = _ = demi- +sesqui- = 1.5 + + +#### BASE UNITS #### + +meter = [length] = m = metre +second = [time] = s = sec +ampere = [current] = A = amp +candela = [luminosity] = cd = candle +gram = [mass] = g +mole = [substance] = mol +kelvin = [temperature]; offset: 0 = K = degK = °K = degree_Kelvin = degreeK # older names supported for compatibility +radian = [] = rad +bit = [] +count = [] + + +#### CONSTANTS #### + +@import constants_en.txt + + +#### UNITS #### +# Common and less common, grouped by quantity. +# Conversion factors are exact (except when noted), +# although floating-point conversion may introduce inaccuracies + +# Angle +turn = 2 * π * radian = _ = revolution = cycle = circle +degree = π / 180 * radian = deg = arcdeg = arcdegree = angular_degree +arcminute = degree / 60 = arcmin = arc_minute = angular_minute +arcsecond = arcminute / 60 = arcsec = arc_second = angular_second +milliarcsecond = 1e-3 * arcsecond = mas +grade = π / 200 * radian = grad = gon +mil = π / 32000 * radian + +# Solid angle +steradian = radian ** 2 = sr +square_degree = (π / 180) ** 2 * sr = sq_deg = sqdeg + +# Information +baud = bit / second = Bd = bps + +byte = 8 * bit = B = octet +# byte = 8 * bit = _ = octet +## NOTE: B (byte) symbol can conflict with Bell + +# Ratios +percent = 0.01 = % +permille = 0.001 = ‰ +ppm = 1e-6 + +# Length +angstrom = 1e-10 * meter = Å = ångström = Å +micron = micrometer = µ = μ +fermi = femtometer = fm +light_year = speed_of_light * julian_year = ly = lightyear +astronomical_unit = 149597870700 * meter = au # since Aug 2012 +parsec = 1 / tansec * astronomical_unit = pc +nautical_mile = 1852 * meter = nmi +bohr = hbar / (alpha * m_e * c) = a_0 = a0 = bohr_radius = atomic_unit_of_length = a_u_length +x_unit_Cu = K_alpha_Cu_d_220 * d_220 / 1537.4 = Xu_Cu +x_unit_Mo = K_alpha_Mo_d_220 * d_220 / 707.831 = Xu_Mo +angstrom_star = K_alpha_W_d_220 * d_220 / 0.2090100 = Å_star +planck_length = (hbar * gravitational_constant / c ** 3) ** 0.5 + +# Mass +metric_ton = 1e3 * kilogram = t = tonne +unified_atomic_mass_unit = atomic_mass_constant = u = amu +dalton = atomic_mass_constant = Da +grain = 64.79891 * milligram = gr +gamma_mass = microgram +carat = 200 * milligram = ct = karat +planck_mass = (hbar * c / gravitational_constant) ** 0.5 + +# Time +minute = 60 * second = min +hour = 60 * minute = h = hr +day = 24 * hour = d +week = 7 * day +fortnight = 2 * week +year = 365.25 * day = a = yr = julian_year +month = year / 12 + +# decade = 10 * year +## NOTE: decade [time] can conflict with decade [dimensionless] + +century = 100 * year = _ = centuries +millennium = 1e3 * year = _ = millennia +eon = 1e9 * year +shake = 1e-8 * second +svedberg = 1e-13 * second +atomic_unit_of_time = hbar / E_h = a_u_time +gregorian_year = 365.2425 * day +sidereal_year = 365.256363004 * day # approximate, as of J2000 epoch +tropical_year = 365.242190402 * day # approximate, as of J2000 epoch +common_year = 365 * day +leap_year = 366 * day +sidereal_day = day / 1.00273790935079524 # approximate +sidereal_month = 27.32166155 * day # approximate +tropical_month = 27.321582 * day # approximate +synodic_month = 29.530589 * day = _ = lunar_month # approximate +planck_time = (hbar * gravitational_constant / c ** 5) ** 0.5 + +# Temperature +degree_Celsius = kelvin; offset: 273.15 = °C = celsius = degC = degreeC +degree_Rankine = 5 / 9 * kelvin; offset: 0 = °R = rankine = degR = degreeR +degree_Fahrenheit = 5 / 9 * kelvin; offset: 233.15 + 200 / 9 = °F = fahrenheit = degF = degreeF +degree_Reaumur = 4 / 5 * kelvin; offset: 273.15 = °Re = reaumur = degRe = degreeRe = degree_Réaumur = réaumur +atomic_unit_of_temperature = E_h / k = a_u_temp +planck_temperature = (hbar * c ** 5 / gravitational_constant / k ** 2) ** 0.5 + +# Area +[area] = [length] ** 2 +are = 100 * meter ** 2 +barn = 1e-28 * meter ** 2 = b +darcy = centipoise * centimeter ** 2 / (second * atmosphere) +hectare = 100 * are = ha + +# Volume +[volume] = [length] ** 3 +liter = decimeter ** 3 = l = L = ℓ = litre +cubic_centimeter = centimeter ** 3 = cc +lambda = microliter = λ +stere = meter ** 3 + +# Frequency +[frequency] = 1 / [time] +hertz = 1 / second = Hz +revolutions_per_minute = revolution / minute = rpm +revolutions_per_second = revolution / second = rps +counts_per_second = count / second = cps + +# Wavenumber +[wavenumber] = 1 / [length] +reciprocal_centimeter = 1 / cm = cm_1 = kayser + +# Velocity +[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 +kilometer_per_second = kilometer / second = kps +meter_per_second = meter / second = mps +foot_per_second = foot / second = fps + +# Volumetric Flow Rate +[volumetric_flow_rate] = [volume] / [time] +sverdrup = 1e6 * meter ** 3 / second = sv + +# Acceleration +[acceleration] = [velocity] / [time] +galileo = centimeter / second ** 2 = Gal + +# Force +[force] = [mass] * [acceleration] +newton = kilogram * meter / second ** 2 = N +dyne = gram * centimeter / second ** 2 = dyn +force_kilogram = g_0 * kilogram = kgf = kilogram_force = pond +force_gram = g_0 * gram = gf = gram_force +force_metric_ton = g_0 * metric_ton = tf = metric_ton_force = force_t = t_force +atomic_unit_of_force = E_h / a_0 = a_u_force + +# Energy +[energy] = [force] * [length] +joule = newton * meter = J +erg = dyne * centimeter +watt_hour = watt * hour = Wh = watthour +electron_volt = e * volt = eV +rydberg = ℎ * c * R_inf = Ry +hartree = 2 * rydberg = E_h = Eh = hartree_energy = atomic_unit_of_energy = a_u_energy +calorie = 4.184 * joule = cal = thermochemical_calorie = cal_th +international_calorie = 4.1868 * joule = cal_it = international_steam_table_calorie +fifteen_degree_calorie = 4.1855 * joule = cal_15 +british_thermal_unit = 1055.056 * joule = Btu = BTU = Btu_iso +international_british_thermal_unit = 1e3 * pound / kilogram * degR / kelvin * international_calorie = Btu_it +thermochemical_british_thermal_unit = 1e3 * pound / kilogram * degR / kelvin * calorie = Btu_th +quadrillion_Btu = 1e15 * Btu = quad +therm = 1e5 * Btu = thm = EC_therm +US_therm = 1.054804e8 * joule # approximate, no exact definition +ton_TNT = 1e9 * calorie = tTNT +tonne_of_oil_equivalent = 1e10 * international_calorie = toe +atmosphere_liter = atmosphere * liter = atm_l + +# Power +[power] = [energy] / [time] +watt = joule / second = W +volt_ampere = volt * ampere = VA +horsepower = 550 * foot * force_pound / second = hp = UK_horsepower = hydraulic_horsepower +boiler_horsepower = 33475 * Btu / hour # unclear which Btu +metric_horsepower = 75 * force_kilogram * meter / second +electrical_horsepower = 746 * watt +refrigeration_ton = 12e3 * Btu / hour = _ = ton_of_refrigeration # approximate, no exact definition +cooling_tower_ton = 1.25 * refrigeration_ton # approximate, no exact definition +standard_liter_per_minute = atmosphere * liter / minute = slpm = slm +conventional_watt_90 = K_J90 ** 2 * R_K90 / (K_J ** 2 * R_K) * watt = W_90 + +# Momentum +[momentum] = [length] * [mass] / [time] + +# Density (as auxiliary for pressure) +[density] = [mass] / [volume] +mercury = 13.5951 * kilogram / liter = Hg = Hg_0C = Hg_32F = conventional_mercury +water = 1.0 * kilogram / liter = H2O = conventional_water +mercury_60F = 13.5568 * kilogram / liter = Hg_60F # approximate +water_39F = 0.999972 * kilogram / liter = water_4C # approximate +water_60F = 0.999001 * kilogram / liter # approximate + +# Pressure +[pressure] = [force] / [area] +pascal = newton / meter ** 2 = Pa +barye = dyne / centimeter ** 2 = Ba = barie = barad = barrie = baryd +bar = 1e5 * pascal +technical_atmosphere = kilogram * g_0 / centimeter ** 2 = at +torr = atm / 760 +pound_force_per_square_inch = force_pound / inch ** 2 = psi +kip_per_square_inch = kip / inch ** 2 = ksi +millimeter_Hg = millimeter * Hg * g_0 = mmHg = mm_Hg = millimeter_Hg_0C +centimeter_Hg = centimeter * Hg * g_0 = cmHg = cm_Hg = centimeter_Hg_0C +inch_Hg = inch * Hg * g_0 = inHg = in_Hg = inch_Hg_32F +inch_Hg_60F = inch * Hg_60F * g_0 +inch_H2O_39F = inch * water_39F * g_0 +inch_H2O_60F = inch * water_60F * g_0 +foot_H2O = foot * water * g_0 = ftH2O = feet_H2O +centimeter_H2O = centimeter * water * g_0 = cmH2O = cm_H2O +sound_pressure_level = 20e-6 * pascal = SPL + +# Torque +[torque] = [force] * [length] +foot_pound = foot * force_pound = ft_lb = footpound + +# Viscosity +[viscosity] = [pressure] * [time] +poise = 0.1 * Pa * second = P +reyn = psi * second + +# Kinematic viscosity +[kinematic_viscosity] = [area] / [time] +stokes = centimeter ** 2 / second = St + +# Fluidity +[fluidity] = 1 / [viscosity] +rhe = 1 / poise + +# Amount of substance +particle = 1 / N_A = _ = molec = molecule + +# Concentration +[concentration] = [substance] / [volume] +molar = mole / liter = M + +# Catalytic activity +[activity] = [substance] / [time] +katal = mole / second = kat +enzyme_unit = micromole / minute = U = enzymeunit + +# Entropy +[entropy] = [energy] / [temperature] +clausius = calorie / kelvin = Cl + +# Molar entropy +[molar_entropy] = [entropy] / [substance] +entropy_unit = calorie / kelvin / mole = eu + +# Radiation +becquerel = counts_per_second = Bq +curie = 3.7e10 * becquerel = Ci +rutherford = 1e6 * becquerel = Rd +gray = joule / kilogram = Gy +sievert = joule / kilogram = Sv +rads = 0.01 * gray +rem = 0.01 * sievert +roentgen = 2.58e-4 * coulomb / kilogram = _ = röntgen # approximate, depends on medium + +# Heat transimission +[heat_transmission] = [energy] / [area] +peak_sun_hour = 1e3 * watt_hour / meter ** 2 = PSH +langley = thermochemical_calorie / centimeter ** 2 = Ly + +# Luminance +[luminance] = [luminosity] / [area] +nit = candela / meter ** 2 +stilb = candela / centimeter ** 2 +lambert = 1 / π * candela / centimeter ** 2 + +# Luminous flux +[luminous_flux] = [luminosity] +lumen = candela * steradian = lm + +# Illuminance +[illuminance] = [luminous_flux] / [area] +lux = lumen / meter ** 2 = lx + +# Intensity +[intensity] = [power] / [area] +atomic_unit_of_intensity = 0.5 * ε_0 * c * atomic_unit_of_electric_field ** 2 = a_u_intensity + +# Current +biot = 10 * ampere = Bi +abampere = biot = abA +atomic_unit_of_current = e / atomic_unit_of_time = a_u_current +mean_international_ampere = mean_international_volt / mean_international_ohm = A_it +US_international_ampere = US_international_volt / US_international_ohm = A_US +conventional_ampere_90 = K_J90 * R_K90 / (K_J * R_K) * ampere = A_90 +planck_current = (c ** 6 / gravitational_constant / k_C) ** 0.5 + +# Charge +[charge] = [current] * [time] +coulomb = ampere * second = C +abcoulomb = 10 * C = abC +faraday = e * N_A * mole +conventional_coulomb_90 = K_J90 * R_K90 / (K_J * R_K) * coulomb = C_90 +ampere_hour = ampere * hour = Ah + +# Electric potential +[electric_potential] = [energy] / [charge] +volt = joule / coulomb = V +abvolt = 1e-8 * volt = abV +mean_international_volt = 1.00034 * volt = V_it # approximate +US_international_volt = 1.00033 * volt = V_US # approximate +conventional_volt_90 = K_J90 / K_J * volt = V_90 + +# Electric field +[electric_field] = [electric_potential] / [length] +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 = Ω +abohm = 1e-9 * ohm = abΩ +mean_international_ohm = 1.00049 * ohm = Ω_it = ohm_it # approximate +US_international_ohm = 1.000495 * ohm = Ω_US = ohm_US # approximate +conventional_ohm_90 = R_K / R_K90 * ohm = Ω_90 = ohm_90 + +# Resistivity +[resistivity] = [resistance] * [length] + +# Conductance +[conductance] = [current] / [electric_potential] +siemens = ampere / volt = S = mho +absiemens = 1e9 * siemens = abS = abmho + +# Capacitance +[capacitance] = [charge] / [electric_potential] +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 field +[magnetic_field] = [magnetic_flux] / [area] +tesla = weber / meter ** 2 = T +gamma = 1e-9 * tesla = γ + +# Magnetomotive force +[magnetomotive_force] = [current] +ampere_turn = ampere = At +biot_turn = biot +gilbert = 1 / (4 * π) * biot_turn = Gb + +# Magnetic field strength +[magnetic_field_strength] = [current] / [length] + +# Electric dipole moment +[electric_dipole] = [charge] * [length] +debye = 1e-9 / ζ * coulomb * angstrom = D # formally 1 D = 1e-10 Fr*Å, but we generally want to use it outside the Gaussian context + +# Electric quadrupole moment +[electric_quadrupole] = [charge] * [area] +buckingham = debye * angstrom + +# Magnetic dipole moment +[magnetic_dipole] = [current] * [area] +bohr_magneton = e * hbar / (2 * m_e) = µ_B = mu_B +nuclear_magneton = e * hbar / (2 * m_p) = µ_N = mu_N + +# Refractive index +[refractive_index] = [] +refractive_index_unit = [] = RIU + +# Logaritmic Unit Definition +# Unit = scale; logbase; logfactor +# x_dB = [logfactor] * log( x_lin / [scale] ) / log( [logbase] ) + +# Logaritmic Units of dimensionless quantity: [ https://en.wikipedia.org/wiki/Level_(logarithmic_quantity) ] + +decibelwatt = watt; logbase: 10; logfactor: 10 = dBW +decibelmilliwatt = 1e-3 watt; logbase: 10; logfactor: 10 = dBm +decibelmicrowatt = 1e-6 watt; logbase: 10; logfactor: 10 = dBu + +decibel = 1 ; logbase: 10; logfactor: 10 = dB +# bell = 1 ; logbase: 10; logfactor: = B +## NOTE: B (Bell) symbol conflicts with byte + +decade = 1 ; logbase: 10; logfactor: 1 +## NOTE: decade [time] can conflict with decade [dimensionless] + +octave = 1 ; logbase: 2; logfactor: 1 = oct + +neper = 1 ; logbase: 2.71828182845904523536028747135266249775724709369995; logfactor: 0.5 = Np +# neper = 1 ; logbase: eulers_number; logfactor: 0.5 = Np + +#### UNIT GROUPS #### +# Mostly for length, area, volume, mass, force +# (customary or specialized units) + +@group USCSLengthInternational + thou = 1e-3 * inch = th = mil_length + inch = yard / 36 = in = international_inch = inches = international_inches + hand = 4 * inch + foot = yard / 3 = ft = international_foot = feet = international_feet + yard = 0.9144 * meter = yd = international_yard # since Jul 1959 + mile = 1760 * yard = mi = international_mile + + circular_mil = π / 4 * mil_length ** 2 = cmil + square_inch = inch ** 2 = sq_in = square_inches + square_foot = foot ** 2 = sq_ft = square_feet + square_yard = yard ** 2 = sq_yd + square_mile = mile ** 2 = sq_mi + + cubic_inch = in ** 3 = cu_in + cubic_foot = ft ** 3 = cu_ft = cubic_feet + cubic_yard = yd ** 3 = cu_yd +@end + +@group USCSLengthSurvey + link = 1e-2 * chain = li = survey_link + survey_foot = 1200 / 3937 * meter = sft + fathom = 6 * survey_foot + rod = 16.5 * survey_foot = rd = pole = perch + chain = 4 * rod + furlong = 40 * rod = fur + cables_length = 120 * fathom + survey_mile = 5280 * survey_foot = smi = us_statute_mile + league = 3 * survey_mile + + square_rod = rod ** 2 = sq_rod = sq_pole = sq_perch + acre = 10 * chain ** 2 + square_survey_mile = survey_mile ** 2 = _ = section + square_league = league ** 2 + + acre_foot = acre * survey_foot = _ = acre_feet +@end + +@group USCSDryVolume + dry_pint = bushel / 64 = dpi = US_dry_pint + dry_quart = bushel / 32 = dqt = US_dry_quart + dry_gallon = bushel / 8 = dgal = US_dry_gallon + peck = bushel / 4 = pk + bushel = 2150.42 cubic_inch = bu + dry_barrel = 7056 cubic_inch = _ = US_dry_barrel + board_foot = ft * ft * in = FBM = board_feet = BF = BDFT = super_foot = superficial_foot = super_feet = superficial_feet +@end + +@group USCSLiquidVolume + minim = pint / 7680 + fluid_dram = pint / 128 = fldr = fluidram = US_fluid_dram = US_liquid_dram + fluid_ounce = pint / 16 = floz = US_fluid_ounce = US_liquid_ounce + gill = pint / 4 = gi = liquid_gill = US_liquid_gill + pint = quart / 2 = pt = liquid_pint = US_pint + fifth = gallon / 5 = _ = US_liquid_fifth + quart = gallon / 4 = qt = liquid_quart = US_liquid_quart + gallon = 231 * cubic_inch = gal = liquid_gallon = US_liquid_gallon +@end + +@group USCSVolumeOther + teaspoon = fluid_ounce / 6 = tsp + tablespoon = fluid_ounce / 2 = tbsp + shot = 3 * tablespoon = jig = US_shot + cup = pint / 2 = cp = liquid_cup = US_liquid_cup + barrel = 31.5 * gallon = bbl + oil_barrel = 42 * gallon = oil_bbl + beer_barrel = 31 * gallon = beer_bbl + hogshead = 63 * gallon +@end + +@group Avoirdupois + dram = pound / 256 = dr = avoirdupois_dram = avdp_dram = drachm + ounce = pound / 16 = oz = avoirdupois_ounce = avdp_ounce + pound = 7e3 * grain = lb = avoirdupois_pound = avdp_pound + stone = 14 * pound + quarter = 28 * stone + bag = 94 * pound + hundredweight = 100 * pound = cwt = short_hundredweight + long_hundredweight = 112 * pound + ton = 2e3 * pound = _ = short_ton + long_ton = 2240 * pound + slug = g_0 * pound * second ** 2 / foot + slinch = g_0 * pound * second ** 2 / inch = blob = slugette + + force_ounce = g_0 * ounce = ozf = ounce_force + force_pound = g_0 * pound = lbf = pound_force + force_ton = g_0 * ton = _ = ton_force = force_short_ton = short_ton_force + force_long_ton = g_0 * long_ton = _ = long_ton_force + kip = 1e3 * force_pound + poundal = pound * foot / second ** 2 = pdl +@end + +@group AvoirdupoisUK using Avoirdupois + UK_hundredweight = long_hundredweight = UK_cwt + UK_ton = long_ton + UK_force_ton = force_long_ton = _ = UK_ton_force +@end + +@group AvoirdupoisUS using Avoirdupois + US_hundredweight = hundredweight = US_cwt + US_ton = ton + US_force_ton = force_ton = _ = US_ton_force +@end + +@group Troy + pennyweight = 24 * grain = dwt + troy_ounce = 480 * grain = toz = ozt + troy_pound = 12 * troy_ounce = tlb = lbt +@end + +@group Apothecary + scruple = 20 * grain + apothecary_dram = 3 * scruple = ap_dr + apothecary_ounce = 8 * apothecary_dram = ap_oz + apothecary_pound = 12 * apothecary_ounce = ap_lb +@end + +@group ImperialVolume + imperial_minim = imperial_fluid_ounce / 480 + imperial_fluid_scruple = imperial_fluid_ounce / 24 + imperial_fluid_drachm = imperial_fluid_ounce / 8 = imperial_fldr = imperial_fluid_dram + imperial_fluid_ounce = imperial_pint / 20 = imperial_floz = UK_fluid_ounce + imperial_gill = imperial_pint / 4 = imperial_gi = UK_gill + imperial_cup = imperial_pint / 2 = imperial_cp = UK_cup + imperial_pint = imperial_gallon / 8 = imperial_pt = UK_pint + imperial_quart = imperial_gallon / 4 = imperial_qt = UK_quart + imperial_gallon = 4.54609 * liter = imperial_gal = UK_gallon + imperial_peck = 2 * imperial_gallon = imperial_pk = UK_pk + imperial_bushel = 8 * imperial_gallon = imperial_bu = UK_bushel + imperial_barrel = 36 * imperial_gallon = imperial_bbl = UK_bbl +@end + +@group Printer + pica = inch / 6 = _ = printers_pica + point = pica / 12 = pp = printers_point = big_point = bp + didot = 1 / 2660 * m + cicero = 12 * didot + tex_point = inch / 72.27 + tex_pica = 12 * tex_point + tex_didot = 1238 / 1157 * tex_point + tex_cicero = 12 * tex_didot + scaled_point = tex_point / 65536 + css_pixel = inch / 96 = px + + pixel = [printing_unit] = _ = dot = pel = picture_element + pixels_per_centimeter = pixel / cm = PPCM + pixels_per_inch = pixel / inch = dots_per_inch = PPI = ppi = DPI = printers_dpi + bits_per_pixel = bit / pixel = bpp +@end + +@group Textile + tex = gram / kilometer = Tt + dtex = decitex + denier = gram / (9 * kilometer) = den + jute = pound / (14400 * yard) = Tj + aberdeen = jute = Ta + RKM = gf / tex + + number_english = 840 * yard / pound = Ne = NeC = ECC + number_meter = kilometer / kilogram = Nm +@end + + +#### CGS ELECTROMAGNETIC UNITS #### + +# === Gaussian system of units === +@group Gaussian + franklin = erg ** 0.5 * centimeter ** 0.5 = Fr = statcoulomb = statC = esu + statvolt = erg / franklin = statV + statampere = franklin / second = statA + gauss = dyne / franklin = G + maxwell = gauss * centimeter ** 2 = Mx + oersted = dyne / maxwell = Oe = ørsted + statohm = statvolt / statampere = statΩ + statfarad = franklin / statvolt = statF + statmho = statampere / statvolt +@end +# Note this system is not commensurate with SI, as ε_0 and µ_0 disappear; +# some quantities with different dimensions in SI have the same +# dimensions in the Gaussian system (e.g. [Mx] = [Fr], but [Wb] != [C]), +# and therefore the conversion factors depend on the context (not in pint sense) +[gaussian_charge] = [length] ** 1.5 * [mass] ** 0.5 / [time] +[gaussian_current] = [gaussian_charge] / [time] +[gaussian_electric_potential] = [gaussian_charge] / [length] +[gaussian_electric_field] = [gaussian_electric_potential] / [length] +[gaussian_electric_displacement_field] = [gaussian_charge] / [area] +[gaussian_electric_flux] = [gaussian_charge] +[gaussian_electric_dipole] = [gaussian_charge] * [length] +[gaussian_electric_quadrupole] = [gaussian_charge] * [area] +[gaussian_magnetic_field] = [force] / [gaussian_charge] +[gaussian_magnetic_field_strength] = [gaussian_magnetic_field] +[gaussian_magnetic_flux] = [gaussian_magnetic_field] * [area] +[gaussian_magnetic_dipole] = [energy] / [gaussian_magnetic_field] +[gaussian_resistance] = [gaussian_electric_potential] / [gaussian_current] +[gaussian_resistivity] = [gaussian_resistance] * [length] +[gaussian_capacitance] = [gaussian_charge] / [gaussian_electric_potential] +[gaussian_inductance] = [gaussian_electric_potential] * [time] / [gaussian_current] +[gaussian_conductance] = [gaussian_current] / [gaussian_electric_potential] +@context Gaussian = Gau + [gaussian_charge] -> [charge]: value / k_C ** 0.5 + [charge] -> [gaussian_charge]: value * k_C ** 0.5 + [gaussian_current] -> [current]: value / k_C ** 0.5 + [current] -> [gaussian_current]: value * k_C ** 0.5 + [gaussian_electric_potential] -> [electric_potential]: value * k_C ** 0.5 + [electric_potential] -> [gaussian_electric_potential]: value / k_C ** 0.5 + [gaussian_electric_field] -> [electric_field]: value * k_C ** 0.5 + [electric_field] -> [gaussian_electric_field]: value / k_C ** 0.5 + [gaussian_electric_displacement_field] -> [electric_displacement_field]: value / (4 * π / ε_0) ** 0.5 + [electric_displacement_field] -> [gaussian_electric_displacement_field]: value * (4 * π / ε_0) ** 0.5 + [gaussian_electric_dipole] -> [electric_dipole]: value / k_C ** 0.5 + [electric_dipole] -> [gaussian_electric_dipole]: value * k_C ** 0.5 + [gaussian_electric_quadrupole] -> [electric_quadrupole]: value / k_C ** 0.5 + [electric_quadrupole] -> [gaussian_electric_quadrupole]: value * k_C ** 0.5 + [gaussian_magnetic_field] -> [magnetic_field]: value / (4 * π / µ_0) ** 0.5 + [magnetic_field] -> [gaussian_magnetic_field]: value * (4 * π / µ_0) ** 0.5 + [gaussian_magnetic_flux] -> [magnetic_flux]: value / (4 * π / µ_0) ** 0.5 + [magnetic_flux] -> [gaussian_magnetic_flux]: value * (4 * π / µ_0) ** 0.5 + [gaussian_magnetic_field_strength] -> [magnetic_field_strength]: value / (4 * π * µ_0) ** 0.5 + [magnetic_field_strength] -> [gaussian_magnetic_field_strength]: value * (4 * π * µ_0) ** 0.5 + [gaussian_magnetic_dipole] -> [magnetic_dipole]: value * (4 * π / µ_0) ** 0.5 + [magnetic_dipole] -> [gaussian_magnetic_dipole]: value / (4 * π / µ_0) ** 0.5 + [gaussian_resistance] -> [resistance]: value * k_C + [resistance] -> [gaussian_resistance]: value / k_C + [gaussian_resistivity] -> [resistivity]: value * k_C + [resistivity] -> [gaussian_resistivity]: value / k_C + [gaussian_capacitance] -> [capacitance]: value / k_C + [capacitance] -> [gaussian_capacitance]: value * k_C + [gaussian_inductance] -> [inductance]: value * k_C + [inductance] -> [gaussian_inductance]: value / k_C + [gaussian_conductance] -> [conductance]: value / k_C + [conductance] -> [gaussian_conductance]: value * k_C +@end + +# === ESU system of units === +# (where different from Gaussian) +# See note for Gaussian system too +@group ESU using Gaussian + statweber = statvolt * second = statWb + stattesla = statweber / centimeter ** 2 = statT + stathenry = statweber / statampere = statH +@end +[esu_charge] = [length] ** 1.5 * [mass] ** 0.5 / [time] +[esu_current] = [esu_charge] / [time] +[esu_electric_potential] = [esu_charge] / [length] +[esu_magnetic_flux] = [esu_electric_potential] * [time] +[esu_magnetic_field] = [esu_magnetic_flux] / [area] +[esu_magnetic_field_strength] = [esu_current] / [length] +[esu_magnetic_dipole] = [esu_current] * [area] +@context ESU = esu + [esu_magnetic_field] -> [magnetic_field]: value * k_C ** 0.5 + [magnetic_field] -> [esu_magnetic_field]: value / k_C ** 0.5 + [esu_magnetic_flux] -> [magnetic_flux]: value * k_C ** 0.5 + [magnetic_flux] -> [esu_magnetic_flux]: value / k_C ** 0.5 + [esu_magnetic_field_strength] -> [magnetic_field_strength]: value / (4 * π / ε_0) ** 0.5 + [magnetic_field_strength] -> [esu_magnetic_field_strength]: value * (4 * π / ε_0) ** 0.5 + [esu_magnetic_dipole] -> [magnetic_dipole]: value / k_C ** 0.5 + [magnetic_dipole] -> [esu_magnetic_dipole]: value * k_C ** 0.5 +@end + + +#### CONVERSION CONTEXTS #### + +@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 + +@context boltzmann + [temperature] -> [energy]: boltzmann_constant * value + [energy] -> [temperature]: value / boltzmann_constant +@end + +@context energy + [energy] -> [energy] / [substance]: value * N_A + [energy] / [substance] -> [energy]: value / N_A + [energy] -> [mass]: value / c ** 2 + [mass] -> [energy]: value * c ** 2 +@end + +@context(mw=0,volume=0,solvent_mass=0) chemistry = chem + # mw is the molecular weight of the species + # volume is the volume of the solution + # solvent_mass is the mass of solvent in the solution + + # moles -> mass require the molecular weight + [substance] -> [mass]: value * mw + [mass] -> [substance]: value / mw + + # moles/volume -> mass/volume and moles/mass -> mass/mass + # require the molecular weight + [substance] / [volume] -> [mass] / [volume]: value * mw + [mass] / [volume] -> [substance] / [volume]: value / mw + [substance] / [mass] -> [mass] / [mass]: value * mw + [mass] / [mass] -> [substance] / [mass]: value / mw + + # moles/volume -> moles requires the solution volume + [substance] / [volume] -> [substance]: value * volume + [substance] -> [substance] / [volume]: value / volume + + # moles/mass -> moles requires the solvent (usually water) mass + [substance] / [mass] -> [substance]: value * solvent_mass + [substance] -> [substance] / [mass]: value / solvent_mass + + # moles/mass -> moles/volume require the solvent mass and the volume + [substance] / [mass] -> [substance]/[volume]: value * solvent_mass / volume + [substance] / [volume] -> [substance] / [mass]: value / solvent_mass * volume + +@end + +@context textile + # Allow switching between Direct count system (i.e. tex) and + # Indirect count system (i.e. Ne, Nm) + [mass] / [length] <-> [length] / [mass]: 1 / value +@end + + +#### SYSTEMS OF UNITS #### + +@system SI + second + meter + kilogram + ampere + kelvin + mole + candela +@end + +@system mks using international + meter + kilogram + second +@end + +@system cgs using international, Gaussian, ESU + centimeter + gram + second +@end + +@system atomic using international + # based on unit m_e, e, hbar, k_C, k + bohr: meter + electron_mass: gram + atomic_unit_of_time: second + atomic_unit_of_current: ampere + atomic_unit_of_temperature: kelvin +@end + +@system Planck using international + # based on unit c, gravitational_constant, hbar, k_C, k + planck_length: meter + planck_mass: gram + planck_time: second + planck_current: ampere + planck_temperature: kelvin +@end + +@system imperial using ImperialVolume, USCSLengthInternational, AvoirdupoisUK + yard + pound +@end + +@system US using USCSLiquidVolume, USCSDryVolume, USCSVolumeOther, USCSLengthInternational, USCSLengthSurvey, AvoirdupoisUS + yard + pound +@end diff --git a/datasette/vendored/pint/definitions.py b/datasette/vendored/pint/definitions.py new file mode 100644 index 0000000000..8a6cc496f7 --- /dev/null +++ b/datasette/vendored/pint/definitions.py @@ -0,0 +1,47 @@ +""" + pint.definitions + ~~~~~~~~~~~~~~~~ + + Kept for backwards compatibility + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import flexparser as fp + +from . import errors +from .delegates import ParserConfig, txt_defparser + + +class Definition: + """This is kept for backwards compatibility""" + + @classmethod + def from_string(cls, input_string: str, non_int_type: type = float) -> Definition: + """Parse a string into a definition object. + + Parameters + ---------- + input_string + Single line string. + non_int_type + Numerical type used for non integer values. + + Raises + ------ + DefinitionSyntaxError + If a syntax error was found. + """ + cfg = ParserConfig(non_int_type) + parser = txt_defparser.DefParser(cfg, None) + pp = parser.parse_string(input_string) + 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 + + # TODO: What shall we do in this return path. diff --git a/datasette/vendored/pint/delegates/__init__.py b/datasette/vendored/pint/delegates/__init__.py new file mode 100644 index 0000000000..639d9618b3 --- /dev/null +++ b/datasette/vendored/pint/delegates/__init__.py @@ -0,0 +1,17 @@ +""" + 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 __future__ import annotations + +from . import txt_defparser +from .base_defparser import ParserConfig, build_disk_cache_class +from .formatter import Formatter + +__all__ = ["txt_defparser", "ParserConfig", "build_disk_cache_class", "Formatter"] diff --git a/datasette/vendored/pint/delegates/base_defparser.py b/datasette/vendored/pint/delegates/base_defparser.py new file mode 100644 index 0000000000..ac19922a82 --- /dev/null +++ b/datasette/vendored/pint/delegates/base_defparser.py @@ -0,0 +1,111 @@ +""" + 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 +from dataclasses import dataclass +from typing import Any + +import flexcache as fc +import flexparser as fp + +from datasette.vendored.pint import errors +from datasette.vendored.pint.facets.plain.definitions import NotNumeric +from datasette.vendored.pint.util import ParserHelper, UnitsContainer + + +@dataclass(frozen=True) +class ParserConfig: + """Configuration used by the parser in Pint.""" + + #: Indicates the output type of non integer numbers. + non_int_type: 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 + + +@dataclass(frozen=True) +class PintParsedStatement(fp.ParsedStatement[ParserConfig]): + """A parsed statement for pint, specialized in the actual config.""" + + +@functools.lru_cache +def build_disk_cache_class(chosen_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 = chosen_non_int_type.__qualname__ + + @dataclass(frozen=True) + class PathHeader(fc.NameByFileContent, PintHeader): + pass + + @dataclass(frozen=True) + class ParsedProjecHeader(fc.NameByHashIter, PintHeader): + @classmethod + def from_parsed_project( + cls, pp: fp.ParsedProject[Any, ParserConfig], reader_id: str + ): + tmp = ( + f"{stmt.content_hash.algorithm_name}:{stmt.content_hash.hexdigest}" + for stmt in pp.iter_statements() + if isinstance(stmt, fp.BOS) + ) + + 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/datasette/vendored/pint/delegates/formatter/__init__.py b/datasette/vendored/pint/delegates/formatter/__init__.py new file mode 100644 index 0000000000..01def8e759 --- /dev/null +++ b/datasette/vendored/pint/delegates/formatter/__init__.py @@ -0,0 +1,27 @@ +""" + pint.delegates.formatter + ~~~~~~~~~~~~~~~~~~~~~~~~ + + Easy to replace and extend string formatting. + + See pint.delegates.formatter.plain.DefaultFormatter for a + description of a formatter. + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from .full import FullFormatter + + +class Formatter(FullFormatter): + """Default Pint Formatter""" + + pass + + +__all__ = [ + "Formatter", +] diff --git a/datasette/vendored/pint/delegates/formatter/_compound_unit_helpers.py b/datasette/vendored/pint/delegates/formatter/_compound_unit_helpers.py new file mode 100644 index 0000000000..5f4255a306 --- /dev/null +++ b/datasette/vendored/pint/delegates/formatter/_compound_unit_helpers.py @@ -0,0 +1,327 @@ +""" + pint.delegates.formatter._compound_unit_helpers + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Convenient functions to help organize compount units. + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import functools +import locale +from collections.abc import Callable, Iterable +from functools import partial +from itertools import filterfalse, tee +from typing import ( + TYPE_CHECKING, + Any, + Literal, + TypedDict, + TypeVar, +) + +from ...compat import TypeAlias, babel_parse +from ...util import UnitsContainer + +T = TypeVar("T") +U = TypeVar("U") +V = TypeVar("V") +W = TypeVar("W") + +if TYPE_CHECKING: + from ...compat import Locale, Number + from ...facets.plain import PlainUnit + from ...registry import UnitRegistry + + +class SortKwds(TypedDict): + registry: UnitRegistry + + +SortFunc: TypeAlias = Callable[ + [Iterable[tuple[str, Any, str]], Any], Iterable[tuple[str, Any, str]] +] + + +class BabelKwds(TypedDict): + """Babel related keywords used in formatters.""" + + use_plural: bool + length: Literal["short", "long", "narrow"] | None + locale: Locale | str | None + + +def partition( + predicate: Callable[[T], bool], iterable: Iterable[T] +) -> tuple[filterfalse[T], filter[T]]: + """Partition entries into false entries and true entries. + + If *predicate* is slow, consider wrapping it with functools.lru_cache(). + """ + # partition(is_odd, range(10)) --> 0 2 4 6 8 and 1 3 5 7 9 + t1, t2 = tee(iterable) + return filterfalse(predicate, t1), filter(predicate, t2) + + +def localize_per( + length: Literal["short", "long", "narrow"] = "long", + locale: Locale | str | None = locale.LC_NUMERIC, + default: str | None = None, +) -> str: + """Localized singular and plural form of a unit. + + THIS IS TAKEN FROM BABEL format_unit. But + - No magnitude is returned in the string. + - If the unit is not found, the default is given. + - If the default is None, then the same value is given. + """ + locale = babel_parse(locale) + + patterns = locale._data["compound_unit_patterns"].get("per", None) + if patterns is None: + return default or "{}/{}" + + patterns = patterns.get(length, None) + if patterns is None: + return default or "{}/{}" + + # babel 2.8 + if isinstance(patterns, str): + return patterns + + # babe; 2.15 + return patterns.get("compound", default or "{}/{}") + + +@functools.lru_cache +def localize_unit_name( + measurement_unit: str, + use_plural: bool, + length: Literal["short", "long", "narrow"] = "long", + locale: Locale | str | None = locale.LC_NUMERIC, + default: str | None = None, +) -> str: + """Localized singular and plural form of a unit. + + THIS IS TAKEN FROM BABEL format_unit. But + - No magnitude is returned in the string. + - If the unit is not found, the default is given. + - If the default is None, then the same value is given. + """ + locale = babel_parse(locale) + from babel.units import _find_unit_pattern, get_unit_name + + q_unit = _find_unit_pattern(measurement_unit, locale=locale) + if not q_unit: + return measurement_unit + + unit_patterns = locale._data["unit_patterns"][q_unit].get(length, {}) + + if use_plural: + grammatical_number = "other" + else: + grammatical_number = "one" + + if grammatical_number in unit_patterns: + return unit_patterns[grammatical_number].format("").replace("\xa0", "").strip() + + if default is not None: + return default + + # Fall back to a somewhat bad representation. + # nb: This is marked as no-cover, as the current CLDR seemingly has no way for this to happen. + fallback_name = get_unit_name( + measurement_unit, length=length, locale=locale + ) # pragma: no cover + return f"{fallback_name or measurement_unit}" # pragma: no cover + + +def extract2(element: tuple[str, T, str]) -> tuple[str, T]: + """Extract display name and exponent from a tuple containing display name, exponent and unit name.""" + + return element[:2] + + +def to_name_exponent_name(element: tuple[str, T]) -> tuple[str, T, str]: + """Convert unit name and exponent to unit name as display name, exponent and unit name.""" + + # TODO: write a generic typing + + return element + (element[0],) + + +def to_symbol_exponent_name( + el: tuple[str, T], registry: UnitRegistry +) -> tuple[str, T, str]: + """Convert unit name and exponent to unit symbol as display name, exponent and unit name.""" + return registry._get_symbol(el[0]), el[1], el[0] + + +def localize_display_exponent_name( + element: tuple[str, T, str], + use_plural: bool, + length: Literal["short", "long", "narrow"] = "long", + locale: Locale | str | None = locale.LC_NUMERIC, + default: str | None = None, +) -> tuple[str, T, str]: + """Localize display name in a triplet display name, exponent and unit name.""" + + return ( + localize_unit_name( + element[2], use_plural, length, locale, default or element[0] + ), + element[1], + element[2], + ) + + +##################### +# Sorting functions +##################### + + +def sort_by_unit_name( + items: Iterable[tuple[str, Number, str]], _registry: UnitRegistry | None +) -> Iterable[tuple[str, Number, str]]: + return sorted(items, key=lambda el: el[2]) + + +def sort_by_display_name( + items: Iterable[tuple[str, Number, str]], _registry: UnitRegistry | None +) -> Iterable[tuple[str, Number, str]]: + return sorted(items) + + +def sort_by_dimensionality( + items: Iterable[tuple[str, Number, str]], registry: UnitRegistry | None +) -> Iterable[tuple[str, Number, str]]: + """Sort a list of units by dimensional order (from `registry.formatter.dim_order`). + + Parameters + ---------- + items : tuple + a list of tuples containing (unit names, exponent values). + registry : UnitRegistry | None + the registry to use for looking up the dimensions of each unit. + + Returns + ------- + list + the list of units sorted by most significant dimension first. + + Raises + ------ + KeyError + If unit cannot be found in the registry. + """ + + if registry is None: + return items + + dim_order = registry.formatter.dim_order + + def sort_key(item: tuple[str, Number, str]): + _display_name, _unit_exponent, unit_name = item + cname = registry.get_name(unit_name) + cname_dims = registry.get_dimensionality(cname) or {"[]": None} + for cname_dim in cname_dims: + if cname_dim in dim_order: + return dim_order.index(cname_dim), cname + + raise KeyError(f"Unit {unit_name} (aka {cname}) has no recognized dimensions") + + return sorted(items, key=sort_key) + + +def prepare_compount_unit( + unit: PlainUnit | UnitsContainer | Iterable[tuple[str, T]], + spec: str = "", + sort_func: SortFunc | None = None, + use_plural: bool = True, + length: Literal["short", "long", "narrow"] | None = None, + locale: Locale | str | None = None, + as_ratio: bool = True, + registry: UnitRegistry | None = None, +) -> tuple[Iterable[tuple[str, T]], Iterable[tuple[str, T]]]: + """Format compound unit into unit container given + an spec and locale. + + Returns + ------- + iterable of display name, exponent, canonical name + """ + + if isinstance(unit, UnitsContainer): + out = unit.items() + elif hasattr(unit, "_units"): + out = unit._units.items() + else: + out = unit + + # out: unit_name, unit_exponent + + if len(out) == 0: + if "~" in spec: + return ([], []) + else: + return ([("dimensionless", 1)], []) + + if "~" in spec: + if registry is None: + raise ValueError( + f"Can't short format a {type(unit)} without a registry." + " This is usually triggered when formatting a instance" + " of the internal `UnitsContainer`." + ) + _to_symbol_exponent_name = partial(to_symbol_exponent_name, registry=registry) + out = map(_to_symbol_exponent_name, out) + else: + out = map(to_name_exponent_name, out) + + # We keep unit_name because the sort or localizing functions might needed. + # out: display_unit_name, unit_exponent, unit_name + + if as_ratio: + numerator, denominator = partition(lambda el: el[1] < 0, out) + else: + numerator, denominator = out, () + + # numerator: display_unit_name, unit_name, unit_exponent + # denominator: display_unit_name, unit_name, unit_exponent + + if locale is None: + if sort_func is not None: + numerator = sort_func(numerator, registry) + denominator = sort_func(denominator, registry) + + return map(extract2, numerator), map(extract2, denominator) + + if length is None: + length = "short" if "~" in spec else "long" + + mapper = partial( + localize_display_exponent_name, use_plural=False, length=length, locale=locale + ) + + numerator = map(mapper, numerator) + denominator = map(mapper, denominator) + + if sort_func is not None: + numerator = sort_func(numerator, registry) + denominator = sort_func(denominator, registry) + + if use_plural: + if not isinstance(numerator, list): + numerator = list(numerator) + numerator[-1] = localize_display_exponent_name( + numerator[-1], + use_plural, + length=length, + locale=locale, + default=numerator[-1][0], + ) + + return map(extract2, numerator), map(extract2, denominator) diff --git a/datasette/vendored/pint/delegates/formatter/_format_helpers.py b/datasette/vendored/pint/delegates/formatter/_format_helpers.py new file mode 100644 index 0000000000..f527e78ce5 --- /dev/null +++ b/datasette/vendored/pint/delegates/formatter/_format_helpers.py @@ -0,0 +1,234 @@ +""" + pint.delegates.formatter._format_helpers + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Convenient functions to help string formatting operations. + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import re +from collections.abc import Callable, Generator, Iterable +from contextlib import contextmanager +from functools import partial +from locale import LC_NUMERIC, getlocale, setlocale +from typing import ( + TYPE_CHECKING, + Any, + TypeVar, +) + +from ...compat import ndarray +from ._spec_helpers import FORMATTER + +try: + from numpy import integer as np_integer +except ImportError: + np_integer = None + +if TYPE_CHECKING: + from ...compat import Locale, Number + +T = TypeVar("T") +U = TypeVar("U") +V = TypeVar("V") +W = TypeVar("W") + +_PRETTY_EXPONENTS = "⁰¹²³⁴⁵⁶⁷⁸⁹" +_JOIN_REG_EXP = re.compile(r"{\d*}") + + +def format_number(value: Any, spec: str = "") -> str: + """Format number + + This function might disapear in the future. + Right now is aiding backwards compatible migration. + """ + if isinstance(value, float): + return format(value, spec or ".16n") + + elif isinstance(value, int): + return format(value, spec or "n") + + elif isinstance(value, ndarray) and value.ndim == 0: + if issubclass(value.dtype.type, np_integer): + return format(value, spec or "n") + else: + return format(value, spec or ".16n") + else: + return str(value) + + +def builtin_format(value: Any, spec: str = "") -> str: + """A keyword enabled replacement for builtin format + + format has positional only arguments + and this cannot be partialized + and np requires a callable. + """ + return format(value, spec) + + +@contextmanager +def override_locale( + spec: str, locale: str | Locale | None +) -> Generator[Callable[[Any], str], Any, None]: + """Given a spec a locale, yields a function to format a number. + + IMPORTANT: When the locale is not None, this function uses setlocale + and therefore is not thread safe. + """ + + if locale is None: + # If locale is None, just return the builtin format function. + yield ("{:" + spec + "}").format + else: + # If locale is not None, change it and return the backwards compatible + # format_number. + prev_locale_string = getlocale(LC_NUMERIC) + if isinstance(locale, str): + setlocale(LC_NUMERIC, locale) + else: + setlocale(LC_NUMERIC, str(locale)) + yield partial(format_number, spec=spec) + setlocale(LC_NUMERIC, prev_locale_string) + + +def pretty_fmt_exponent(num: Number) -> str: + """Format an number into a pretty printed exponent.""" + # unicode dot operator (U+22C5) looks like a superscript decimal + ret = f"{num:n}".replace("-", "⁻").replace(".", "\u22C5") + for n in range(10): + ret = ret.replace(str(n), _PRETTY_EXPONENTS[n]) + return ret + + +def join_u(fmt: str, iterable: Iterable[Any]) -> str: + """Join an iterable with the format specified in fmt. + + The format can be specified in two ways: + - PEP3101 format with two replacement fields (eg. '{} * {}') + - The concatenating string (eg. ' * ') + """ + if not iterable: + return "" + if not _JOIN_REG_EXP.search(fmt): + return fmt.join(iterable) + miter = iter(iterable) + first = next(miter) + for val in miter: + ret = fmt.format(first, val) + first = ret + return first + + +def join_mu(joint_fstring: str, mstr: str, ustr: str) -> str: + """Join magnitude and units. + + This avoids that `3 and `1 / m` becomes `3 1 / m` + """ + if ustr == "": + return mstr + if ustr.startswith("1 / "): + return joint_fstring.format(mstr, ustr[2:]) + return joint_fstring.format(mstr, ustr) + + +def join_unc(joint_fstring: str, lpar: str, rpar: str, mstr: str, ustr: str) -> str: + """Join uncertainty magnitude and units. + + Uncertainty magnitudes might require extra parenthesis when joined to units. + - YES: 3 +/- 1 + - NO : 3(1) + - NO : (3 +/ 1)e-9 + + This avoids that `(3 + 1)` and `meter` becomes ((3 +/- 1) meter) + """ + if mstr.startswith(lpar) or mstr.endswith(rpar): + return joint_fstring.format(mstr, ustr) + return joint_fstring.format(lpar + mstr + rpar, ustr) + + +def formatter( + numerator: Iterable[tuple[str, Number]], + denominator: Iterable[tuple[str, Number]], + as_ratio: bool = True, + single_denominator: bool = False, + product_fmt: str = " * ", + division_fmt: str = " / ", + power_fmt: str = "{} ** {}", + parentheses_fmt: str = "({0})", + exp_call: FORMATTER = "{:n}".format, +) -> str: + """Format a list of (name, exponent) pairs. + + Parameters + ---------- + items : list + a list of (name, exponent) pairs. + as_ratio : bool, optional + True to display as ratio, False as negative powers. (Default value = True) + single_denominator : bool, optional + all with terms with negative exponents are + collected together. (Default value = False) + product_fmt : str + the format used for multiplication. (Default value = " * ") + division_fmt : str + the format used for division. (Default value = " / ") + power_fmt : str + the format used for exponentiation. (Default value = "{} ** {}") + parentheses_fmt : str + the format used for parenthesis. (Default value = "({0})") + exp_call : callable + (Default value = lambda x: f"{x:n}") + + Returns + ------- + str + the formula as a string. + + """ + + if as_ratio: + fun = lambda x: exp_call(abs(x)) + else: + fun = exp_call + + pos_terms: list[str] = [] + for key, value in numerator: + if value == 1: + pos_terms.append(key) + else: + pos_terms.append(power_fmt.format(key, fun(value))) + + neg_terms: list[str] = [] + for key, value in denominator: + if value == -1 and as_ratio: + neg_terms.append(key) + else: + neg_terms.append(power_fmt.format(key, fun(value))) + + if not pos_terms and not neg_terms: + return "" + + if not as_ratio: + # Show as Product: positive * negative terms ** -1 + return join_u(product_fmt, pos_terms + neg_terms) + + # Show as Ratio: positive terms / negative terms + pos_ret = join_u(product_fmt, pos_terms) or "1" + + if not neg_terms: + return pos_ret + + if single_denominator: + neg_ret = join_u(product_fmt, neg_terms) + if len(neg_terms) > 1: + neg_ret = parentheses_fmt.format(neg_ret) + else: + neg_ret = join_u(division_fmt, neg_terms) + + return join_u(division_fmt, [pos_ret, neg_ret]) diff --git a/datasette/vendored/pint/delegates/formatter/_spec_helpers.py b/datasette/vendored/pint/delegates/formatter/_spec_helpers.py new file mode 100644 index 0000000000..344859b388 --- /dev/null +++ b/datasette/vendored/pint/delegates/formatter/_spec_helpers.py @@ -0,0 +1,131 @@ +""" + pint.delegates.formatter._spec_helpers + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Convenient functions to deal with format specifications. + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import functools +import re +import warnings +from collections.abc import Callable +from typing import Any + +FORMATTER = Callable[ + [ + Any, + ], + str, +] + +# Extract just the type from the specification mini-language: see +# http://docs.python.org/2/library/string.html#format-specification-mini-language +# We also add uS for uncertainties. +_BASIC_TYPES = frozenset("bcdeEfFgGnosxX%uS") + +REGISTERED_FORMATTERS: dict[str, Any] = {} + + +def parse_spec(spec: str) -> str: + """Parse and return spec. + + If an unknown item is found, raise a ValueError. + + This function still needs work: + - what happens if two distinct values are found? + + """ + + result = "" + for ch in reversed(spec): + if ch == "~" or ch in _BASIC_TYPES: + continue + elif ch in list(REGISTERED_FORMATTERS.keys()) + ["~"]: + if result: + raise ValueError("expected ':' after format specifier") + else: + result = ch + elif ch.isalpha(): + raise ValueError("Unknown conversion specified " + ch) + else: + break + return result + + +def extract_custom_flags(spec: str) -> str: + """Return custom flags present in a format specification + + (i.e those not part of Python's formatting mini language) + """ + + if not spec: + return "" + + # sort by length, with longer items first + known_flags = sorted(REGISTERED_FORMATTERS.keys(), key=len, reverse=True) + + flag_re = re.compile("(" + "|".join(known_flags + ["~"]) + ")") + custom_flags = flag_re.findall(spec) + + return "".join(custom_flags) + + +def remove_custom_flags(spec: str) -> str: + """Remove custom flags present in a format specification + + (i.e those not part of Python's formatting mini language) + """ + + for flag in sorted(REGISTERED_FORMATTERS.keys(), key=len, reverse=True) + ["~"]: + if flag: + spec = spec.replace(flag, "") + return spec + + +@functools.lru_cache +def split_format( + spec: str, default: str, separate_format_defaults: bool = True +) -> tuple[str, str]: + """Split format specification into magnitude and unit format.""" + mspec = remove_custom_flags(spec) + uspec = extract_custom_flags(spec) + + default_mspec = remove_custom_flags(default) + default_uspec = extract_custom_flags(default) + + if separate_format_defaults in (False, None): + # should we warn always or only if there was no explicit choice? + # Given that we want to eventually remove the flag again, I'd say yes? + if spec and separate_format_defaults is None: + if not uspec and default_uspec: + warnings.warn( + ( + "The given format spec does not contain a unit formatter." + " Falling back to the builtin defaults, but in the future" + " the unit formatter specified in the `default_format`" + " attribute will be used instead." + ), + DeprecationWarning, + ) + if not mspec and default_mspec: + warnings.warn( + ( + "The given format spec does not contain a magnitude formatter." + " Falling back to the builtin defaults, but in the future" + " the magnitude formatter specified in the `default_format`" + " attribute will be used instead." + ), + DeprecationWarning, + ) + elif not spec: + mspec, uspec = default_mspec, default_uspec + else: + mspec = mspec or default_mspec + uspec = uspec or default_uspec + + return mspec, uspec diff --git a/datasette/vendored/pint/delegates/formatter/_to_register.py b/datasette/vendored/pint/delegates/formatter/_to_register.py new file mode 100644 index 0000000000..697973716c --- /dev/null +++ b/datasette/vendored/pint/delegates/formatter/_to_register.py @@ -0,0 +1,132 @@ +""" + pint.delegates.formatter.base_formatter + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Common class and function for all formatters. + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, Iterable + +from ..._typing import Magnitude +from ...compat import Unpack, ndarray, np +from ...util import UnitsContainer +from ._compound_unit_helpers import BabelKwds, prepare_compount_unit +from ._format_helpers import join_mu, override_locale +from ._spec_helpers import REGISTERED_FORMATTERS, split_format +from .plain import BaseFormatter + +if TYPE_CHECKING: + from ...facets.plain import MagnitudeT, PlainQuantity, PlainUnit + from ...registry import UnitRegistry + + +def register_unit_format(name: str): + """register a function as a new format for units + + The registered function must have a signature of: + + .. code:: python + + def new_format(unit, registry, **options): + pass + + Parameters + ---------- + name : str + The name of the new format (to be used in the format mini-language). A error is + raised if the new format would overwrite a existing format. + + Examples + -------- + .. code:: python + + @pint.register_unit_format("custom") + def format_custom(unit, registry, **options): + result = "" # do the formatting + return result + + + ureg = pint.UnitRegistry() + u = ureg.m / ureg.s ** 2 + f"{u:custom}" + """ + + # TODO: kwargs missing in typing + def wrapper(func: Callable[[PlainUnit, UnitRegistry], str]): + if name in REGISTERED_FORMATTERS: + raise ValueError(f"format {name!r} already exists") # or warn instead + + class NewFormatter(BaseFormatter): + spec = name + + def format_magnitude( + self, + magnitude: Magnitude, + mspec: str = "", + **babel_kwds: Unpack[BabelKwds], + ) -> str: + with override_locale( + mspec, babel_kwds.get("locale", None) + ) as format_number: + if isinstance(magnitude, ndarray) and magnitude.ndim > 0: + # Use custom ndarray text formatting--need to handle scalars differently + # since they don't respond to printoptions + with np.printoptions(formatter={"float_kind": format_number}): + mstr = format(magnitude).replace("\n", "") + else: + mstr = format_number(magnitude) + + return mstr + + def format_unit( + self, + unit: PlainUnit | Iterable[tuple[str, Any]], + uspec: str = "", + **babel_kwds: Unpack[BabelKwds], + ) -> str: + numerator, _denominator = prepare_compount_unit( + unit, + uspec, + **babel_kwds, + as_ratio=False, + registry=self._registry, + ) + + if self._registry is None: + units = UnitsContainer(numerator) + else: + units = self._registry.UnitsContainer(numerator) + + return func(units, registry=self._registry) + + def format_quantity( + self, + quantity: PlainQuantity[MagnitudeT], + qspec: str = "", + **babel_kwds: Unpack[BabelKwds], + ) -> str: + registry = self._registry + + if registry is None: + mspec, uspec = split_format(qspec, "", True) + else: + mspec, uspec = split_format( + qspec, + registry.formatter.default_format, + registry.separate_format_defaults, + ) + + joint_fstring = "{} {}" + return join_mu( + joint_fstring, + self.format_magnitude(quantity.magnitude, mspec, **babel_kwds), + self.format_unit(quantity.unit_items(), uspec, **babel_kwds), + ) + + REGISTERED_FORMATTERS[name] = NewFormatter() + + return wrapper diff --git a/datasette/vendored/pint/delegates/formatter/full.py b/datasette/vendored/pint/delegates/formatter/full.py new file mode 100644 index 0000000000..d5de433269 --- /dev/null +++ b/datasette/vendored/pint/delegates/formatter/full.py @@ -0,0 +1,267 @@ +""" + pint.delegates.formatter.full + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Implements: + - Full: dispatch to other formats, accept defaults. + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import locale +from typing import TYPE_CHECKING, Any, Iterable, Literal + +from ..._typing import Magnitude +from ...compat import Unpack, babel_parse +from ...util import iterable +from ._compound_unit_helpers import BabelKwds, SortFunc, sort_by_unit_name +from ._to_register import REGISTERED_FORMATTERS +from .html import HTMLFormatter +from .latex import LatexFormatter, SIunitxFormatter +from .plain import ( + BaseFormatter, + CompactFormatter, + DefaultFormatter, + PrettyFormatter, + RawFormatter, +) + +if TYPE_CHECKING: + from ...compat import Locale + from ...facets.measurement import Measurement + from ...facets.plain import ( + MagnitudeT, + PlainQuantity, + PlainUnit, + ) + from ...registry import UnitRegistry + + +class FullFormatter(BaseFormatter): + """A formatter that dispatch to other formatters. + + Has a default format, locale and babel_length + """ + + _formatters: dict[str, Any] = {} + + default_format: str = "" + + # TODO: This can be over-riden by the registry definitions file + dim_order: tuple[str, ...] = ( + "[substance]", + "[mass]", + "[current]", + "[luminosity]", + "[length]", + "[]", + "[time]", + "[temperature]", + ) + + default_sort_func: SortFunc | None = staticmethod(sort_by_unit_name) + + locale: Locale | None = None + + def __init__(self, registry: UnitRegistry | None = None): + super().__init__(registry) + + self._formatters = {} + self._formatters["raw"] = RawFormatter(registry) + self._formatters["D"] = DefaultFormatter(registry) + self._formatters["H"] = HTMLFormatter(registry) + self._formatters["P"] = PrettyFormatter(registry) + self._formatters["Lx"] = SIunitxFormatter(registry) + self._formatters["L"] = LatexFormatter(registry) + self._formatters["C"] = CompactFormatter(registry) + + def set_locale(self, loc: str | None) -> None: + """Change the locale used by default by `format_babel`. + + Parameters + ---------- + loc : str or None + None (do not translate), 'sys' (detect the system locale) or a locale id string. + """ + if isinstance(loc, str): + if loc == "sys": + loc = locale.getdefaultlocale()[0] + + # We call babel parse to fail here and not in the formatting operation + babel_parse(loc) + + self.locale = loc + + def get_formatter(self, spec: str): + if spec == "": + return self._formatters["D"] + for k, v in self._formatters.items(): + if k in spec: + return v + + for k, v in REGISTERED_FORMATTERS.items(): + if k in spec: + orphan_fmt = REGISTERED_FORMATTERS[k] + break + else: + return self._formatters["D"] + + try: + fmt = orphan_fmt.__class__(self._registry) + spec = getattr(fmt, "spec", spec) + self._formatters[spec] = fmt + return fmt + except Exception: + return orphan_fmt + + def format_magnitude( + self, magnitude: Magnitude, mspec: str = "", **babel_kwds: Unpack[BabelKwds] + ) -> str: + mspec = mspec or self.default_format + return self.get_formatter(mspec).format_magnitude( + magnitude, mspec, **babel_kwds + ) + + def format_unit( + self, + unit: PlainUnit | Iterable[tuple[str, Any]], + uspec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + uspec = uspec or self.default_format + sort_func = sort_func or self.default_sort_func + return self.get_formatter(uspec).format_unit( + unit, uspec, sort_func=sort_func, **babel_kwds + ) + + def format_quantity( + self, + quantity: PlainQuantity[MagnitudeT], + spec: str = "", + **babel_kwds: Unpack[BabelKwds], + ) -> str: + spec = spec or self.default_format + # If Compact is selected, do it at the beginning + if "#" in spec: + spec = spec.replace("#", "") + obj = quantity.to_compact() + else: + obj = quantity + + del quantity + + locale = babel_kwds.get("locale", self.locale) + + if locale: + if "use_plural" in babel_kwds: + use_plural = babel_kwds["use_plural"] + else: + use_plural = obj.magnitude > 1 + if iterable(use_plural): + use_plural = True + else: + use_plural = False + + return self.get_formatter(spec).format_quantity( + obj, + spec, + sort_func=self.default_sort_func, + use_plural=use_plural, + length=babel_kwds.get("length", None), + locale=locale, + ) + + def format_measurement( + self, + measurement: Measurement, + meas_spec: str = "", + **babel_kwds: Unpack[BabelKwds], + ) -> str: + meas_spec = meas_spec or self.default_format + # If Compact is selected, do it at the beginning + if "#" in meas_spec: + meas_spec = meas_spec.replace("#", "") + obj = measurement.to_compact() + else: + obj = measurement + + del measurement + + use_plural = obj.magnitude.nominal_value > 1 + if iterable(use_plural): + use_plural = True + + return self.get_formatter(meas_spec).format_measurement( + obj, + meas_spec, + sort_func=self.default_sort_func, + use_plural=babel_kwds.get("use_plural", use_plural), + length=babel_kwds.get("length", None), + locale=babel_kwds.get("locale", self.locale), + ) + + ####################################### + # This is for backwards compatibility + ####################################### + + def format_unit_babel( + self, + unit: PlainUnit | Iterable[tuple[str, Any]], + spec: str = "", + length: Literal["short", "long", "narrow"] | None = None, + locale: Locale | None = None, + ) -> str: + if self.locale is None and locale is None: + raise ValueError( + "format_babel requires a locale argumente if the Formatter locale is not set." + ) + + return self.format_unit( + unit, + spec or self.default_format, + sort_func=self.default_sort_func, + use_plural=False, + length=length, + locale=locale or self.locale, + ) + + def format_quantity_babel( + self, + quantity: PlainQuantity[MagnitudeT], + spec: str = "", + length: Literal["short", "long", "narrow"] | None = None, + locale: Locale | None = None, + ) -> str: + if self.locale is None and locale is None: + raise ValueError( + "format_babel requires a locale argumente if the Formatter locale is not set." + ) + + use_plural = quantity.magnitude > 1 + if iterable(use_plural): + use_plural = True + + return self.format_quantity( + quantity, + spec or self.default_format, + sort_func=self.default_sort_func, + use_plural=use_plural, + length=length, + locale=locale or self.locale, + ) + + +################################################################ +# This allows to format units independently of the registry +# +REGISTERED_FORMATTERS["raw"] = RawFormatter() +REGISTERED_FORMATTERS["D"] = DefaultFormatter() +REGISTERED_FORMATTERS["H"] = HTMLFormatter() +REGISTERED_FORMATTERS["P"] = PrettyFormatter() +REGISTERED_FORMATTERS["Lx"] = SIunitxFormatter() +REGISTERED_FORMATTERS["L"] = LatexFormatter() +REGISTERED_FORMATTERS["C"] = CompactFormatter() diff --git a/datasette/vendored/pint/delegates/formatter/html.py b/datasette/vendored/pint/delegates/formatter/html.py new file mode 100644 index 0000000000..b8e3f517fa --- /dev/null +++ b/datasette/vendored/pint/delegates/formatter/html.py @@ -0,0 +1,188 @@ +""" + pint.delegates.formatter.html + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Implements: + - HTML: suitable for web/jupyter notebook outputs. + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import re +from typing import TYPE_CHECKING, Any, Iterable + +from ..._typing import Magnitude +from ...compat import Unpack, ndarray, np +from ...util import iterable +from ._compound_unit_helpers import ( + BabelKwds, + SortFunc, + localize_per, + prepare_compount_unit, +) +from ._format_helpers import ( + formatter, + join_mu, + join_unc, + override_locale, +) +from ._spec_helpers import ( + remove_custom_flags, + split_format, +) +from .plain import BaseFormatter + +if TYPE_CHECKING: + from ...facets.measurement import Measurement + from ...facets.plain import MagnitudeT, PlainQuantity, PlainUnit + +_EXP_PATTERN = re.compile(r"([0-9]\.?[0-9]*)e(-?)\+?0*([0-9]+)") + + +class HTMLFormatter(BaseFormatter): + """HTML localizable text formatter.""" + + def format_magnitude( + self, magnitude: Magnitude, mspec: str = "", **babel_kwds: Unpack[BabelKwds] + ) -> str: + with override_locale(mspec, babel_kwds.get("locale", None)) as format_number: + if hasattr(magnitude, "_repr_html_"): + # If magnitude has an HTML repr, nest it within Pint's + mstr = magnitude._repr_html_() # type: ignore + assert isinstance(mstr, str) + else: + if isinstance(magnitude, ndarray): + # Need to override for scalars, which are detected as iterable, + # and don't respond to printoptions. + if magnitude.ndim == 0: + mstr = format_number(magnitude) + else: + with np.printoptions(formatter={"float_kind": format_number}): + mstr = ( + "
" + format(magnitude).replace("\n", "") + "
" + ) + elif not iterable(magnitude): + # Use plain text for scalars + mstr = format_number(magnitude) + else: + # Use monospace font for other array-likes + mstr = ( + "
"
+                        + format_number(magnitude).replace("\n", "
") + + "
" + ) + + m = _EXP_PATTERN.match(mstr) + _exp_formatter = lambda s: f"{s}" + + if m: + exp = int(m.group(2) + m.group(3)) + mstr = _EXP_PATTERN.sub(r"\1×10" + _exp_formatter(exp), mstr) + + return mstr + + def format_unit( + self, + unit: PlainUnit | Iterable[tuple[str, Any]], + uspec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + numerator, denominator = prepare_compount_unit( + unit, + uspec, + sort_func=sort_func, + **babel_kwds, + registry=self._registry, + ) + + if babel_kwds.get("locale", None): + length = babel_kwds.get("length") or ("short" if "~" in uspec else "long") + division_fmt = localize_per(length, babel_kwds.get("locale"), "{}/{}") + else: + division_fmt = "{}/{}" + + return formatter( + numerator, + denominator, + as_ratio=True, + single_denominator=True, + product_fmt=r" ", + division_fmt=division_fmt, + power_fmt=r"{}{}", + parentheses_fmt=r"({})", + ) + + def format_quantity( + self, + quantity: PlainQuantity[MagnitudeT], + qspec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + registry = self._registry + + mspec, uspec = split_format( + qspec, registry.formatter.default_format, registry.separate_format_defaults + ) + + if iterable(quantity.magnitude): + # Use HTML table instead of plain text template for array-likes + joint_fstring = ( + "" + "" + "" + "" + "
Magnitude{}
Units{}
" + ) + else: + joint_fstring = "{} {}" + + return join_mu( + joint_fstring, + self.format_magnitude(quantity.magnitude, mspec, **babel_kwds), + self.format_unit(quantity.unit_items(), uspec, sort_func, **babel_kwds), + ) + + def format_uncertainty( + self, + uncertainty, + unc_spec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + unc_str = format(uncertainty, unc_spec).replace("+/-", " ± ") + + unc_str = re.sub(r"\)e\+0?(\d+)", r")×10\1", unc_str) + unc_str = re.sub(r"\)e-0?(\d+)", r")×10-\1", unc_str) + return unc_str + + def format_measurement( + self, + measurement: Measurement, + meas_spec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + registry = self._registry + + mspec, uspec = split_format( + meas_spec, + registry.formatter.default_format, + registry.separate_format_defaults, + ) + + unc_spec = remove_custom_flags(meas_spec) + + joint_fstring = "{} {}" + + return join_unc( + joint_fstring, + "(", + ")", + self.format_uncertainty(measurement.magnitude, unc_spec, **babel_kwds), + self.format_unit(measurement.units, uspec, sort_func, **babel_kwds), + ) diff --git a/datasette/vendored/pint/delegates/formatter/latex.py b/datasette/vendored/pint/delegates/formatter/latex.py new file mode 100644 index 0000000000..cc2b46af7e --- /dev/null +++ b/datasette/vendored/pint/delegates/formatter/latex.py @@ -0,0 +1,420 @@ +""" + pint.delegates.formatter.latex + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Implements: + - Latex: uses vainilla latex. + - SIunitx: uses latex siunitx package format. + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import functools +import re +from collections.abc import Iterable +from typing import TYPE_CHECKING, Any + +from ..._typing import Magnitude +from ...compat import Number, Unpack, ndarray +from ._compound_unit_helpers import ( + BabelKwds, + SortFunc, + prepare_compount_unit, +) +from ._format_helpers import ( + FORMATTER, + formatter, + join_mu, + join_unc, + override_locale, +) +from ._spec_helpers import ( + remove_custom_flags, + split_format, +) +from .plain import BaseFormatter + +if TYPE_CHECKING: + from ...facets.measurement import Measurement + from ...facets.plain import MagnitudeT, PlainQuantity, PlainUnit + from ...registry import UnitRegistry + from ...util import ItMatrix + + +def vector_to_latex( + vec: Iterable[Any], fmtfun: FORMATTER | str = "{:.2n}".format +) -> str: + """Format a vector into a latex string.""" + return matrix_to_latex([vec], fmtfun) + + +def matrix_to_latex(matrix: ItMatrix, fmtfun: FORMATTER | str = "{:.2n}".format) -> str: + """Format a matrix into a latex string.""" + + ret: list[str] = [] + + for row in matrix: + ret += [" & ".join(fmtfun(f) for f in row)] + + return r"\begin{pmatrix}%s\end{pmatrix}" % "\\\\ \n".join(ret) + + +def ndarray_to_latex_parts( + ndarr: ndarray, fmtfun: FORMATTER = "{:.2n}".format, dim: tuple[int, ...] = tuple() +) -> list[str]: + """Convert an numpy array into an iterable of elements to be print. + + e.g. + - if the array is 2d, it will return an iterable of rows. + - if the array is 3d, it will return an iterable of matrices. + """ + + if isinstance(fmtfun, str): + fmtfun = fmtfun.format + + if ndarr.ndim == 0: + _ndarr = ndarr.reshape(1) + return [vector_to_latex(_ndarr, fmtfun)] + if ndarr.ndim == 1: + return [vector_to_latex(ndarr, fmtfun)] + if ndarr.ndim == 2: + return [matrix_to_latex(ndarr, fmtfun)] + else: + ret = [] + if ndarr.ndim == 3: + header = ("arr[%s," % ",".join("%d" % d for d in dim)) + "%d,:,:]" + for elno, el in enumerate(ndarr): + ret += [header % elno + " = " + matrix_to_latex(el, fmtfun)] + else: + for elno, el in enumerate(ndarr): + ret += ndarray_to_latex_parts(el, fmtfun, dim + (elno,)) + + return ret + + +def ndarray_to_latex( + ndarr: ndarray, + fmtfun: FORMATTER | str = "{:.2n}".format, + dim: tuple[int, ...] = tuple(), +) -> str: + """Format a numpy array into string.""" + return "\n".join(ndarray_to_latex_parts(ndarr, fmtfun, dim)) + + +def latex_escape(string: str) -> str: + """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), + ) + + +def siunitx_format_unit( + units: Iterable[tuple[str, Number]], registry: UnitRegistry +) -> str: + """Returns LaTeX code for the unit that can be put into an siunitx command.""" + + def _tothe(power) -> str: + if power == int(power): + if power == 1: + return "" + elif power == 2: + return r"\squared" + elif power == 3: + return r"\cubed" + else: + return rf"\tothe{{{int(power):d}}}" + else: + # limit float powers to 3 decimal places + return rf"\tothe{{{power:.3f}}}".rstrip("0") + + lpos = [] + lneg = [] + # loop through all units in the container + for unit, power in sorted(units): + # remove unit prefix if it exists + # siunitx supports \prefix commands + + 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.name) + if len(p) > 0 and unit.find(p) == 0: + prefix = p + unit = unit.replace(prefix, "", 1) + + if power < 0: + lpick.append(r"\per") + if prefix is not None: + lpick.append(rf"\{prefix}") + lpick.append(rf"\{unit}") + lpick.append(rf"{_tothe(abs(power))}") + + return "".join(lpos) + "".join(lneg) + + +_EXP_PATTERN = re.compile(r"([0-9]\.?[0-9]*)e(-?)\+?0*([0-9]+)") + + +class LatexFormatter(BaseFormatter): + """Latex localizable text formatter.""" + + def format_magnitude( + self, magnitude: Magnitude, mspec: str = "", **babel_kwds: Unpack[BabelKwds] + ) -> str: + with override_locale(mspec, babel_kwds.get("locale", None)) as format_number: + if isinstance(magnitude, ndarray): + mstr = ndarray_to_latex(magnitude, mspec) + else: + mstr = format_number(magnitude) + + mstr = _EXP_PATTERN.sub(r"\1\\times 10^{\2\3}", mstr) + + return mstr + + def format_unit( + self, + unit: PlainUnit | Iterable[tuple[str, Any]], + uspec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + numerator, denominator = prepare_compount_unit( + unit, + uspec, + sort_func=sort_func, + **babel_kwds, + registry=self._registry, + ) + + numerator = ((rf"\mathrm{{{latex_escape(u)}}}", p) for u, p in numerator) + denominator = ((rf"\mathrm{{{latex_escape(u)}}}", p) for u, p in denominator) + + # Localized latex + # if babel_kwds.get("locale", None): + # length = babel_kwds.get("length") or ("short" if "~" in uspec else "long") + # division_fmt = localize_per(length, babel_kwds.get("locale"), "{}/{}") + # else: + # division_fmt = "{}/{}" + + # division_fmt = r"\frac" + division_fmt.format("[{}]", "[{}]") + + formatted = formatter( + numerator, + denominator, + as_ratio=True, + single_denominator=True, + product_fmt=r" \cdot ", + division_fmt=r"\frac[{}][{}]", + power_fmt="{}^[{}]", + parentheses_fmt=r"\left({}\right)", + ) + + return formatted.replace("[", "{").replace("]", "}") + + def format_quantity( + self, + quantity: PlainQuantity[MagnitudeT], + qspec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + registry = self._registry + + mspec, uspec = split_format( + qspec, registry.formatter.default_format, registry.separate_format_defaults + ) + + joint_fstring = r"{}\ {}" + + return join_mu( + joint_fstring, + self.format_magnitude(quantity.magnitude, mspec, **babel_kwds), + self.format_unit(quantity.unit_items(), uspec, sort_func, **babel_kwds), + ) + + def format_uncertainty( + self, + uncertainty, + unc_spec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + # uncertainties handles everythin related to latex. + unc_str = format(uncertainty, unc_spec) + + if unc_str.startswith(r"\left"): + return unc_str + + return unc_str.replace("(", r"\left(").replace(")", r"\right)") + + def format_measurement( + self, + measurement: Measurement, + meas_spec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + registry = self._registry + + mspec, uspec = split_format( + meas_spec, + registry.formatter.default_format, + registry.separate_format_defaults, + ) + + unc_spec = remove_custom_flags(meas_spec) + + # TODO: ugly. uncertainties recognizes L + if "L" not in unc_spec: + unc_spec += "L" + + joint_fstring = r"{}\ {}" + + return join_unc( + joint_fstring, + r"\left(", + r"\right)", + self.format_uncertainty(measurement.magnitude, unc_spec, **babel_kwds), + self.format_unit(measurement.units, uspec, sort_func, **babel_kwds), + ) + + +class SIunitxFormatter(BaseFormatter): + """Latex localizable text formatter with siunitx format. + + See: https://ctan.org/pkg/siunitx + """ + + def format_magnitude( + self, + magnitude: Magnitude, + mspec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + with override_locale(mspec, babel_kwds.get("locale", None)) as format_number: + if isinstance(magnitude, ndarray): + mstr = ndarray_to_latex(magnitude, mspec) + else: + mstr = format_number(magnitude) + + # TODO: Why this is not needed in siunitx? + # mstr = _EXP_PATTERN.sub(r"\1\\times 10^{\2\3}", mstr) + + return mstr + + def format_unit( + self, + unit: PlainUnit | Iterable[tuple[str, Any]], + uspec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + registry = self._registry + if registry is None: + raise ValueError( + "Can't format as siunitx without a registry." + " This is usually triggered when formatting a instance" + ' of the internal `UnitsContainer` with a spec of `"Lx"`' + " and might indicate a bug in `pint`." + ) + + # TODO: not sure if I should call format_compound_unit here. + # siunitx_format_unit requires certain specific names? + # should unit names be translated? + # should unit names be shortened? + # units = format_compound_unit(unit, uspec, **babel_kwds) + + try: + units = unit._units.items() + except Exception: + units = unit + + formatted = siunitx_format_unit(units, registry) + + if "~" in uspec: + formatted = formatted.replace(r"\percent", r"\%") + + # TODO: is this the right behaviour? Should we return the \si[] when only + # the units are returned? + return rf"\si[]{{{formatted}}}" + + def format_quantity( + self, + quantity: PlainQuantity[MagnitudeT], + qspec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + registry = self._registry + + mspec, uspec = split_format( + qspec, registry.formatter.default_format, registry.separate_format_defaults + ) + + joint_fstring = "{}{}" + + mstr = self.format_magnitude(quantity.magnitude, mspec, **babel_kwds) + ustr = self.format_unit(quantity.unit_items(), uspec, sort_func, **babel_kwds)[ + len(r"\si[]") : + ] + return r"\SI[]" + join_mu(joint_fstring, "{%s}" % mstr, ustr) + + def format_uncertainty( + self, + uncertainty, + unc_spec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + # SIunitx requires space between "+-" (or "\pm") and the nominal value + # and uncertainty, and doesn't accept "+/-" + # SIunitx doesn't accept parentheses, which uncs uses with + # scientific notation ('e' or 'E' and sometimes 'g' or 'G'). + return ( + format(uncertainty, unc_spec) + .replace("+/-", r" +- ") + .replace("(", "") + .replace(")", " ") + ) + + def format_measurement( + self, + measurement: Measurement, + meas_spec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + registry = self._registry + + mspec, uspec = split_format( + meas_spec, + registry.formatter.default_format, + registry.separate_format_defaults, + ) + + unc_spec = remove_custom_flags(meas_spec) + + joint_fstring = "{}{}" + + return r"\SI" + join_unc( + joint_fstring, + r"", + r"", + "{%s}" + % self.format_uncertainty(measurement.magnitude, unc_spec, **babel_kwds), + self.format_unit(measurement.units, uspec, sort_func, **babel_kwds)[ + len(r"\si[]") : + ], + ) diff --git a/datasette/vendored/pint/delegates/formatter/plain.py b/datasette/vendored/pint/delegates/formatter/plain.py new file mode 100644 index 0000000000..d40ec1ae07 --- /dev/null +++ b/datasette/vendored/pint/delegates/formatter/plain.py @@ -0,0 +1,486 @@ +""" + pint.delegates.formatter.plain + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Implements plain text formatters: + - Raw: as simple as it gets (no locale aware, no unit formatter.) + - Default: used when no string spec is given. + - Compact: like default but with less spaces. + - Pretty: pretty printed formatter. + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import itertools +import re +from typing import TYPE_CHECKING, Any, Iterable + +from ..._typing import Magnitude +from ...compat import Unpack, ndarray, np +from ._compound_unit_helpers import ( + BabelKwds, + SortFunc, + localize_per, + prepare_compount_unit, +) +from ._format_helpers import ( + formatter, + join_mu, + join_unc, + override_locale, + pretty_fmt_exponent, +) +from ._spec_helpers import ( + remove_custom_flags, + split_format, +) + +if TYPE_CHECKING: + from ...facets.measurement import Measurement + from ...facets.plain import MagnitudeT, PlainQuantity, PlainUnit + from ...registry import UnitRegistry + + +_EXP_PATTERN = re.compile(r"([0-9]\.?[0-9]*)e(-?)\+?0*([0-9]+)") + + +class BaseFormatter: + def __init__(self, registry: UnitRegistry | None = None): + self._registry = registry + + +class DefaultFormatter(BaseFormatter): + """Simple, localizable plain text formatter. + + A formatter is a class with methods to format into string each of the objects + that appear in pint (magnitude, unit, quantity, uncertainty, measurement) + """ + + def format_magnitude( + self, magnitude: Magnitude, mspec: str = "", **babel_kwds: Unpack[BabelKwds] + ) -> str: + """Format scalar/array into string + given a string formatting specification and locale related arguments. + """ + with override_locale(mspec, babel_kwds.get("locale", None)) as format_number: + if isinstance(magnitude, ndarray) and magnitude.ndim > 0: + # Use custom ndarray text formatting--need to handle scalars differently + # since they don't respond to printoptions + with np.printoptions(formatter={"float_kind": format_number}): + mstr = format(magnitude).replace("\n", "") + else: + mstr = format_number(magnitude) + + return mstr + + def format_unit( + self, + unit: PlainUnit | Iterable[tuple[str, Any]], + uspec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + """Format a unit (can be compound) into string + given a string formatting specification and locale related arguments. + """ + + numerator, denominator = prepare_compount_unit( + unit, + uspec, + sort_func=sort_func, + **babel_kwds, + registry=self._registry, + ) + + if babel_kwds.get("locale", None): + length = babel_kwds.get("length") or ("short" if "~" in uspec else "long") + division_fmt = localize_per(length, babel_kwds.get("locale"), "{} / {}") + else: + division_fmt = "{} / {}" + + return formatter( + numerator, + denominator, + as_ratio=True, + single_denominator=False, + product_fmt="{} * {}", + division_fmt=division_fmt, + power_fmt="{} ** {}", + parentheses_fmt=r"({})", + ) + + def format_quantity( + self, + quantity: PlainQuantity[MagnitudeT], + qspec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + """Format a quantity (magnitude and unit) into string + given a string formatting specification and locale related arguments. + """ + + registry = self._registry + + mspec, uspec = split_format( + qspec, registry.formatter.default_format, registry.separate_format_defaults + ) + + joint_fstring = "{} {}" + return join_mu( + joint_fstring, + self.format_magnitude(quantity.magnitude, mspec, **babel_kwds), + self.format_unit(quantity.unit_items(), uspec, sort_func, **babel_kwds), + ) + + def format_uncertainty( + self, + uncertainty, + unc_spec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + """Format an uncertainty magnitude (nominal value and stdev) into string + given a string formatting specification and locale related arguments. + """ + + return format(uncertainty, unc_spec).replace("+/-", " +/- ") + + def format_measurement( + self, + measurement: Measurement, + meas_spec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + """Format an measurement (uncertainty and units) into string + given a string formatting specification and locale related arguments. + """ + + registry = self._registry + + mspec, uspec = split_format( + meas_spec, + registry.formatter.default_format, + registry.separate_format_defaults, + ) + + unc_spec = remove_custom_flags(meas_spec) + + joint_fstring = "{} {}" + + return join_unc( + joint_fstring, + "(", + ")", + self.format_uncertainty(measurement.magnitude, unc_spec, **babel_kwds), + self.format_unit(measurement.units, uspec, sort_func, **babel_kwds), + ) + + +class CompactFormatter(BaseFormatter): + """Simple, localizable plain text formatter without extra spaces.""" + + def format_magnitude( + self, magnitude: Magnitude, mspec: str = "", **babel_kwds: Unpack[BabelKwds] + ) -> str: + with override_locale(mspec, babel_kwds.get("locale", None)) as format_number: + if isinstance(magnitude, ndarray) and magnitude.ndim > 0: + # Use custom ndarray text formatting--need to handle scalars differently + # since they don't respond to printoptions + with np.printoptions(formatter={"float_kind": format_number}): + mstr = format(magnitude).replace("\n", "") + else: + mstr = format_number(magnitude) + + return mstr + + def format_unit( + self, + unit: PlainUnit | Iterable[tuple[str, Any]], + uspec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + numerator, denominator = prepare_compount_unit( + unit, + uspec, + sort_func=sort_func, + **babel_kwds, + registry=self._registry, + ) + + # Division format in compact formatter is not localized. + division_fmt = "{}/{}" + + return formatter( + numerator, + denominator, + as_ratio=True, + single_denominator=False, + product_fmt="*", # TODO: Should this just be ''? + division_fmt=division_fmt, + power_fmt="{}**{}", + parentheses_fmt=r"({})", + ) + + def format_quantity( + self, + quantity: PlainQuantity[MagnitudeT], + qspec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + registry = self._registry + + mspec, uspec = split_format( + qspec, registry.formatter.default_format, registry.separate_format_defaults + ) + + joint_fstring = "{} {}" + + return join_mu( + joint_fstring, + self.format_magnitude(quantity.magnitude, mspec, **babel_kwds), + self.format_unit(quantity.unit_items(), uspec, sort_func, **babel_kwds), + ) + + def format_uncertainty( + self, + uncertainty, + unc_spec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + return format(uncertainty, unc_spec).replace("+/-", "+/-") + + def format_measurement( + self, + measurement: Measurement, + meas_spec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + registry = self._registry + + mspec, uspec = split_format( + meas_spec, + registry.formatter.default_format, + registry.separate_format_defaults, + ) + + unc_spec = remove_custom_flags(meas_spec) + + joint_fstring = "{} {}" + + return join_unc( + joint_fstring, + "(", + ")", + self.format_uncertainty(measurement.magnitude, unc_spec, **babel_kwds), + self.format_unit(measurement.units, uspec, sort_func, **babel_kwds), + ) + + +class PrettyFormatter(BaseFormatter): + """Pretty printed localizable plain text formatter without extra spaces.""" + + def format_magnitude( + self, magnitude: Magnitude, mspec: str = "", **babel_kwds: Unpack[BabelKwds] + ) -> str: + with override_locale(mspec, babel_kwds.get("locale", None)) as format_number: + if isinstance(magnitude, ndarray) and magnitude.ndim > 0: + # Use custom ndarray text formatting--need to handle scalars differently + # since they don't respond to printoptions + with np.printoptions(formatter={"float_kind": format_number}): + mstr = format(magnitude).replace("\n", "") + else: + mstr = format_number(magnitude) + + m = _EXP_PATTERN.match(mstr) + + if m: + exp = int(m.group(2) + m.group(3)) + mstr = _EXP_PATTERN.sub(r"\1×10" + pretty_fmt_exponent(exp), mstr) + + return mstr + + def format_unit( + self, + unit: PlainUnit | Iterable[tuple[str, Any]], + uspec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + numerator, denominator = prepare_compount_unit( + unit, + uspec, + sort_func=sort_func, + **babel_kwds, + registry=self._registry, + ) + + if babel_kwds.get("locale", None): + length = babel_kwds.get("length") or ("short" if "~" in uspec else "long") + division_fmt = localize_per(length, babel_kwds.get("locale"), "{}/{}") + else: + division_fmt = "{}/{}" + + return formatter( + numerator, + denominator, + as_ratio=True, + single_denominator=False, + product_fmt="·", + division_fmt=division_fmt, + power_fmt="{}{}", + parentheses_fmt="({})", + exp_call=pretty_fmt_exponent, + ) + + def format_quantity( + self, + quantity: PlainQuantity[MagnitudeT], + qspec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + registry = self._registry + + mspec, uspec = split_format( + qspec, registry.formatter.default_format, registry.separate_format_defaults + ) + + joint_fstring = "{} {}" + + return join_mu( + joint_fstring, + self.format_magnitude(quantity.magnitude, mspec, **babel_kwds), + self.format_unit(quantity.unit_items(), uspec, sort_func, **babel_kwds), + ) + + def format_uncertainty( + self, + uncertainty, + unc_spec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + return format(uncertainty, unc_spec).replace("±", " ± ") + + def format_measurement( + self, + measurement: Measurement, + meas_spec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + registry = self._registry + + mspec, uspec = split_format( + meas_spec, + registry.formatter.default_format, + registry.separate_format_defaults, + ) + + unc_spec = meas_spec + joint_fstring = "{} {}" + + return join_unc( + joint_fstring, + "(", + ")", + self.format_uncertainty(measurement.magnitude, unc_spec, **babel_kwds), + self.format_unit(measurement.units, uspec, sort_func, **babel_kwds), + ) + + +class RawFormatter(BaseFormatter): + """Very simple non-localizable plain text formatter. + + Ignores all pint custom string formatting specification. + """ + + def format_magnitude( + self, magnitude: Magnitude, mspec: str = "", **babel_kwds: Unpack[BabelKwds] + ) -> str: + return str(magnitude) + + def format_unit( + self, + unit: PlainUnit | Iterable[tuple[str, Any]], + uspec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + numerator, denominator = prepare_compount_unit( + unit, + uspec, + sort_func=sort_func, + **babel_kwds, + registry=self._registry, + ) + + return " * ".join( + k if v == 1 else f"{k} ** {v}" + for k, v in itertools.chain(numerator, denominator) + ) + + def format_quantity( + self, + quantity: PlainQuantity[MagnitudeT], + qspec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + registry = self._registry + + mspec, uspec = split_format( + qspec, registry.formatter.default_format, registry.separate_format_defaults + ) + + joint_fstring = "{} {}" + return join_mu( + joint_fstring, + self.format_magnitude(quantity.magnitude, mspec, **babel_kwds), + self.format_unit(quantity.unit_items(), uspec, sort_func, **babel_kwds), + ) + + def format_uncertainty( + self, + uncertainty, + unc_spec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + return format(uncertainty, unc_spec) + + def format_measurement( + self, + measurement: Measurement, + meas_spec: str = "", + sort_func: SortFunc | None = None, + **babel_kwds: Unpack[BabelKwds], + ) -> str: + registry = self._registry + + mspec, uspec = split_format( + meas_spec, + registry.formatter.default_format, + registry.separate_format_defaults, + ) + + unc_spec = remove_custom_flags(meas_spec) + + joint_fstring = "{} {}" + + return join_unc( + joint_fstring, + "(", + ")", + self.format_uncertainty(measurement.magnitude, unc_spec, **babel_kwds), + self.format_unit(measurement.units, uspec, sort_func, **babel_kwds), + ) diff --git a/datasette/vendored/pint/delegates/txt_defparser/__init__.py b/datasette/vendored/pint/delegates/txt_defparser/__init__.py new file mode 100644 index 0000000000..9352b35070 --- /dev/null +++ b/datasette/vendored/pint/delegates/txt_defparser/__init__.py @@ -0,0 +1,17 @@ +""" + 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 __future__ import annotations + +from .defparser import DefParser + +__all__ = [ + "DefParser", +] diff --git a/datasette/vendored/pint/delegates/txt_defparser/block.py b/datasette/vendored/pint/delegates/txt_defparser/block.py new file mode 100644 index 0000000000..01ac353b2f --- /dev/null +++ b/datasette/vendored/pint/delegates/txt_defparser/block.py @@ -0,0 +1,52 @@ +""" + pint.delegates.txt_defparser.block + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Classes for Pint Blocks, which are defined by: + + @ + + @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 typing import Generic, TypeVar + +import flexparser as fp + +from ..base_defparser import ParserConfig, PintParsedStatement + + +@dataclass(frozen=True) +class EndDirectiveBlock(PintParsedStatement): + """An EndDirectiveBlock is simply an "@end" statement.""" + + @classmethod + def from_string(cls, s: str) -> fp.NullableParsedResult[EndDirectiveBlock]: + if s == "@end": + return cls() + return None + + +OPST = TypeVar("OPST", bound="PintParsedStatement") +IPST = TypeVar("IPST", bound="PintParsedStatement") + +DefT = TypeVar("DefT") + + +@dataclass(frozen=True) +class DirectiveBlock( + Generic[DefT, OPST, IPST], fp.Block[OPST, IPST, EndDirectiveBlock, ParserConfig] +): + """Directive blocks have beginning statement starting with a @ character. + and ending with a "@end" (captured using a EndDirectiveBlock). + + Subclass this class for convenience. + """ + + def derive_definition(self) -> DefT: ... diff --git a/datasette/vendored/pint/delegates/txt_defparser/common.py b/datasette/vendored/pint/delegates/txt_defparser/common.py new file mode 100644 index 0000000000..ebdabc062c --- /dev/null +++ b/datasette/vendored/pint/delegates/txt_defparser/common.py @@ -0,0 +1,59 @@ +""" + 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 + +import flexparser as fp + +from ... import errors +from ..base_defparser import ParserConfig + + +@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) -> str: + 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: str) -> None: + super().__setattr__("location", value) + + +@dataclass(frozen=True) +class ImportDefinition(fp.IncludeStatement[ParserConfig]): + value: str + + @property + def target(self) -> str: + return self.value + + @classmethod + def from_string(cls, s: str) -> fp.NullableParsedResult[ImportDefinition]: + if s.startswith("@import"): + return ImportDefinition(s[len("@import") :].strip()) + return None diff --git a/datasette/vendored/pint/delegates/txt_defparser/context.py b/datasette/vendored/pint/delegates/txt_defparser/context.py new file mode 100644 index 0000000000..029b60445d --- /dev/null +++ b/datasette/vendored/pint/delegates/txt_defparser/context.py @@ -0,0 +1,203 @@ +""" + 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 Union + +import flexparser as fp + +from ...facets.context import definitions +from ..base_defparser import ParserConfig, PintParsedStatement +from . import block, common, plain + +# TODO check syntax +T = ty.TypeVar("T", bound="Union[ForwardRelation, BidirectionalRelation]") + + +def _from_string_and_context_sep( + cls: type[T], s: str, config: ParserConfig, separator: str +) -> T | None: + 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(PintParsedStatement, definitions.ForwardRelation): + """A relation connecting a dimension to another via a transformation function. + + -> : + """ + + @classmethod + def from_string_and_config( + cls, s: str, config: ParserConfig + ) -> fp.NullableParsedResult[ForwardRelation]: + return _from_string_and_context_sep(cls, s, config, "->") + + +@dataclass(frozen=True) +class BidirectionalRelation(PintParsedStatement, definitions.BidirectionalRelation): + """A bidirectional relation connecting a dimension to another + via a simple transformation function. + + <-> : + + """ + + @classmethod + def from_string_and_config( + cls, s: str, config: ParserConfig + ) -> fp.NullableParsedResult[BidirectionalRelation]: + return _from_string_and_context_sep(cls, s, config, "<->") + + +@dataclass(frozen=True) +class BeginContext(PintParsedStatement): + """Being of a context directive. + + @context[(defaults)] [= ] [= ] + """ + + _header_re = re.compile( + r"@context\s*(?P\(.*\))?\s+(?P\w+)\s*(=(?P.*))*" + ) + + name: str + aliases: tuple[str, ...] + defaults: dict[str, numbers.Number] + + @classmethod + def from_string_and_config( + cls, s: str, config: ParserConfig + ) -> fp.NullableParsedResult[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[ + definitions.ContextDefinition, + BeginContext, + ty.Union[ + plain.CommentDefinition, + BidirectionalRelation, + ForwardRelation, + plain.UnitDefinition, + ], + ] +): + """Definition of a Context + + @context[(defaults)] [= ] [= ] + # units can be redefined within the context + = + + # can establish unidirectional relationships between dimensions + -> : + + # can establish bidirectionl relationships between dimensions + <-> : + @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 + """ + + def derive_definition(self) -> definitions.ContextDefinition: + return definitions.ContextDefinition( + self.name, self.aliases, self.defaults, self.relations, self.redefinitions + ) + + @property + def name(self) -> str: + assert isinstance(self.opening, BeginContext) + return self.opening.name + + @property + def aliases(self) -> tuple[str, ...]: + assert isinstance(self.opening, BeginContext) + return self.opening.aliases + + @property + def defaults(self) -> dict[str, numbers.Number]: + assert isinstance(self.opening, BeginContext) + return self.opening.defaults + + @property + def relations(self) -> tuple[BidirectionalRelation | ForwardRelation, ...]: + return tuple( + r + for r in self.body + if isinstance(r, (ForwardRelation, BidirectionalRelation)) + ) + + @property + def redefinitions(self) -> tuple[plain.UnitDefinition, ...]: + return tuple(r for r in self.body if isinstance(r, plain.UnitDefinition)) diff --git a/datasette/vendored/pint/delegates/txt_defparser/defaults.py b/datasette/vendored/pint/delegates/txt_defparser/defaults.py new file mode 100644 index 0000000000..669daddb4a --- /dev/null +++ b/datasette/vendored/pint/delegates/txt_defparser/defaults.py @@ -0,0 +1,80 @@ +""" + 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 + +import flexparser as fp + +from ...facets.plain import definitions +from ..base_defparser import PintParsedStatement +from . import block, plain + + +@dataclass(frozen=True) +class BeginDefaults(PintParsedStatement): + """Being of a defaults directive. + + @defaults + """ + + @classmethod + def from_string(cls, s: str) -> fp.NullableParsedResult[BeginDefaults]: + if s.strip() == "@defaults": + return cls() + return None + + +@dataclass(frozen=True) +class DefaultsDefinition( + block.DirectiveBlock[ + definitions.DefaultsDefinition, + BeginDefaults, + ty.Union[ + plain.CommentDefinition, + plain.Equality, + ], + ] +): + """Directive to store values. + + @defaults + system = mks + @end + + See Equality and Comment for more parsing related information. + """ + + @property + def _valid_fields(self) -> tuple[str, ...]: + return tuple(f.name for f in fields(definitions.DefaultsDefinition)) + + def derive_definition(self) -> definitions.DefaultsDefinition: + 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: str) -> str: + for stmt in self.body: + if isinstance(stmt, plain.Equality) and stmt.lhs == key: + return stmt.rhs + raise KeyError(key) diff --git a/datasette/vendored/pint/delegates/txt_defparser/defparser.py b/datasette/vendored/pint/delegates/txt_defparser/defparser.py new file mode 100644 index 0000000000..8c57ac3067 --- /dev/null +++ b/datasette/vendored/pint/delegates/txt_defparser/defparser.py @@ -0,0 +1,143 @@ +from __future__ import annotations + +import pathlib +import typing as ty + +import flexcache as fc +import flexparser as fp + +from ..base_defparser import ParserConfig +from . import block, common, context, defaults, group, plain, system + + +class PintRootBlock( + fp.RootBlock[ + ty.Union[ + plain.CommentDefinition, + common.ImportDefinition, + context.ContextDefinition, + defaults.DefaultsDefinition, + system.SystemDefinition, + group.GroupDefinition, + plain.AliasDefinition, + plain.DerivedDimensionDefinition, + plain.DimensionDefinition, + plain.PrefixDefinition, + plain.UnitDefinition, + ], + ParserConfig, + ] +): + pass + + +class _PintParser(fp.Parser[PintRootBlock, ParserConfig]): + """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 | None + + def __init__(self, config: ParserConfig, *args: ty.Any, **kwargs: ty.Any): + self._diskcache = kwargs.pop("diskcache", None) + super().__init__(config, *args, **kwargs) + + def parse_file( + self, path: pathlib.Path + ) -> fp.ParsedSource[PintRootBlock, ParserConfig]: + 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: tuple[type, ...] = ( + fp.BOF, + fp.BOR, + fp.BOS, + fp.EOS, + plain.CommentDefinition, + ) + + def __init__(self, default_config: ParserConfig, diskcache: fc.DiskCache): + self._default_config = default_config + self._diskcache = diskcache + + def iter_parsed_project( + self, parsed_project: fp.ParsedProject[PintRootBlock, ParserConfig] + ) -> ty.Generator[fp.ParsedStatement[ParserConfig], None, None]: + last_location = None + for stmt in parsed_project.iter_blocks(): + if isinstance(stmt, fp.BOS): + if isinstance(stmt, fp.BOF): + last_location = str(stmt.path) + continue + elif isinstance(stmt, fp.BOR): + last_location = ( + f"[package: {stmt.package}, resource: {stmt.resource_name}]" + ) + continue + else: + last_location = "orphan string" + continue + + if isinstance(stmt, self.skip_classes): + continue + + assert isinstance(last_location, str) + 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 | str, cfg: ParserConfig | None = None + ) -> fp.ParsedProject[PintRootBlock, ParserConfig]: + return fp.parse( + filename, + _PintParser, + cfg or self._default_config, + diskcache=self._diskcache, + strip_spaces=True, + delimiters=_PintParser._delimiters, + ) + + def parse_string( + self, content: str, cfg: ParserConfig | None = None + ) -> fp.ParsedProject[PintRootBlock, ParserConfig]: + return fp.parse_bytes( + content.encode("utf-8"), + _PintParser, + cfg or self._default_config, + diskcache=self._diskcache, + strip_spaces=True, + delimiters=_PintParser._delimiters, + ) diff --git a/datasette/vendored/pint/delegates/txt_defparser/group.py b/datasette/vendored/pint/delegates/txt_defparser/group.py new file mode 100644 index 0000000000..120438a831 --- /dev/null +++ b/datasette/vendored/pint/delegates/txt_defparser/group.py @@ -0,0 +1,111 @@ +""" + 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 + +import flexparser as fp + +from ...facets.group import definitions +from ..base_defparser import PintParsedStatement +from . import block, common, plain + + +@dataclass(frozen=True) +class BeginGroup(PintParsedStatement): + """Being of a group directive. + + @group [using , ..., ] + """ + + #: Regex to match the header parts of a definition. + _header_re = re.compile(r"@group\s+(?P\w+)\s*(using\s(?P.*))*") + + name: str + using_group_names: ty.Tuple[str, ...] + + @classmethod + def from_string(cls, s: str) -> fp.NullableParsedResult[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[ + definitions.GroupDefinition, + BeginGroup, + ty.Union[ + plain.CommentDefinition, + plain.UnitDefinition, + ], + ] +): + """Definition of a group. + + @group [using , ..., ] + + ... + + @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 + + """ + + def derive_definition(self) -> definitions.GroupDefinition: + return definitions.GroupDefinition( + self.name, self.using_group_names, self.definitions + ) + + @property + def name(self) -> str: + assert isinstance(self.opening, BeginGroup) + return self.opening.name + + @property + def using_group_names(self) -> tuple[str, ...]: + assert isinstance(self.opening, BeginGroup) + return self.opening.using_group_names + + @property + def definitions(self) -> tuple[plain.UnitDefinition, ...]: + return tuple(el for el in self.body if isinstance(el, plain.UnitDefinition)) diff --git a/datasette/vendored/pint/delegates/txt_defparser/plain.py b/datasette/vendored/pint/delegates/txt_defparser/plain.py new file mode 100644 index 0000000000..ac4230bcb8 --- /dev/null +++ b/datasette/vendored/pint/delegates/txt_defparser/plain.py @@ -0,0 +1,279 @@ +""" + 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 + +import flexparser as fp + +from ...converters import Converter +from ...facets.plain import definitions +from ...util import UnitsContainer +from ..base_defparser import ParserConfig, PintParsedStatement +from . import common + + +@dataclass(frozen=True) +class Equality(PintParsedStatement, 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.NullableParsedResult[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(PintParsedStatement, 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.NullableParsedResult[CommentDefinition]: + if not s.startswith("#"): + return None + return cls(s[1:].strip()) + + +@dataclass(frozen=True) +class PrefixDefinition(PintParsedStatement, definitions.PrefixDefinition): + """Definition of a prefix:: + + - = [= ] [= ] [ = ] [...] + + Example:: + + deca- = 1e+1 = da- = deka- + """ + + @classmethod + def from_string_and_config( + cls, s: str, config: ParserConfig + ) -> fp.NullableParsedResult[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(PintParsedStatement, definitions.UnitDefinition): + """Definition of a unit:: + + = [= ] [= ] [ = ] [...] + + 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.NullableParsedResult[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 = { + 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(PintParsedStatement, definitions.DimensionDefinition): + """Definition of a root dimension:: + + [dimension name] + + Example:: + + [volume] + """ + + @classmethod + def from_string(cls, s: str) -> fp.NullableParsedResult[DimensionDefinition]: + s = s.strip() + + if not (s.startswith("[") and "=" not in s): + return None + + return cls(s) + + +@dataclass(frozen=True) +class DerivedDimensionDefinition( + PintParsedStatement, definitions.DerivedDimensionDefinition +): + """Definition of a derived dimension:: + + [dimension name] = + + Example:: + + [density] = [mass] / [volume] + """ + + @classmethod + def from_string_and_config( + cls, s: str, config: ParserConfig + ) -> fp.NullableParsedResult[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(PintParsedStatement, definitions.AliasDefinition): + """Additional alias(es) for an already existing unit:: + + @alias = [ = ] [...] + + Example:: + + @alias meter = my_meter + """ + + @classmethod + def from_string(cls, s: str) -> fp.NullableParsedResult[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/datasette/vendored/pint/delegates/txt_defparser/system.py b/datasette/vendored/pint/delegates/txt_defparser/system.py new file mode 100644 index 0000000000..8c45b0b0b2 --- /dev/null +++ b/datasette/vendored/pint/delegates/txt_defparser/system.py @@ -0,0 +1,117 @@ +""" + 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 + +import flexparser as fp + +from ...facets.system import definitions +from ..base_defparser import PintParsedStatement +from . import block, common, plain + + +@dataclass(frozen=True) +class BaseUnitRule(PintParsedStatement, definitions.BaseUnitRule): + @classmethod + def from_string(cls, s: str) -> fp.NullableParsedResult[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(PintParsedStatement): + """Being of a system directive. + + @system [using , ..., ] + """ + + #: Regex to match the header parts of a context. + _header_re = re.compile(r"@system\s+(?P\w+)\s*(using\s(?P.*))*") + + name: str + using_group_names: ty.Tuple[str, ...] + + @classmethod + def from_string(cls, s: str) -> fp.NullableParsedResult[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[ + definitions.SystemDefinition, + BeginSystem, + ty.Union[plain.CommentDefinition, BaseUnitRule], + ] +): + """Definition of a System: + + @system [using , ..., ] + + ... + + @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. + """ + + def derive_definition(self) -> definitions.SystemDefinition: + return definitions.SystemDefinition( + self.name, self.using_group_names, self.rules + ) + + @property + def name(self) -> str: + assert isinstance(self.opening, BeginSystem) + return self.opening.name + + @property + def using_group_names(self) -> tuple[str, ...]: + assert isinstance(self.opening, BeginSystem) + return self.opening.using_group_names + + @property + def rules(self) -> tuple[BaseUnitRule, ...]: + return tuple(el for el in self.body if isinstance(el, BaseUnitRule)) diff --git a/datasette/vendored/pint/errors.py b/datasette/vendored/pint/errors.py new file mode 100644 index 0000000000..faa9785bbc --- /dev/null +++ b/datasette/vendored/pint/errors.py @@ -0,0 +1,255 @@ +""" + pint.errors + ~~~~~~~~~~~ + + Functions and classes related to unit definitions and conversions. + + :copyright: 2016 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 + +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" + +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: str) -> bool: + """Return True if the name is flanked by square brackets `[` and `]`.""" + return name[0] == "[" and name[-1] == "]" + + +def is_valid_prefix_name(name: str) -> bool: + """Return True if the name is a valid python identifier or empty.""" + return str.isidentifier(name) or name == "" + + +is_valid_unit_name = is_valid_system_name = is_valid_context_name = str.isidentifier + + +def _no_space(name: str) -> bool: + """Return False if the name contains a space in any position.""" + 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: str) -> bool: + """Return True if the name is consistent with a dimension name. + + - flanked by square brackets. + - empty dimension name or identifier. + """ + + # TODO: shall we check also fro spaces? + 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: str): + return DefinitionError(self.name, self.__class__, msg) + + +@dataclass(frozen=True) +class PintError(Exception): + """Base exception for all Pint errors.""" + + +@dataclass(frozen=True) +class DefinitionError(ValueError, PintError): + """Raised when a definition is not properly constructed.""" + + name: str + definition_type: type + msg: str + + def __str__(self): + 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)) + + +@dataclass(frozen=True) +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 self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + + +@dataclass(frozen=True) +class RedefinitionError(ValueError, PintError): + """Raised when a unit or prefix is redefined.""" + + name: str + definition_type: type + + def __str__(self): + msg = f"Cannot redefine '{self.name}' ({self.definition_type})" + return msg + + def __reduce__(self): + return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + + +@dataclass(frozen=True) +class UndefinedUnitError(AttributeError, PintError): + """Raised when the units are not defined in the unit registry.""" + + unit_names: str | tuple[str, ...] + + def __str__(self): + 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=True) +class PintTypeError(TypeError, PintError): + def __reduce__(self): + return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + + +@dataclass(frozen=True) +class DimensionalityError(PintTypeError): + """Raised when trying to convert between incompatible units.""" + + units1: ty.Any + units2: ty.Any + dim1: str = "" + dim2: str = "" + extra_msg: str = "" + + def __str__(self): + if self.dim1 or self.dim2: + dim1 = f" ({self.dim1})" + dim2 = f" ({self.dim2})" + else: + dim1 = "" + dim2 = "" + + return ( + f"Cannot convert from '{self.units1}'{dim1} to " + f"'{self.units2}'{dim2}{self.extra_msg}" + ) + + def __reduce__(self): + return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + + +@dataclass(frozen=True) +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.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=True) +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.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=True) +class UnitStrippedWarning(UserWarning, PintError): + msg: str + + def __reduce__(self): + return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + + +@dataclass(frozen=True) +class UnexpectedScaleInContainer(Exception): + def __reduce__(self): + return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + + +@dataclass(frozen=True) +class UndefinedBehavior(UserWarning, PintError): + msg: str + + def __reduce__(self): + return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) diff --git a/datasette/vendored/pint/facets/__init__.py b/datasette/vendored/pint/facets/__init__.py new file mode 100644 index 0000000000..12729289cf --- /dev/null +++ b/datasette/vendored/pint/facets/__init__.py @@ -0,0 +1,106 @@ +""" + pint.facets + ~~~~~~~~~~~ + + Facets are way to add a specific set of funcionalities to Pint. It is more + an organization logic than anything else. It aims to enable growth while + keeping each part small enough to be hackable. + + Each facet contains one or more of the following modules: + - definitions: classes describing specific unit-related definitons. + 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) + + 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. + + 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 + other module (except for testing). This is Python, so accessing it cannot be + really limited. So is more an agreement than a rule. + + It is worth noticing that a Pint Quantity or Unit is always connected to a + *specific* registry. Therefore we need to provide a way in which functionality + can be added to a Quantity class in an easy way. This is achieved beautifully + using specific class attributes. For example, the NumpyRegistry looks like this: + + class NumpyRegistry: + + Quantity = NumpyQuantity + Unit = NumpyUnit + + This tells pint that it should use NumpyQuantity as base class for a quantity + class that belongs to a registry that has NumpyRegistry as one of its bases. + + Currently the folowing facets are implemented: + + - plain: basic manipulation and calculation with multiplicative + dimensions, units and quantities (e.g. length, time, mass, etc). + + - nonmultiplicative: manipulation and calculation with offset and + log units and quantities (e.g. temperature and decibel). + + - measurement: manipulation and calculation of a quantity with + an uncertainty. + + - numpy: using numpy array as magnitude and properly handling + numpy functions operating on quantities. + + - dask: allows pint to interoperate with dask by implementing + dask magic methods. + + - group: allow to make collections of units that can be then + addressed together. + + - system: redefine base units for dimensions for a particular + collection of units (e.g. imperial) + + - context: provides the means to interconvert between incompatible + units through well defined relations (e.g. spectroscopy allows + converting between spatial wavelength and temporal frequency) + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from .context import ContextRegistry, GenericContextRegistry +from .dask import DaskRegistry, GenericDaskRegistry +from .group import GenericGroupRegistry, GroupRegistry +from .measurement import GenericMeasurementRegistry, MeasurementRegistry +from .nonmultiplicative import ( + GenericNonMultiplicativeRegistry, + NonMultiplicativeRegistry, +) +from .numpy import GenericNumpyRegistry, NumpyRegistry +from .plain import GenericPlainRegistry, MagnitudeT, PlainRegistry, QuantityT, UnitT +from .system import GenericSystemRegistry, SystemRegistry + +__all__ = [ + "ContextRegistry", + "DaskRegistry", + "FormattingRegistry", + "GroupRegistry", + "MeasurementRegistry", + "NonMultiplicativeRegistry", + "NumpyRegistry", + "PlainRegistry", + "SystemRegistry", + "GenericContextRegistry", + "GenericDaskRegistry", + "GenericFormattingRegistry", + "GenericGroupRegistry", + "GenericMeasurementRegistry", + "GenericNonMultiplicativeRegistry", + "GenericNumpyRegistry", + "GenericPlainRegistry", + "GenericSystemRegistry", + "QuantityT", + "UnitT", + "MagnitudeT", +] diff --git a/datasette/vendored/pint/facets/context/__init__.py b/datasette/vendored/pint/facets/context/__init__.py new file mode 100644 index 0000000000..28c7b5ced0 --- /dev/null +++ b/datasette/vendored/pint/facets/context/__init__.py @@ -0,0 +1,18 @@ +""" + pint.facets.context + ~~~~~~~~~~~~~~~~~~~ + + Adds pint the capability to contexts: predefined conversions + between incompatible dimensions. + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from .definitions import ContextDefinition +from .objects import Context +from .registry import ContextRegistry, GenericContextRegistry + +__all__ = ["ContextDefinition", "Context", "ContextRegistry", "GenericContextRegistry"] diff --git a/datasette/vendored/pint/facets/context/definitions.py b/datasette/vendored/pint/facets/context/definitions.py new file mode 100644 index 0000000000..76f84d63d8 --- /dev/null +++ b/datasette/vendored/pint/facets/context/definitions.py @@ -0,0 +1,157 @@ +""" + pint.facets.context.definitions + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import itertools +import numbers +import re +from collections.abc import Callable, Iterable +from dataclasses import dataclass +from typing import TYPE_CHECKING + +from ... import errors +from ..plain import UnitDefinition + +if TYPE_CHECKING: + from ..._typing import Quantity, UnitsContainer + + +@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]: + """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) -> bool: + raise NotImplementedError + + +@dataclass(frozen=True) +class ForwardRelation(Relation): + """A relation connecting a dimension to another via a transformation function. + + -> : + """ + + @property + def bidirectional(self) -> bool: + return False + + +@dataclass(frozen=True) +class BidirectionalRelation(Relation): + """A bidirectional relation connecting a dimension to another + via a simple transformation function. + + <-> : + + """ + + @property + def bidirectional(self) -> bool: + return True + + +@dataclass(frozen=True) +class ContextDefinition(errors.WithDefErr): + """Definition of a Context""" + + #: name of the context + name: str + #: other na + aliases: tuple[str, ...] + defaults: dict[str, numbers.Number] + relations: tuple[Relation, ...] + redefinitions: tuple[UnitDefinition, ...] + + @property + def variables(self) -> set[str]: + """Return all variable names in all transformations.""" + return set().union(*(r.variables for r in self.relations)) + + @classmethod + def from_lines(cls, lines: Iterable[str], non_int_type: 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 invalid: + raise self.def_err( + f"relation refers to {', '.join(invalid)} that " + + errors.MSG_INVALID_DIMENSION_NAME + ) + + 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/datasette/vendored/pint/facets/context/objects.py b/datasette/vendored/pint/facets/context/objects.py new file mode 100644 index 0000000000..bf8c74d9a6 --- /dev/null +++ b/datasette/vendored/pint/facets/context/objects.py @@ -0,0 +1,336 @@ +""" + pint.facets.context.objects + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import weakref +from collections import ChainMap, defaultdict +from collections.abc import Callable, Iterable +from typing import TYPE_CHECKING, Any, Generic, Protocol + +from ..._typing import Magnitude +from ...facets.plain import MagnitudeT, PlainQuantity, PlainUnit, UnitDefinition +from ...util import UnitsContainer, to_units_container +from .definitions import ContextDefinition + +if TYPE_CHECKING: + from ...registry import UnitRegistry + + +class Transformation(Protocol): + def __call__( + self, ureg: UnitRegistry, value: PlainQuantity, **kwargs: Any + ) -> PlainQuantity: ... + + +from ..._typing import UnitLike + +ToBaseFunc = Callable[[UnitsContainer], UnitsContainer] +SrcDst = tuple[UnitsContainer, UnitsContainer] + + +class ContextQuantity(Generic[MagnitudeT], PlainQuantity[MagnitudeT]): + pass + + +class ContextUnit(PlainUnit): + pass + + +class Context: + """A specialized container that defines transformation functions from one + dimension to another. Each Dimension are specified using a UnitsContainer. + Simple transformation are given with a function taking a single parameter. + + Conversion functions may take optional keyword arguments and the context + can have default values for these arguments. + + Additionally, a context may host redefinitions. + + A redefinition must be performed among units that already exist in the registry. It + cannot change the dimensionality of a unit. The symbol and aliases are automatically + inherited from the registry. + + See ContextDefinition for the definition file syntax. + + Parameters + ---------- + name : str or None, optional + Name of the context (must be unique within the registry). + Use None for anonymous Context. (Default value = None). + aliases : iterable of str + Other names for the context. + defaults : None or dict + Maps variable names to values. + + Example + ------- + + >>> from datasette.vendored.pint.util import UnitsContainer + >>> from datasette.vendored.pint import Context, UnitRegistry + >>> ureg = UnitRegistry() + >>> timedim = UnitsContainer({'[time]': 1}) + >>> spacedim = UnitsContainer({'[length]': 1}) + >>> def time_to_len(ureg, time): + ... 'Time to length converter' + ... return 3. * time + >>> c = Context() + >>> c.add_transformation(timedim, spacedim, time_to_len) + >>> c.transform(timedim, spacedim, ureg, 2) + 6.0 + >>> def time_to_len_indexed(ureg, time, n=1): + ... 'Time to length converter, n is the index of refraction of the material' + ... return 3. * time / n + >>> c = Context(defaults={'n':3}) + >>> c.add_transformation(timedim, spacedim, time_to_len_indexed) + >>> c.transform(timedim, spacedim, ureg, 2) + 2.0 + >>> c.redefine("pound = 0.5 kg") + """ + + def __init__( + self, + name: str | None = None, + aliases: tuple[str, ...] = tuple(), + defaults: dict[str, Any] | None = None, + ) -> None: + self.name: str | None = name + self.aliases: tuple[str, ...] = aliases + + #: Maps (src, dst) -> transformation function + self.funcs: dict[SrcDst, Transformation] = {} + + #: Maps defaults variable names to values + self.defaults: dict[str, Any] = defaults or {} + + # Store Definition objects that are context-specific + # TODO: narrow type this if possible. + self.redefinitions: list[Any] = [] + + # Flag set to True by the Registry the first time the context is enabled + self.checked = False + + #: Maps (src, dst) -> self + #: Used as a convenience dictionary to be composed by ContextChain + self.relation_to_context: weakref.WeakValueDictionary[SrcDst, Context] = ( + weakref.WeakValueDictionary() + ) + + @classmethod + def from_context(cls, context: Context, **defaults: Any) -> Context: + """Creates a new context that shares the funcs dictionary with the + original context. The default values are copied from the original + context and updated with the new defaults. + + If defaults is empty, return the same context. + + Parameters + ---------- + context : pint.Context + Original context. + **defaults + + + Returns + ------- + pint.Context + """ + if defaults: + newdef = dict(context.defaults, **defaults) + c = cls(context.name, context.aliases, newdef) + c.funcs = context.funcs + c.redefinitions = context.redefinitions + for edge in context.funcs: + c.relation_to_context[edge] = c + return c + return context + + @classmethod + def from_lines( + cls, + lines: Iterable[str], + to_base_func: ToBaseFunc | None = None, + non_int_type: type = float, + ) -> Context: + context_definition = ContextDefinition.from_lines(lines, non_int_type) + + if context_definition is None: + raise ValueError(f"Could not define Context from from {lines}") + + return cls.from_definition(context_definition, to_base_func) + + @classmethod + def from_definition( + cls, cd: ContextDefinition, to_base_func: ToBaseFunc | None = None + ) -> Context: + ctx = cls(cd.name, cd.aliases, cd.defaults) + + for definition in cd.redefinitions: + ctx._redefine(definition) + + for relation in cd.relations: + try: + # TODO: check to_base_func. Is it a good API idea? + 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.transformation) + if relation.bidirectional: + ctx.add_transformation(dst, src, relation.transformation) + except Exception as exc: + raise ValueError( + f"Could not add Context {cd.name} relation {relation}" + ) from exc + + return ctx + + def add_transformation( + self, src: UnitLike, dst: UnitLike, func: Transformation + ) -> None: + """Add a transformation function to the context.""" + + _key = self.__keytransform__(src, dst) + self.funcs[_key] = func + self.relation_to_context[_key] = self + + def remove_transformation(self, src: UnitLike, dst: UnitLike) -> None: + """Add a transformation function to the context.""" + + _key = self.__keytransform__(src, dst) + del self.funcs[_key] + del self.relation_to_context[_key] + + @staticmethod + def __keytransform__(src: UnitLike, dst: UnitLike) -> SrcDst: + return to_units_container(src), to_units_container(dst) + + def transform( + self, src: UnitLike, dst: UnitLike, registry: Any, value: Magnitude + ) -> Magnitude: + """Transform a value.""" + + _key = self.__keytransform__(src, dst) + func = self.funcs[_key] + return func(registry, value, **self.defaults) + + def redefine(self, definition: str) -> None: + """Override the definition of a unit in the registry. + + Parameters + ---------- + definition : str + = ``, e.g. ``pound = 0.5 kg`` + """ + 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) + + def hashable( + self, + ) -> tuple[ + str | None, + tuple[str, ...], + frozenset[tuple[SrcDst, int]], + frozenset[tuple[str, Any]], + tuple[Any, ...], + ]: + """Generate a unique hashable and comparable representation of self, which can + be used as a key in a dict. This class cannot define ``__hash__`` because it is + mutable, and the Python interpreter does cache the output of ``__hash__``. + + Returns + ------- + tuple + """ + return ( + self.name, + tuple(self.aliases), + frozenset((k, id(v)) for k, v in self.funcs.items()), + frozenset(self.defaults.items()), + tuple(self.redefinitions), + ) + + +class ContextChain(ChainMap[SrcDst, Context]): + """A specialized ChainMap for contexts that simplifies finding rules + to transform from one dimension to another. + """ + + def __init__(self): + super().__init__() + self.contexts: list[Context] = [] + self.maps.clear() # Remove default empty map + self._graph: dict[SrcDst, set[UnitsContainer]] | None = None + + def insert_contexts(self, *contexts: Context): + """Insert one or more contexts in reversed order the chained map. + (A rule in last context will take precedence) + + To facilitate the identification of the context with the matching rule, + the *relation_to_context* dictionary of the context is used. + """ + + self.contexts = list(reversed(contexts)) + self.contexts + self.maps = [ctx.relation_to_context for ctx in reversed(contexts)] + self.maps + self._graph = None + + def remove_contexts(self, n: int | None = None): + """Remove the last n inserted contexts from the chain. + + Parameters + ---------- + n: int + (Default value = None) + """ + + del self.contexts[:n] + del self.maps[:n] + self._graph = None + + @property + def defaults(self) -> dict[str, Any]: + for ctx in self.values(): + return ctx.defaults + return {} + + @property + def graph(self): + """The graph relating""" + if self._graph is None: + self._graph = defaultdict(set) + for fr_, to_ in self: + self._graph[fr_].add(to_) + return self._graph + + # TODO: type registry + def transform( + self, src: UnitsContainer, dst: UnitsContainer, registry: Any, value: Magnitude + ): + """Transform the value, finding the rule in the chained context. + (A rule in last context will take precedence) + """ + return self[(src, dst)].transform(src, dst, registry, value) + + def hashable(self) -> tuple[Any, ...]: + """Generate a unique hashable and comparable representation of self, which can + be used as a key in a dict. This class cannot define ``__hash__`` because it is + mutable, and the Python interpreter does cache the output of ``__hash__``. + """ + return tuple(ctx.hashable() for ctx in self.contexts) diff --git a/datasette/vendored/pint/facets/context/registry.py b/datasette/vendored/pint/facets/context/registry.py new file mode 100644 index 0000000000..8f9f71ca5e --- /dev/null +++ b/datasette/vendored/pint/facets/context/registry.py @@ -0,0 +1,428 @@ +""" + pint.facets.context.registry + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import functools +from collections import ChainMap +from collections.abc import Callable, Generator +from contextlib import contextmanager +from typing import Any, Generic + +from ..._typing import F, Magnitude +from ...compat import TypeAlias +from ...errors import UndefinedUnitError +from ...util import UnitsContainer, find_connected_nodes, find_shortest_path, logger +from ..plain import GenericPlainRegistry, QuantityT, UnitDefinition, UnitT +from . import objects +from .definitions import ContextDefinition + +# TODO: Put back annotation when possible +# registry_cache: "RegistryCache" + + +class ContextCacheOverlay: + """Layer on top of the plain UnitRegistry cache, specific to a combination of + active contexts which contain unit redefinitions. + """ + + def __init__(self, registry_cache) -> None: + self.dimensional_equivalents = registry_cache.dimensional_equivalents + self.root_units = {} + self.dimensionality = registry_cache.dimensionality + self.parse_unit = registry_cache.parse_unit + self.conversion_factor = {} + + +class GenericContextRegistry( + Generic[QuantityT, UnitT], GenericPlainRegistry[QuantityT, UnitT] +): + """Handle of Contexts. + + Conversion between units with different dimensions according + to previously established relations (contexts). + (e.g. in the spectroscopy, conversion between frequency and energy is possible) + + Capabilities: + + - Register contexts. + - Enable and disable contexts. + - Parse @context directive. + """ + + Context: type[objects.Context] = objects.Context + + def __init__(self, **kwargs: Any) -> None: + # Map context name (string) or abbreviation to context. + self._contexts: dict[str, objects.Context] = {} + # Stores active contexts. + self._active_ctx = objects.ContextChain() + # Map context chain to cache + self._caches = {} + # Map context chain to units override + self._context_units = {} + + super().__init__(**kwargs) + + # Allow contexts to add override layers to the units + self._units: ChainMap[str, UnitDefinition] = ChainMap(self._units) + + def _register_definition_adders(self) -> None: + super()._register_definition_adders() + self._register_adder(ContextDefinition, self.add_context) + + def add_context(self, context: objects.Context | ContextDefinition) -> None: + """Add a context object to the registry. + + The context will be accessible by its name and aliases. + + Notice that this method will NOT enable the context; + see :meth:`enable_contexts`. + """ + if isinstance(context, ContextDefinition): + context = objects.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: + logger.warning( + "The name %s was already registered for another context.", context.name + ) + self._contexts[context.name] = context + for alias in context.aliases: + if alias in self._contexts: + logger.warning( + "The name %s was already registered for another context", + context.name, + ) + self._contexts[alias] = context + + def remove_context(self, name_or_alias: str) -> objects.Context: + """Remove a context from the registry and return it. + + Notice that this methods will not disable the context; + see :meth:`disable_contexts`. + """ + context = self._contexts[name_or_alias] + + del self._contexts[context.name] + for alias in context.aliases: + del self._contexts[alias] + + return context + + def _build_cache(self, loaded_files=None) -> None: + super()._build_cache(loaded_files) + self._caches[()] = self._cache + + def _switch_context_cache_and_units(self) -> None: + """If any of the active contexts redefine units, create variant self._cache + and self._units specific to the combination of active contexts. + The next time this method is invoked with the same combination of contexts, + reuse the same variant self._cache and self._units as in the previous time. + """ + del self._units.maps[:-1] + units_overlay = any(ctx.redefinitions for ctx in self._active_ctx.contexts) + if not units_overlay: + # Use the default _cache and _units + self._cache = self._caches[()] + return + + key = self._active_ctx.hashable() + try: + self._cache = self._caches[key] + self._units.maps.insert(0, self._context_units[key]) + except KeyError: + pass + + # First time using this specific combination of contexts and it contains + # unit redefinitions + base_cache = self._caches[()] + self._caches[key] = self._cache = ContextCacheOverlay(base_cache) + + self._context_units[key] = units_overlay = {} + self._units.maps.insert(0, units_overlay) + + on_redefinition_backup = self._on_redefinition + self._on_redefinition = "ignore" + try: + for ctx in reversed(self._active_ctx.contexts): + for definition in ctx.redefinitions: + self._redefine(definition) + finally: + self._on_redefinition = on_redefinition_backup + + def _redefine(self, definition: UnitDefinition) -> None: + """Redefine a unit from a context""" + # Find original definition in the UnitRegistry + candidates = self.parse_unit_name(definition.name) + if not candidates: + raise UndefinedUnitError(definition.name) + candidates_no_prefix = [c for c in candidates if not c[0]] + if not candidates_no_prefix: + raise ValueError(f"Can't redefine a unit with a prefix: {definition.name}") + assert len(candidates_no_prefix) == 1 + _, name, _ = candidates_no_prefix[0] + try: + basedef = self._units[name] + except KeyError: + raise UndefinedUnitError(name) + + # Rebuild definition as a variant of the plain + if basedef.is_base: + raise ValueError("Can't redefine a plain unit to a derived one") + + dims_old = self._get_dimensionality(basedef.reference) + dims_new = self._get_dimensionality(definition.reference) + if dims_old != dims_new: + raise ValueError( + f"Can't change dimensionality of {basedef.name} " + f"from {dims_old} to {dims_new} in a context" + ) + + # Do not modify in place the original definition, as (1) the context may + # be shared by other registries, and (2) it would alter the cache key + definition = UnitDefinition( + name=basedef.name, + defined_symbol=basedef.symbol, + aliases=basedef.aliases, + reference=definition.reference, + converter=definition.converter, + ) + + # Write into the context-specific self._units.maps[0] and self._cache.root_units + self.define(definition) + + def enable_contexts( + self, *names_or_contexts: str | objects.Context, **kwargs: Any + ) -> None: + """Enable contexts provided by name or by object. + + Parameters + ---------- + *names_or_contexts : + one or more contexts or context names/aliases + **kwargs : + keyword arguments for the context(s) + + Examples + -------- + See :meth:`context` + """ + + # If present, copy the defaults from the containing contexts + if self._active_ctx.defaults: + kwargs = dict(self._active_ctx.defaults, **kwargs) + + # For each name, we first find the corresponding context + ctxs = [ + self._contexts[name] if isinstance(name, str) else name + for name in names_or_contexts + ] + + # Check if the contexts have been checked first, if not we make sure + # that dimensions are expressed in terms of plain dimensions. + for ctx in ctxs: + if ctx.checked: + continue + funcs_copy = dict(ctx.funcs) + for (src, dst), func in funcs_copy.items(): + src_ = self._get_dimensionality(src) + dst_ = self._get_dimensionality(dst) + if src != src_ or dst != dst_: + ctx.remove_transformation(src, dst) + ctx.add_transformation(src_, dst_, func) + ctx.checked = True + + # and create a new one with the new defaults. + contexts = tuple(objects.Context.from_context(ctx, **kwargs) for ctx in ctxs) + + # Finally we add them to the active context. + self._active_ctx.insert_contexts(*contexts) + self._switch_context_cache_and_units() + + def disable_contexts(self, n: int | None = None) -> None: + """Disable the last n enabled contexts. + + Parameters + ---------- + n : int + Number of contexts to disable. Default: disable all contexts. + """ + self._active_ctx.remove_contexts(n) + self._switch_context_cache_and_units() + + @contextmanager + def context( + self: GenericContextRegistry[QuantityT, UnitT], *names: str, **kwargs: Any + ) -> Generator[GenericContextRegistry[QuantityT, UnitT], None, None]: + """Used as a context manager, this function enables to activate a context + which is removed after usage. + + Parameters + ---------- + *names : name(s) of the context(s). + **kwargs : keyword arguments for the contexts. + + 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 + + If a context has an argument, you can specify its value as a keyword argument: + + >>> with ureg.context('one', n=1): + ... pass + + Multiple contexts can be entered in single call: + + >>> with ureg.context('one', 'two', n=1): + ... pass + + 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 + + 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 + """ + # Enable the contexts. + self.enable_contexts(*names, **kwargs) + + try: + # After adding the context and rebuilding the graph, the registry + # is ready to use. + yield self + finally: + # Upon leaving the with statement, + # the added contexts are removed from the active one. + self.disable_contexts(len(names)) + + def with_context(self, name: str, **kwargs: Any) -> Callable[[F], F]: + """Decorator to wrap a function call in a Pint context. + + Use it to ensure that a certain context is active when + calling a function. + + Parameters + ---------- + name : + name of the context. + **kwargs : + keyword arguments for the context + + + Returns + ------- + callable: the wrapped function. + + Examples + -------- + >>> @ureg.with_context('sp') + ... def my_cool_fun(wavelength): + ... print('This wavelength is equivalent to: %s', wavelength.to('terahertz')) + """ + + def decorator(func): + assigned = tuple( + attr for attr in functools.WRAPPER_ASSIGNMENTS if hasattr(func, attr) + ) + updated = tuple( + attr for attr in functools.WRAPPER_UPDATES if hasattr(func, attr) + ) + + @functools.wraps(func, assigned=assigned, updated=updated) + def wrapper(*values, **wrapper_kwargs): + with self.context(name, **kwargs): + return func(*values, **wrapper_kwargs) + + return wrapper + + return decorator + + def _convert( + self, + value: Magnitude, + src: UnitsContainer, + dst: UnitsContainer, + inplace: bool = False, + ) -> Magnitude: + """Convert value from some source to destination units. + + In addition to what is done by the PlainRegistry, + converts between units with different dimensions by following + transformation rules defined in the context. + + Parameters + ---------- + value : + value + src : UnitsContainer + source units. + dst : UnitsContainer + destination units. + inplace : + (Default value = False) + + Returns + ------- + callable + converted value + """ + # If there is an active context, we look for a path connecting source and + # 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) + + path = find_shortest_path(self._active_ctx.graph, src_dim, dst_dim) + if path: + src = self.Quantity(value, src) + for a, b in zip(path[:-1], path[1:]): + src = self._active_ctx.transform(a, b, self, src) + + value, src = src._magnitude, src._units + + return super()._convert(value, src, dst, inplace) + + def _get_compatible_units( + self, input_units: UnitsContainer, group_or_system: str | None = None + ): + src_dim = self._get_dimensionality(input_units) + + ret = super()._get_compatible_units(input_units, group_or_system) + + if self._active_ctx: + ret = ret.copy() # Do not alter self._cache + nodes = find_connected_nodes(self._active_ctx.graph, src_dim) + if nodes: + for node in nodes: + ret |= self._cache.dimensional_equivalents[node] + + return ret + + +class ContextRegistry( + GenericContextRegistry[objects.ContextQuantity[Any], objects.ContextUnit] +): + Quantity: TypeAlias = objects.ContextQuantity[Any] + Unit: TypeAlias = objects.ContextUnit diff --git a/datasette/vendored/pint/facets/dask/__init__.py b/datasette/vendored/pint/facets/dask/__init__.py new file mode 100644 index 0000000000..49ff57ef7c --- /dev/null +++ b/datasette/vendored/pint/facets/dask/__init__.py @@ -0,0 +1,141 @@ +""" + pint.facets.dask + ~~~~~~~~~~~~~~~~ + + Adds pint the capability to interoperate with Dask + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import functools +from typing import Any, Generic + +from ...compat import TypeAlias, compute, dask_array, persist, visualize +from ..plain import ( + GenericPlainRegistry, + MagnitudeT, + PlainQuantity, + PlainUnit, + QuantityT, + UnitT, +) + + +def check_dask_array(f): + @functools.wraps(f) + def wrapper(self, *args, **kwargs): + if isinstance(self._magnitude, dask_array.Array): + return f(self, *args, **kwargs) + else: + msg = "Method {} only implemented for objects of {}, not {}".format( + f.__name__, dask_array.Array, self._magnitude.__class__ + ) + raise AttributeError(msg) + + return wrapper + + +class DaskQuantity(Generic[MagnitudeT], PlainQuantity[MagnitudeT]): + # Dask.array.Array ducking + def __dask_graph__(self): + if isinstance(self._magnitude, dask_array.Array): + return self._magnitude.__dask_graph__() + + return None + + def __dask_keys__(self): + return self._magnitude.__dask_keys__() + + def __dask_tokenize__(self): + from dask.base import tokenize + + return (type(self), tokenize(self._magnitude), self.units) + + @property + def __dask_optimize__(self): + return dask_array.Array.__dask_optimize__ + + @property + def __dask_scheduler__(self): + return dask_array.Array.__dask_scheduler__ + + def __dask_postcompute__(self): + func, args = self._magnitude.__dask_postcompute__() + return self._dask_finalize, (func, args, self.units) + + def __dask_postpersist__(self): + func, args = self._magnitude.__dask_postpersist__() + return self._dask_finalize, (func, args, self.units) + + def _dask_finalize(self, results, func, args, units): + values = func(results, *args) + return type(self)(values, units) + + @check_dask_array + def compute(self, **kwargs): + """Compute the Dask array wrapped by pint.PlainQuantity. + + Parameters + ---------- + **kwargs : dict + Any keyword arguments to pass to ``dask.compute``. + + Returns + ------- + pint.PlainQuantity + A pint.PlainQuantity wrapped numpy array. + """ + (result,) = compute(self, **kwargs) + return result + + @check_dask_array + def persist(self, **kwargs): + """Persist the Dask Array wrapped by pint.PlainQuantity. + + Parameters + ---------- + **kwargs : dict + Any keyword arguments to pass to ``dask.persist``. + + Returns + ------- + pint.PlainQuantity + A pint.PlainQuantity wrapped Dask array. + """ + (result,) = persist(self, **kwargs) + return result + + @check_dask_array + def visualize(self, **kwargs): + """Produce a visual representation of the Dask graph. + + The graphviz library is required. + + Parameters + ---------- + **kwargs : dict + Any keyword arguments to pass to ``dask.visualize``. + + Returns + ------- + + """ + visualize(self, **kwargs) + + +class DaskUnit(PlainUnit): + pass + + +class GenericDaskRegistry( + Generic[QuantityT, UnitT], GenericPlainRegistry[QuantityT, UnitT] +): + pass + + +class DaskRegistry(GenericDaskRegistry[DaskQuantity[Any], DaskUnit]): + Quantity: TypeAlias = DaskQuantity[Any] + Unit: TypeAlias = DaskUnit diff --git a/datasette/vendored/pint/facets/group/__init__.py b/datasette/vendored/pint/facets/group/__init__.py new file mode 100644 index 0000000000..db488deacb --- /dev/null +++ b/datasette/vendored/pint/facets/group/__init__.py @@ -0,0 +1,24 @@ +""" + pint.facets.group + ~~~~~~~~~~~~~~~~~ + + Adds pint the capability to group units. + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from .definitions import GroupDefinition +from .objects import Group, GroupQuantity, GroupUnit +from .registry import GenericGroupRegistry, GroupRegistry + +__all__ = [ + "GroupDefinition", + "Group", + "GroupRegistry", + "GenericGroupRegistry", + "GroupQuantity", + "GroupUnit", +] diff --git a/datasette/vendored/pint/facets/group/definitions.py b/datasette/vendored/pint/facets/group/definitions.py new file mode 100644 index 0000000000..bec7d8ac08 --- /dev/null +++ b/datasette/vendored/pint/facets/group/definitions.py @@ -0,0 +1,56 @@ +""" + pint.facets.group.definitions + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass + +from ... import errors +from ...compat import Self +from .. import plain + + +@dataclass(frozen=True) +class GroupDefinition(errors.WithDefErr): + """Definition of a group.""" + + #: name of the group + name: str + #: unit groups that will be included within the group + using_group_names: tuple[str, ...] + #: definitions for the units existing within the group + definitions: tuple[plain.UnitDefinition, ...] + + @classmethod + def from_lines( + cls: type[Self], lines: Iterable[str], non_int_type: type + ) -> Self | None: + # 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_names(self) -> tuple[str, ...]: + return tuple(el.name for el in self.definitions) + + def __post_init__(self) -> None: + if not errors.is_valid_group_name(self.name): + raise self.def_err(errors.MSG_INVALID_GROUP_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 + ) diff --git a/datasette/vendored/pint/facets/group/objects.py b/datasette/vendored/pint/facets/group/objects.py new file mode 100644 index 0000000000..751dd37650 --- /dev/null +++ b/datasette/vendored/pint/facets/group/objects.py @@ -0,0 +1,224 @@ +""" + pint.facets.group.objects + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from collections.abc import Callable, Generator, Iterable +from typing import TYPE_CHECKING, Any, Generic + +from ...util import SharedRegistryObject, getattr_maybe_raise +from ..plain import MagnitudeT, PlainQuantity, PlainUnit +from .definitions import GroupDefinition + +if TYPE_CHECKING: + from ..plain import UnitDefinition + + DefineFunc = Callable[ + [ + Any, + ], + None, + ] + AddUnitFunc = Callable[ + [ + UnitDefinition, + ], + None, + ] + + +class GroupQuantity(Generic[MagnitudeT], PlainQuantity[MagnitudeT]): + pass + + +class GroupUnit(PlainUnit): + pass + + +class Group(SharedRegistryObject): + """A group is a set of units. + + Units can be added directly or by including other groups. + + Members are computed dynamically, that is if a unit is added to a group X + all groups that include X are affected. + + The group belongs to one Registry. + + See GroupDefinition for the definition file syntax. + + Parameters + ---------- + name + If not given, a root Group will be created. + """ + + def __init__(self, name: str): + # The name of the group. + self.name = name + + #: Names of the units in this group. + #: :type: set[str] + self._unit_names: set[str] = set() + + #: Names of the groups in this group. + self._used_groups: set[str] = set() + + #: Names of the groups in which this group is contained. + self._used_by: set[str] = set() + + # Add this group to the group dictionary + self._REGISTRY._groups[self.name] = self + + if name != "root": + # All groups are added to root group + self._REGISTRY._groups["root"].add_groups(name) + + #: A cache of the included units. + #: None indicates that the cache has been invalidated. + self._computed_members: frozenset[str] | None = None + + @property + def members(self) -> frozenset[str]: + """Names of the units that are members of the group. + + Calculated to include to all units in all included _used_groups. + + """ + if self._computed_members is None: + tmp = set(self._unit_names) + + for _, group in self.iter_used_groups(): + tmp |= group.members + + self._computed_members = frozenset(tmp) + + return self._computed_members + + def invalidate_members(self) -> None: + """Invalidate computed members in this Group and all parent nodes.""" + self._computed_members = None + d = self._REGISTRY._groups + for name in self._used_by: + d[name].invalidate_members() + + def iter_used_groups(self) -> Generator[tuple[str, Group], None, None]: + pending = set(self._used_groups) + d = self._REGISTRY._groups + while pending: + name = pending.pop() + group = d[name] + pending |= group._used_groups + yield name, d[name] + + def is_used_group(self, group_name: str) -> bool: + for name, _ in self.iter_used_groups(): + if name == group_name: + return True + return False + + def add_units(self, *unit_names: str) -> None: + """Add units to group.""" + for unit_name in unit_names: + self._unit_names.add(unit_name) + + self.invalidate_members() + + @property + def non_inherited_unit_names(self) -> frozenset[str]: + return frozenset(self._unit_names) + + def remove_units(self, *unit_names: str) -> None: + """Remove units from group.""" + for unit_name in unit_names: + self._unit_names.remove(unit_name) + + self.invalidate_members() + + def add_groups(self, *group_names: str) -> None: + """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): + raise ValueError( + "Cyclic relationship found between %s and %s" + % (self.name, group_name) + ) + + self._used_groups.add(group_name) + grp._used_by.add(self.name) + + self.invalidate_members() + + def remove_groups(self, *group_names: str) -> None: + """Remove groups from group.""" + d = self._REGISTRY._groups + for group_name in group_names: + grp = d[group_name] + + self._used_groups.remove(group_name) + grp._used_by.remove(self.name) + + self.invalidate_members() + + @classmethod + def from_lines( + cls, lines: Iterable[str], define_func: DefineFunc, non_int_type: type = float + ) -> Group: + """Return a Group object parsing an iterable of lines. + + 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. + + Returns + ------- + + """ + group_definition = GroupDefinition.from_lines(lines, non_int_type) + + if group_definition is None: + raise ValueError(f"Could not define group from {lines}") + + return cls.from_definition(group_definition, define_func) + + @classmethod + def from_definition( + cls, + group_definition: GroupDefinition, + add_unit_func: AddUnitFunc | None = None, + ) -> Group: + grp = cls(group_definition.name) + + 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) + + return grp + + def __getattr__(self, item: str): + getattr_maybe_raise(self, item) + return self._REGISTRY diff --git a/datasette/vendored/pint/facets/group/registry.py b/datasette/vendored/pint/facets/group/registry.py new file mode 100644 index 0000000000..33f78c6452 --- /dev/null +++ b/datasette/vendored/pint/facets/group/registry.py @@ -0,0 +1,155 @@ +""" + pint.facets.group.registry + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Generic + +from ... import errors +from ...compat import TypeAlias + +if TYPE_CHECKING: + from ..._typing import Unit, UnitsContainer + +from ...util import create_class_with_registry, to_units_container +from ..plain import ( + GenericPlainRegistry, + QuantityT, + UnitDefinition, + UnitT, +) +from . import objects +from .definitions import GroupDefinition + + +class GenericGroupRegistry( + Generic[QuantityT, UnitT], GenericPlainRegistry[QuantityT, UnitT] +): + """Handle of Groups. + + Group units + + Capabilities: + - Register groups. + - 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 = type[objects.Group] + + def __init__(self, **kwargs): + super().__init__(**kwargs) + #: Map group name to group. + self._groups: dict[str, objects.Group] = {} + self._groups["root"] = self.Group("root") + + def _init_dynamic_classes(self) -> None: + """Generate subclasses on the fly and attach them to self""" + super()._init_dynamic_classes() + self.Group = create_class_with_registry(self, objects.Group) + + def _after_init(self) -> None: + """Invoked at the end of ``__init__``. + + - Create default group and add all orphan units to it + - Set default system + """ + super()._after_init() + + #: Copy units not defined in any group to the default group + if "group" in self._defaults: + grp = self.get_group(self._defaults["group"], True) + group_units = frozenset( + [ + member + for group in self._groups.values() + if group.name != "root" + for member in group.members + ] + ) + all_units = self.get_group("root", False).members + grp.add_units(*(all_units - group_units)) + + 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) -> objects.Group: + """Return a Group. + + Parameters + ---------- + name : str + Name of the group to be + create_if_needed : bool + If True, create a group if not found. If False, raise an Exception. + (Default value = True) + + Returns + ------- + Group + Group + """ + if name in self._groups: + return self._groups[name] + + if not create_if_needed: + raise ValueError("Unknown group %s" % name) + + return self.Group(name) + + def get_compatible_units( + self, input_units: UnitsContainer, group: str | None = None + ) -> frozenset[Unit]: + """ """ + if group is None: + return super().get_compatible_units(input_units) + + input_units = to_units_container(input_units) + + equiv = self._get_compatible_units(input_units, group) + + return frozenset(self.Unit(eq) for eq in equiv) + + def _get_compatible_units( + self, input_units: UnitsContainer, group: str | None = None + ) -> frozenset[str]: + ret = super()._get_compatible_units(input_units) + + if not group: + return ret + + if group in self._groups: + members = self._groups[group].members + else: + raise ValueError("Unknown Group with name '%s'" % group) + return frozenset(ret & members) + + +class GroupRegistry( + GenericGroupRegistry[objects.GroupQuantity[Any], objects.GroupUnit] +): + Quantity: TypeAlias = objects.GroupQuantity[Any] + Unit: TypeAlias = objects.GroupUnit diff --git a/datasette/vendored/pint/facets/measurement/__init__.py b/datasette/vendored/pint/facets/measurement/__init__.py new file mode 100644 index 0000000000..0b241ea1de --- /dev/null +++ b/datasette/vendored/pint/facets/measurement/__init__.py @@ -0,0 +1,21 @@ +""" + pint.facets.measurement + ~~~~~~~~~~~~~~~~~~~~~~~ + + Adds pint the capability to handle measurements (quantities with uncertainties). + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from .objects import Measurement, MeasurementQuantity +from .registry import GenericMeasurementRegistry, MeasurementRegistry + +__all__ = [ + "Measurement", + "MeasurementQuantity", + "MeasurementRegistry", + "GenericMeasurementRegistry", +] diff --git a/datasette/vendored/pint/facets/measurement/objects.py b/datasette/vendored/pint/facets/measurement/objects.py new file mode 100644 index 0000000000..28ee7552ea --- /dev/null +++ b/datasette/vendored/pint/facets/measurement/objects.py @@ -0,0 +1,195 @@ +""" + pint.facets.measurement.objects + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import copy +import re +from typing import Generic + +from ...compat import ufloat +from ..plain import MagnitudeT, PlainQuantity, PlainUnit + +MISSING = object() + + +class MeasurementQuantity(Generic[MagnitudeT], PlainQuantity[MagnitudeT]): + # Measurement support + def plus_minus(self, error, relative=False): + if isinstance(error, self.__class__): + if relative: + raise ValueError(f"{error} is not a valid relative error.") + error = error.to(self._units).magnitude + else: + if relative: + error = error * abs(self.magnitude) + + return self._REGISTRY.Measurement(copy.copy(self.magnitude), error, self._units) + + +class MeasurementUnit(PlainUnit): + pass + + +class Measurement(PlainQuantity): + """Implements a class to describe a quantity with uncertainty. + + Parameters + ---------- + value : pint.Quantity or any numeric type + The expected value of the measurement + error : pint.Quantity or any numeric type + The error or uncertainty of the measurement + + Returns + ------- + + """ + + def __new__(cls, value, error=MISSING, units=MISSING): + if units is MISSING: + try: + value, units = value.magnitude, value.units + except AttributeError: + # if called with two arguments and the first looks like a ufloat + # then assume the second argument is the units, keep value intact + if hasattr(value, "nominal_value"): + units = error + error = MISSING # used for check below + else: + units = "" + if error is MISSING: + # We've already extracted the units from the Quantity above + mag = value + else: + try: + error = error.to(units).magnitude + except AttributeError: + pass + if error < 0: + raise ValueError("The magnitude of the error cannot be negative") + else: + mag = ufloat(value, error) + + inst = super().__new__(cls, mag, units) + return inst + + @property + def value(self): + return self._REGISTRY.Quantity(self.magnitude.nominal_value, self.units) + + @property + def error(self): + return self._REGISTRY.Quantity(self.magnitude.std_dev, self.units) + + @property + def rel(self): + return abs(self.magnitude.std_dev / self.magnitude.nominal_value) + + def __reduce__(self): + # See notes in Quantity.__reduce__ + from datasette.vendored.pint import _unpickle_measurement + + return _unpickle_measurement, (Measurement, self.magnitude, self._units) + + def __repr__(self): + return "".format( + self.magnitude.nominal_value, self.magnitude.std_dev, self.units + ) + + def __str__(self): + return f"{self}" + + def __format__(self, spec): + spec = spec or self._REGISTRY.default_format + return self._REGISTRY.formatter.format_measurement(self, spec) + + def old_format(self, spec): + # TODO: provisional + from ...formatting import _FORMATS, extract_custom_flags, siunitx_format_unit + + # special cases + if "Lx" in spec: # the LaTeX siunitx code + # the uncertainties module supports formatting + # numbers in value(unc) notation (i.e. 1.23(45) instead of 1.23 +/- 0.45), + # using type code 'S', which siunitx actually accepts as input. + # However, the implementation is incompatible with siunitx. + # Uncertainties will do 9.1(1.1), which is invalid, should be 9.1(11). + # TODO: add support for extracting options + # + # Get rid of this code, we'll deal with it here + spec = spec.replace("Lx", "") + # The most compatible format from uncertainties is the default format, + # but even this requires fixups. + # For one, SIUnitx does not except some formats that unc does, like 'P', + # and 'S' is broken as stated, so... + spec = spec.replace("S", "").replace("P", "") + # get SIunitx options + # TODO: allow user to set this value, somehow + opts = _FORMATS["Lx"]["siopts"] + if opts != "": + opts = r"[" + opts + r"]" + # SI requires space between "+-" (or "\pm") and the nominal value + # and uncertainty, and doesn't accept "+/-", so this setting + # selects the desired replacement. + pm_fmt = _FORMATS["Lx"]["pm_fmt"] + mstr = format(self.magnitude, spec).replace(r"+/-", pm_fmt) + # Also, SIunitx doesn't accept parentheses, which uncs uses with + # scientific notation ('e' or 'E' and sometimes 'g' or 'G'). + mstr = mstr.replace("(", "").replace(")", " ") + ustr = siunitx_format_unit(self.units._units.items(), self._REGISTRY) + return rf"\SI{opts}{{{mstr}}}{{{ustr}}}" + + # standard cases + if "L" in spec: + newpm = pm = r" \pm " + pars = _FORMATS["L"]["parentheses_fmt"] + elif "P" in spec: + newpm = pm = "±" + pars = _FORMATS["P"]["parentheses_fmt"] + else: + newpm = pm = "+/-" + pars = _FORMATS[""]["parentheses_fmt"] + + if "C" in spec: + sp = "" + newspec = spec.replace("C", "") + pars = _FORMATS["C"]["parentheses_fmt"] + else: + sp = " " + newspec = spec + + if "H" in spec: + newpm = "±" + newspec = spec.replace("H", "") + pars = _FORMATS["H"]["parentheses_fmt"] + + mag = format(self.magnitude, newspec).replace(pm, sp + newpm + sp) + if "(" in mag: + # Exponential format has its own parentheses + pars = "{}" + + if "L" in newspec and "S" in newspec: + mag = mag.replace("(", r"\left(").replace(")", r"\right)") + + if "L" in newspec: + space = r"\ " + else: + space = " " + + uspec = extract_custom_flags(spec) + ustr = format(self.units, uspec) + if not ("uS" in newspec or "ue" in newspec or "u%" in newspec): + mag = pars.format(mag) + + if "H" in spec: + # Fix exponential format + mag = re.sub(r"\)e\+0?(\d+)", r")×10\1", mag) + mag = re.sub(r"\)e-0?(\d+)", r")×10-\1", mag) + + return mag + space + ustr diff --git a/datasette/vendored/pint/facets/measurement/registry.py b/datasette/vendored/pint/facets/measurement/registry.py new file mode 100644 index 0000000000..63d0eb8608 --- /dev/null +++ b/datasette/vendored/pint/facets/measurement/registry.py @@ -0,0 +1,46 @@ +""" + pint.facets.measurement.registry + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from typing import Any, Generic + +from ...compat import TypeAlias, ufloat +from ...util import create_class_with_registry +from ..plain import GenericPlainRegistry, QuantityT, UnitT +from . import objects + + +class GenericMeasurementRegistry( + Generic[QuantityT, UnitT], GenericPlainRegistry[QuantityT, UnitT] +): + Measurement = objects.Measurement + + def _init_dynamic_classes(self) -> None: + """Generate subclasses on the fly and attach them to self""" + super()._init_dynamic_classes() + + if ufloat is not None: + self.Measurement = create_class_with_registry(self, self.Measurement) + else: + + def no_uncertainties(*args, **kwargs): + raise RuntimeError( + "Pint requires the 'uncertainties' package to create a Measurement object." + ) + + self.Measurement = no_uncertainties + + +class MeasurementRegistry( + GenericMeasurementRegistry[ + objects.MeasurementQuantity[Any], objects.MeasurementUnit + ] +): + Quantity: TypeAlias = objects.MeasurementQuantity[Any] + Unit: TypeAlias = objects.MeasurementUnit diff --git a/datasette/vendored/pint/facets/nonmultiplicative/__init__.py b/datasette/vendored/pint/facets/nonmultiplicative/__init__.py new file mode 100644 index 0000000000..a338dc34a8 --- /dev/null +++ b/datasette/vendored/pint/facets/nonmultiplicative/__init__.py @@ -0,0 +1,20 @@ +""" + pint.facets.nonmultiplicative + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Adds pint the capability to handle nonmultiplicative units: + - offset + - logarithmic + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +# This import register LogarithmicConverter and OffsetConverter to be usable +# (via subclassing) +from .definitions import LogarithmicConverter, OffsetConverter # noqa: F401 +from .registry import GenericNonMultiplicativeRegistry, NonMultiplicativeRegistry + +__all__ = ["NonMultiplicativeRegistry", "GenericNonMultiplicativeRegistry"] diff --git a/datasette/vendored/pint/facets/nonmultiplicative/definitions.py b/datasette/vendored/pint/facets/nonmultiplicative/definitions.py new file mode 100644 index 0000000000..f795cf0464 --- /dev/null +++ b/datasette/vendored/pint/facets/nonmultiplicative/definitions.py @@ -0,0 +1,117 @@ +""" + pint.facets.nonmultiplicative.definitions + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :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 ..._typing import Magnitude +from ...compat import HAS_NUMPY, exp, log +from ..plain import ScaleConverter + + +@dataclass(frozen=True) +class OffsetConverter(ScaleConverter): + """An affine transformation.""" + + offset: float + + @property + def is_multiplicative(self): + return self.offset == 0 + + def to_reference(self, value: Magnitude, inplace: bool = False) -> Magnitude: + if inplace: + value *= self.scale + value += self.offset + else: + value = value * self.scale + self.offset + + return value + + def from_reference(self, value: Magnitude, inplace: bool = False) -> Magnitude: + if inplace: + value -= self.offset + value /= self.scale + else: + value = (value - self.offset) / self.scale + + return value + + @classmethod + def preprocess_kwargs(cls, **kwargs): + if "offset" in kwargs and kwargs["offset"] == 0: + return {"scale": kwargs["scale"]} + return None + + +@dataclass(frozen=True) +class LogarithmicConverter(ScaleConverter): + """Converts between linear units and logarithmic units, such as dB, octave, neper or pH. + Q_log = logfactor * log( Q_lin / scale ) / log(log_base) + + Parameters + ---------- + scale : float + unit of reference at denominator for logarithmic unit conversion + logbase : float + plain of logarithm used in the logarithmic unit conversion + logfactor : float + factor multiplied to logarithm for unit conversion + inplace : bool + controls if computation is done in place + """ + + # TODO: Can I use PintScalar here? + logbase: float + logfactor: float + + @property + def is_multiplicative(self): + return False + + @property + def is_logarithmic(self): + return True + + def from_reference(self, value: Magnitude, inplace: bool = False) -> Magnitude: + """Converts value from the reference unit to the logarithmic unit + + dBm <------ mW + y dBm = 10 log10( x / 1mW ) + """ + if inplace: + value /= self.scale + if HAS_NUMPY: + log(value, value) + else: + value = log(value) + value *= self.logfactor / log(self.logbase) + else: + value = self.logfactor * log(value / self.scale) / log(self.logbase) + + return value + + def to_reference(self, value: Magnitude, inplace: bool = False) -> Magnitude: + """Converts value to the reference unit from the logarithmic unit + + dBm ------> mW + y dBm = 10 log10( x / 1mW ) + """ + if inplace: + value /= self.logfactor + value *= log(self.logbase) + if HAS_NUMPY: + exp(value, value) + else: + value = exp(value) + value *= self.scale + else: + value = self.scale * exp(log(self.logbase) * (value / self.logfactor)) + + return value diff --git a/datasette/vendored/pint/facets/nonmultiplicative/objects.py b/datasette/vendored/pint/facets/nonmultiplicative/objects.py new file mode 100644 index 0000000000..114a256af5 --- /dev/null +++ b/datasette/vendored/pint/facets/nonmultiplicative/objects.py @@ -0,0 +1,67 @@ +""" + pint.facets.nonmultiplicative.objects + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from typing import Generic + +from ..plain import MagnitudeT, PlainQuantity, PlainUnit + + +class NonMultiplicativeQuantity(Generic[MagnitudeT], PlainQuantity[MagnitudeT]): + @property + def _is_multiplicative(self) -> bool: + """Check if the PlainQuantity object has only multiplicative units.""" + return not self._get_non_multiplicative_units() + + def _get_non_multiplicative_units(self) -> list[str]: + """Return a list of the of non-multiplicative units of the PlainQuantity object.""" + return [ + unit + for unit in self._units + if not self._get_unit_definition(unit).is_multiplicative + ] + + def _get_delta_units(self) -> list[str]: + """Return list of delta units ot the PlainQuantity object.""" + return [u for u in self._units if u.startswith("delta_")] + + def _has_compatible_delta(self, unit: str) -> bool: + """ "Check if PlainQuantity object has a delta_unit that is compatible with unit""" + deltas = self._get_delta_units() + if "delta_" + unit in deltas: + return True + # Look for delta units with same dimension as the offset unit + offset_unit_dim = self._get_unit_definition(unit).reference + return any( + self._get_unit_definition(d).reference == offset_unit_dim for d in deltas + ) + + def _ok_for_muldiv(self, no_offset_units: int | None = None) -> bool: + """Checks if PlainQuantity object can be multiplied or divided""" + + is_ok = True + if no_offset_units is None: + no_offset_units = len(self._get_non_multiplicative_units()) + if no_offset_units > 1: + is_ok = False + if no_offset_units == 1: + if len(self._units) > 1: + is_ok = False + if ( + len(self._units) == 1 + and not self._REGISTRY.autoconvert_offset_to_baseunit + ): + is_ok = False + if next(iter(self._units.values())) != 1: + is_ok = False + return is_ok + + +class NonMultiplicativeUnit(PlainUnit): + pass diff --git a/datasette/vendored/pint/facets/nonmultiplicative/registry.py b/datasette/vendored/pint/facets/nonmultiplicative/registry.py new file mode 100644 index 0000000000..7f58d060cb --- /dev/null +++ b/datasette/vendored/pint/facets/nonmultiplicative/registry.py @@ -0,0 +1,304 @@ +""" + pint.facets.nonmultiplicative.registry + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from typing import Any, Generic, TypeVar + +from ...compat import TypeAlias +from ...errors import DimensionalityError, UndefinedUnitError +from ...util import UnitsContainer, logger +from ..plain import GenericPlainRegistry, QuantityT, UnitDefinition, UnitT +from . import objects +from .definitions import OffsetConverter, ScaleConverter + +T = TypeVar("T") + + +class GenericNonMultiplicativeRegistry( + Generic[QuantityT, UnitT], GenericPlainRegistry[QuantityT, UnitT] +): + """Handle of non multiplicative units (e.g. Temperature). + + Capabilities: + - Register non-multiplicative units and their relations. + - Convert between non-multiplicative units. + + Parameters + ---------- + default_as_delta : bool + If True, non-multiplicative units are interpreted as + their *delta* counterparts in multiplications. + autoconvert_offset_to_baseunit : bool + If True, non-multiplicative units are + converted to plain units in multiplications. + + """ + + def __init__( + self, + default_as_delta: bool = True, + autoconvert_offset_to_baseunit: bool = False, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + + #: When performing a multiplication of units, interpret + #: non-multiplicative units as their *delta* counterparts. + self.default_as_delta = default_as_delta + + # Determines if quantities with offset units are converted to their + # plain units on multiplication and division. + self.autoconvert_offset_to_baseunit = autoconvert_offset_to_baseunit + + def parse_units_as_container( + self, + input_string: str, + as_delta: bool | None = None, + case_sensitive: bool | None = None, + ) -> UnitsContainer: + """ """ + if as_delta is None: + as_delta = self.default_as_delta + + return super().parse_units_as_container(input_string, as_delta, case_sensitive) + + def _add_unit(self, definition: UnitDefinition) -> None: + super()._add_unit(definition) + + if definition.is_multiplicative: + return + + if definition.is_logarithmic: + return + + 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, unit_name: str) -> bool: + """True if the unit is multiplicative. + + Parameters + ---------- + unit_name + Name of the unit to check. + Can be prefixed, pluralized or even an alias + + Raises + ------ + UndefinedUnitError + If the unit is not in the registry. + """ + if unit_name in self._units: + return self._units[unit_name].is_multiplicative + + # If the unit is not in the registry might be because it is not + # registered with its prefixed version. + # TODO: Might be better to register them. + names = self.parse_unit_name(unit_name) + assert len(names) == 1 + _, base_name, _ = names[0] + try: + return self._units[base_name].is_multiplicative + except KeyError: + raise UndefinedUnitError(unit_name) + + def _validate_and_extract(self, units: UnitsContainer) -> str | None: + """Used to check if a given units is suitable for a simple + conversion. + + Return None if all units are non-multiplicative + Return the unit name if a single non-multiplicative unit is found + and is raised to a power equals to 1. + + Otherwise, raise an Exception. + + Parameters + ---------- + units + Compound dictionary. + + Raises + ------ + ValueError + If the more than a single non-multiplicative unit is present, + or a single one is present but raised to a power different from 1. + + """ + + # TODO: document what happens if autoconvert_offset_to_baseunit + # TODO: Clarify docs + + # u is for unit, e is for exponent + nonmult_units = [ + (u, e) for u, e in units.items() if not self._is_multiplicative(u) + ] + + # Let's validate source offset units + if len(nonmult_units) > 1: + # More than one src offset unit is not allowed + raise ValueError("more than one offset unit.") + + elif len(nonmult_units) == 1: + # A single src offset unit is present. Extract it + # But check that: + # - the exponent is 1 + # - is not used in multiplicative context + nonmult_unit, exponent = nonmult_units.pop() + + if exponent != 1: + raise ValueError("offset units in higher order.") + + if len(units) > 1 and not self.autoconvert_offset_to_baseunit: + raise ValueError("offset unit used in multiplicative context.") + + return nonmult_unit + + return None + + def _add_ref_of_log_or_offset_unit( + self, offset_unit: str, all_units: UnitsContainer + ) -> UnitsContainer: + slct_unit = self._units[offset_unit] + if slct_unit.is_logarithmic: + # Extract reference unit + slct_ref = slct_unit.reference + + # TODO: Check that reference is None + + # If reference unit is not dimensionless + if slct_ref != UnitsContainer(): + # Extract reference unit + (u, e) = [(u, e) for u, e in slct_ref.items()].pop() + # Add it back to the unit list + return all_units.add(u, e) + + if not slct_unit.is_multiplicative: # is offset unit + # Extract reference unit + return slct_unit.reference + + # Otherwise, return the units unmodified + return all_units + + def _convert( + self, value: T, src: UnitsContainer, dst: UnitsContainer, inplace: bool = False + ) -> T: + """Convert value from some source to destination units. + + In addition to what is done by the PlainRegistry, + converts between non-multiplicative units. + + Parameters + ---------- + value : + value + src : UnitsContainer + source units. + dst : UnitsContainer + destination units. + inplace : + (Default value = False) + + Returns + ------- + type + converted value + + """ + + # Conversion needs to consider if non-multiplicative (AKA offset + # units) are involved. Conversion is only possible if src and dst + # have at most one offset unit per dimension. Other rules are applied + # by validate and extract. + try: + src_offset_unit = self._validate_and_extract(src) + except ValueError as ex: + raise DimensionalityError(src, dst, extra_msg=f" - In source units, {ex}") + + try: + dst_offset_unit = self._validate_and_extract(dst) + except ValueError as ex: + raise DimensionalityError( + src, dst, extra_msg=f" - In destination units, {ex}" + ) + + # convert if no offset units are present + if not (src_offset_unit or dst_offset_unit): + return super()._convert(value, src, dst, inplace) + + src_dim = self._get_dimensionality(src) + dst_dim = self._get_dimensionality(dst) + + # If the source and destination dimensionality are different, + # then the conversion cannot be performed. + if src_dim != dst_dim: + raise DimensionalityError(src, dst, src_dim, dst_dim) + + # clean src from offset units by converting to reference + if src_offset_unit: + if any(u.startswith("delta_") for u in dst): + raise DimensionalityError(src, dst) + value = self._units[src_offset_unit].converter.to_reference(value, inplace) + src = src.remove([src_offset_unit]) + # Add reference unit for multiplicative section + src = self._add_ref_of_log_or_offset_unit(src_offset_unit, src) + + # clean dst units from offset units + if dst_offset_unit: + if any(u.startswith("delta_") for u in src): + raise DimensionalityError(src, dst) + dst = dst.remove([dst_offset_unit]) + # Add reference unit for multiplicative section + dst = self._add_ref_of_log_or_offset_unit(dst_offset_unit, dst) + + # Convert non multiplicative units to the dst. + value = super()._convert(value, src, dst, inplace, False) + + # Finally convert to offset units specified in destination + if dst_offset_unit: + value = self._units[dst_offset_unit].converter.from_reference( + value, inplace + ) + + return value + + +class NonMultiplicativeRegistry( + GenericNonMultiplicativeRegistry[ + objects.NonMultiplicativeQuantity[Any], objects.NonMultiplicativeUnit + ] +): + Quantity: TypeAlias = objects.NonMultiplicativeQuantity[Any] + Unit: TypeAlias = objects.NonMultiplicativeUnit diff --git a/datasette/vendored/pint/facets/numpy/__init__.py b/datasette/vendored/pint/facets/numpy/__init__.py new file mode 100644 index 0000000000..477c09579d --- /dev/null +++ b/datasette/vendored/pint/facets/numpy/__init__.py @@ -0,0 +1,15 @@ +""" + pint.facets.numpy + ~~~~~~~~~~~~~~~~~ + + Adds pint the capability to interoperate with NumPy + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from .registry import GenericNumpyRegistry, NumpyRegistry + +__all__ = ["NumpyRegistry", "GenericNumpyRegistry"] diff --git a/datasette/vendored/pint/facets/numpy/numpy_func.py b/datasette/vendored/pint/facets/numpy/numpy_func.py new file mode 100644 index 0000000000..066039738c --- /dev/null +++ b/datasette/vendored/pint/facets/numpy/numpy_func.py @@ -0,0 +1,1073 @@ +""" + pint.facets.numpy.numpy_func + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import warnings +from inspect import signature +from itertools import chain + +from ...compat import is_upcast_type, np, zero_or_nan +from ...errors import DimensionalityError, OffsetUnitCalculusError, UnitStrippedWarning +from ...util import iterable, sized + +HANDLED_UFUNCS = {} +HANDLED_FUNCTIONS = {} + + +# Shared Implementation Utilities + + +def _is_quantity(obj): + """Test for _units and _magnitude attrs. + + This is done in place of isinstance(Quantity, arg), which would cause a circular import. + + Parameters + ---------- + obj : Object + + + Returns + ------- + bool + """ + return hasattr(obj, "_units") and hasattr(obj, "_magnitude") + + +def _is_sequence_with_quantity_elements(obj): + """Test for sequences of quantities. + + Parameters + ---------- + obj : object + + + Returns + ------- + True if obj is a sequence and at least one element is a Quantity; False otherwise + """ + if np is not None and isinstance(obj, np.ndarray) and not obj.dtype.hasobject: + # If obj is a numpy array, avoid looping on all elements + # if dtype does not have objects + return False + return ( + iterable(obj) + and sized(obj) + and not isinstance(obj, str) + and any(_is_quantity(item) for item in obj) + ) + + +def _get_first_input_units(args, kwargs=None): + """Obtain the first valid unit from a collection of args and kwargs.""" + kwargs = kwargs or {} + for arg in chain(args, kwargs.values()): + if _is_quantity(arg): + return arg.units + elif _is_sequence_with_quantity_elements(arg): + return next(arg_i.units for arg_i in arg if _is_quantity(arg_i)) + raise TypeError("Expected at least one Quantity; found none") + + +def convert_arg(arg, pre_calc_units): + """Convert quantities and sequences of quantities to pre_calc_units and strip 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) + elif _is_sequence_with_quantity_elements(arg): + return [convert_arg(item, pre_calc_units) for item in arg] + elif arg is not None: + if pre_calc_units.dimensionless: + return pre_calc_units._REGISTRY.Quantity(arg).m_as(pre_calc_units) + elif not _is_quantity(arg) and zero_or_nan(arg, True): + return arg + else: + raise DimensionalityError("dimensionless", pre_calc_units) + elif _is_quantity(arg): + return arg.m + elif _is_sequence_with_quantity_elements(arg): + return [convert_arg(item, pre_calc_units) for item in arg] + return arg + + +def convert_to_consistent_units(*args, pre_calc_units=None, **kwargs): + """Prepare args and kwargs for wrapping by unit conversion and stripping. + + 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 (except booleans) are + treated as dimensionless Quantities. If pre_calc_units is None, units are simply + stripped. + """ + return ( + tuple(convert_arg(arg, pre_calc_units=pre_calc_units) for arg in args), + { + key: convert_arg(arg, pre_calc_units=pre_calc_units) + for key, arg in kwargs.items() + }, + ) + + +def unwrap_and_wrap_consistent_units(*args): + """Strip units from args while providing a rewrapping function. + + Returns the given args as parsed by convert_to_consistent_units assuming units of + first arg with units, along with a wrapper to restore that unit to the output. + + """ + if all(not _is_quantity(arg) for arg in args): + return args, lambda x: x + + first_input_units = _get_first_input_units(args) + args, _ = convert_to_consistent_units(*args, pre_calc_units=first_input_units) + return ( + args, + lambda value: first_input_units._REGISTRY.Quantity(value, first_input_units), + ) + + +def get_op_output_unit(unit_op, first_input_units, all_args=None, size=None): + """Determine resulting unit from given operation. + + Options for `unit_op`: + + - "sum": `first_input_units`, unless non-multiplicative, which raises + OffsetUnitCalculusError + - "mul": product of all units in `all_args` + - "delta": `first_input_units`, unless non-multiplicative, which uses delta version + - "delta,div": like "delta", but divided by all units in `all_args` except the first + - "div": unit of first argument in `all_args` (or dimensionless if not a Quantity) divided + by all following units + - "variance": square of `first_input_units`, unless non-multiplicative, which raises + OffsetUnitCalculusError + - "square": square of `first_input_units` + - "sqrt": square root of `first_input_units` + - "reciprocal": reciprocal of `first_input_units` + - "size": `first_input_units` raised to the power of `size` + - "invdiv": inverse of `div`, product of all following units divided by first argument unit + + Parameters + ---------- + unit_op : + + first_input_units : + + all_args : + (Default value = None) + size : + (Default value = None) + + Returns + ------- + + """ + all_args = all_args or [] + + if unit_op == "sum": + result_unit = (1 * first_input_units + 1 * first_input_units).units + elif unit_op == "mul": + product = first_input_units._REGISTRY.parse_units("") + for x in all_args: + if hasattr(x, "units"): + product *= x.units + result_unit = product + elif unit_op == "delta": + result_unit = (1 * first_input_units - 1 * first_input_units).units + elif unit_op == "delta,div": + product = (1 * first_input_units - 1 * first_input_units).units + for x in all_args[1:]: + if hasattr(x, "units"): + product /= x.units + result_unit = product + elif unit_op == "div": + # Start with first arg in numerator, all others in denominator + product = getattr( + all_args[0], "units", first_input_units._REGISTRY.parse_units("") + ) + for x in all_args[1:]: + if hasattr(x, "units"): + product /= x.units + result_unit = product + elif unit_op == "variance": + result_unit = ((1 * first_input_units + 1 * first_input_units) ** 2).units + elif unit_op == "square": + result_unit = first_input_units**2 + elif unit_op == "sqrt": + result_unit = first_input_units**0.5 + elif unit_op == "cbrt": + result_unit = first_input_units ** (1 / 3) + elif unit_op == "reciprocal": + result_unit = first_input_units**-1 + elif unit_op == "size": + if size is None: + raise ValueError('size argument must be given when unit_op=="size"') + result_unit = first_input_units**size + elif unit_op == "invdiv": + # Start with first arg in numerator, all others in denominator + product = getattr( + all_args[0], "units", first_input_units._REGISTRY.parse_units("") + ) + for x in all_args[1:]: + if hasattr(x, "units"): + product /= x.units + result_unit = product**-1 + else: + raise ValueError(f"Output unit method {unit_op} not understood") + + return result_unit + + +def implements(numpy_func_string, func_type): + """Register an __array_function__/__array_ufunc__ implementation for Quantity + objects. + + """ + + def decorator(func): + if func_type == "function": + HANDLED_FUNCTIONS[numpy_func_string] = func + elif func_type == "ufunc": + HANDLED_UFUNCS[numpy_func_string] = func + else: + raise ValueError(f"Invalid func_type {func_type}") + return func + + return decorator + + +def implement_func(func_type, func_str, input_units=None, output_unit=None): + """Add default-behavior NumPy function/ufunc to the handled list. + + Parameters + ---------- + func_type : str + "function" for NumPy functions, "ufunc" for NumPy ufuncs + func_str : str + String representing the name of the NumPy function/ufunc to add + input_units : pint.Unit or str or None + Parameter to control how the function downcasts to magnitudes of arguments. If + `pint.Unit`, converts all args and kwargs to this unit before downcasting to + magnitude. If "all_consistent", converts all args and kwargs to the unit of the + first Quantity in args and kwargs before downcasting to magnitude. If some + other string, the string is parsed as a unit, and all args and kwargs are + converted to that unit. If None, units are stripped without conversion. + output_unit : pint.Unit or str or None + Parameter to control the unit of the output. If `pint.Unit`, output is wrapped + with that unit. If "match_input", output is wrapped with the unit of the first + Quantity in args and kwargs. If a string representing a unit operation defined + in `get_op_output_unit`, output is wrapped by the unit determined by + `get_op_output_unit`. If some other string, the string is parsed as a unit, + which becomes the unit of the output. If None, the bare magnitude is returned. + + + """ + # If NumPy is not available, do not attempt implement that which does not exist + if np is None: + return + + # Handle functions in submodules + func_str_split = func_str.split(".") + func = getattr(np, func_str_split[0], None) + # If the function is not available, do not attempt to implement it + if func is None: + return + for func_str_piece in func_str_split[1:]: + func = getattr(func, func_str_piece) + + @implements(func_str, func_type) + def implementation(*args, **kwargs): + if func_str in ["multiply", "true_divide", "divide", "floor_divide"] and any( + [ + not _is_quantity(arg) and _is_sequence_with_quantity_elements(arg) + for arg in args + ] + ): + # the sequence may contain different units, so fall back to element-wise + return np.array( + [func(*func_args) for func_args in zip(*args)], dtype=object + ) + + first_input_units = _get_first_input_units(args, kwargs) + if input_units == "all_consistent": + # Match all input args/kwargs to same units + stripped_args, stripped_kwargs = convert_to_consistent_units( + *args, pre_calc_units=first_input_units, **kwargs + ) + else: + if isinstance(input_units, str): + # Conversion requires Unit, not str + pre_calc_units = first_input_units._REGISTRY.parse_units(input_units) + else: + pre_calc_units = input_units + + # Match all input args/kwargs to input_units, or if input_units is None, + # simply strip units + stripped_args, stripped_kwargs = convert_to_consistent_units( + *args, pre_calc_units=pre_calc_units, **kwargs + ) + + # Determine result through plain numpy function on stripped arguments + result_magnitude = func(*stripped_args, **stripped_kwargs) + + if output_unit is None: + # Short circuit and return magnitude alone + return result_magnitude + elif output_unit == "match_input": + result_unit = first_input_units + elif output_unit in ( + "sum", + "mul", + "delta", + "delta,div", + "div", + "invdiv", + "variance", + "square", + "sqrt", + "cbrt", + "reciprocal", + "size", + ): + result_unit = get_op_output_unit( + output_unit, first_input_units, tuple(chain(args, kwargs.values())) + ) + else: + result_unit = output_unit + + return first_input_units._REGISTRY.Quantity(result_magnitude, result_unit) + + +""" +Define ufunc behavior collections. + +- `strip_unit_input_output_ufuncs`: units should be ignored on both input and output +- `matching_input_bare_output_ufuncs`: inputs are converted to matching units, but + outputs are returned as-is +- `matching_input_set_units_output_ufuncs`: inputs are converted to matching units, and + the output units are as set by the dict value +- `set_units_ufuncs`: dict values are specified as (in_unit, out_unit), so that inputs + are converted to in_unit before having magnitude passed to NumPy ufunc, and outputs + are set to have out_unit +- `matching_input_copy_units_output_ufuncs`: inputs are converted to matching units, and + outputs are set to that unit +- `copy_units_output_ufuncs`: input units (except the first) are ignored, and output is + set to that of the first input unit +- `op_units_output_ufuncs`: determine output unit from input unit as determined by + operation (see `get_op_output_unit`) +""" +strip_unit_input_output_ufuncs = ["isnan", "isinf", "isfinite", "signbit", "sign"] +matching_input_bare_output_ufuncs = [ + "equal", + "greater", + "greater_equal", + "less", + "less_equal", + "not_equal", +] +matching_input_set_units_output_ufuncs = {"arctan2": "radian"} +set_units_ufuncs = { + "cumprod": ("", ""), + "arccos": ("", "radian"), + "arcsin": ("", "radian"), + "arctan": ("", "radian"), + "arccosh": ("", "radian"), + "arcsinh": ("", "radian"), + "arctanh": ("", "radian"), + "exp": ("", ""), + "expm1": ("", ""), + "exp2": ("", ""), + "log": ("", ""), + "log10": ("", ""), + "log1p": ("", ""), + "log2": ("", ""), + "sin": ("radian", ""), + "cos": ("radian", ""), + "tan": ("radian", ""), + "sinh": ("radian", ""), + "cosh": ("radian", ""), + "tanh": ("radian", ""), + "radians": ("degree", "radian"), + "degrees": ("radian", "degree"), + "deg2rad": ("degree", "radian"), + "rad2deg": ("radian", "degree"), + "logaddexp": ("", ""), + "logaddexp2": ("", ""), +} +# TODO (#905 follow-up): +# while this matches previous behavior, some of these have optional arguments that +# should not be Quantities. This should be fixed, and tests using these optional +# arguments should be added. +matching_input_copy_units_output_ufuncs = [ + "compress", + "conj", + "conjugate", + "copy", + "diagonal", + "max", + "mean", + "min", + "ptp", + "ravel", + "repeat", + "reshape", + "round", + "squeeze", + "swapaxes", + "take", + "trace", + "transpose", + "roll", + "ceil", + "floor", + "hypot", + "rint", + "copysign", + "nextafter", + "trunc", + "absolute", + "positive", + "negative", + "maximum", + "minimum", + "fabs", +] +copy_units_output_ufuncs = ["ldexp", "fmod", "mod", "remainder"] +op_units_output_ufuncs = { + "var": "square", + "multiply": "mul", + "true_divide": "div", + "divide": "div", + "floor_divide": "div", + "sqrt": "sqrt", + "cbrt": "cbrt", + "square": "square", + "reciprocal": "reciprocal", + "std": "sum", + "sum": "sum", + "cumsum": "sum", + "matmul": "mul", +} + + +# Perform the standard ufunc implementations based on behavior collections + +for ufunc_str in strip_unit_input_output_ufuncs: + # Ignore units + implement_func("ufunc", ufunc_str, input_units=None, output_unit=None) + +for ufunc_str in matching_input_bare_output_ufuncs: + # Require all inputs to match units, but output plain ndarray/duck array + implement_func("ufunc", ufunc_str, input_units="all_consistent", output_unit=None) + +for ufunc_str, out_unit in matching_input_set_units_output_ufuncs.items(): + # Require all inputs to match units, but output in specified unit + implement_func( + "ufunc", ufunc_str, input_units="all_consistent", output_unit=out_unit + ) + +for ufunc_str, (in_unit, out_unit) in set_units_ufuncs.items(): + # Require inputs in specified unit, and output in specified unit + implement_func("ufunc", ufunc_str, input_units=in_unit, output_unit=out_unit) + +for ufunc_str in matching_input_copy_units_output_ufuncs: + # Require all inputs to match units, and output as first unit in arguments + implement_func( + "ufunc", ufunc_str, input_units="all_consistent", output_unit="match_input" + ) + +for ufunc_str in copy_units_output_ufuncs: + # Output as first unit in arguments, but do not convert inputs + implement_func("ufunc", ufunc_str, input_units=None, output_unit="match_input") + +for ufunc_str, unit_op in op_units_output_ufuncs.items(): + implement_func("ufunc", ufunc_str, input_units=None, output_unit=unit_op) + + +# Define custom ufunc implementations for atypical cases + + +@implements("modf", "ufunc") +def _modf(x, *args, **kwargs): + (x,), output_wrap = unwrap_and_wrap_consistent_units(x) + return tuple(output_wrap(y) for y in np.modf(x, *args, **kwargs)) + + +@implements("frexp", "ufunc") +def _frexp(x, *args, **kwargs): + (x,), output_wrap = unwrap_and_wrap_consistent_units(x) + mantissa, exponent = np.frexp(x, *args, **kwargs) + return output_wrap(mantissa), exponent + + +@implements("power", "ufunc") +def _power(x1, x2): + if _is_quantity(x1): + return x1**x2 + + return x2.__rpow__(x1) + + +@implements("add", "ufunc") +def _add(x1, x2, *args, **kwargs): + (x1, x2), output_wrap = unwrap_and_wrap_consistent_units(x1, x2) + return output_wrap(np.add(x1, x2, *args, **kwargs)) + + +@implements("subtract", "ufunc") +def _subtract(x1, x2, *args, **kwargs): + (x1, x2), output_wrap = unwrap_and_wrap_consistent_units(x1, x2) + return output_wrap(np.subtract(x1, x2, *args, **kwargs)) + + +# Define custom function implementations + + +@implements("meshgrid", "function") +def _meshgrid(*xi, **kwargs): + # Simply need to map input units to onto list of outputs + input_units = (x.units for x in xi) + res = np.meshgrid(*(x.m for x in xi), **kwargs) + return [out * unit for out, unit in zip(res, input_units)] + + +@implements("full_like", "function") +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, **kwargs) * fill_value.m, + fill_value.units, + ) + + return np.ones_like(a, **kwargs) * fill_value + + +@implements("interp", "function") +def _interp(x, xp, fp, left=None, right=None, period=None): + # Need to handle x and y units separately + (x, xp, period), _ = unwrap_and_wrap_consistent_units(x, xp, period) + (fp, right, left), output_wrap = unwrap_and_wrap_consistent_units(fp, left, right) + return output_wrap(np.interp(x, xp, fp, left=left, right=right, period=period)) + + +@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)) + + +@implements("concatenate", "function") +def _concatenate(sequence, *args, **kwargs): + sequence, output_wrap = unwrap_and_wrap_consistent_units(*sequence) + return output_wrap(np.concatenate(sequence, *args, **kwargs)) + + +@implements("stack", "function") +def _stack(arrays, *args, **kwargs): + arrays, output_wrap = unwrap_and_wrap_consistent_units(*arrays) + return output_wrap(np.stack(arrays, *args, **kwargs)) + + +@implements("unwrap", "function") +def _unwrap(p, discont=None, axis=-1): + # np.unwrap only dispatches over p argument, so assume it is a Quantity + discont = np.pi if discont is None else discont + return p._REGISTRY.Quantity(np.unwrap(p.m_as("rad"), discont, axis=axis), "rad").to( + p.units + ) + + +@implements("copyto", "function") +def _copyto(dst, src, casting="same_kind", where=True): + if _is_quantity(dst): + if _is_quantity(src): + src = src.m_as(dst.units) + np.copyto(dst._magnitude, src, casting=casting, where=where) + else: + warnings.warn( + "The unit of the quantity is stripped when copying to non-quantity", + UnitStrippedWarning, + stacklevel=2, + ) + np.copyto(dst, src.m, casting=casting, where=where) + + +@implements("einsum", "function") +def _einsum(subscripts, *operands, **kwargs): + operand_magnitudes, _ = convert_to_consistent_units(*operands, pre_calc_units=None) + output_unit = get_op_output_unit("mul", _get_first_input_units(operands), operands) + return np.einsum(subscripts, *operand_magnitudes, **kwargs) * output_unit + + +@implements("isin", "function") +def _isin(element, test_elements, assume_unique=False, invert=False): + if not _is_quantity(element): + raise ValueError( + "Cannot test if unit-aware elements are in not-unit-aware array" + ) + + if _is_quantity(test_elements): + try: + test_elements = test_elements.m_as(element.units) + except DimensionalityError: + # Incompatible unit test elements cannot be in element + return np.full(element.shape, False) + elif _is_sequence_with_quantity_elements(test_elements): + compatible_test_elements = [] + for test_element in test_elements: + if not _is_quantity(test_element): + pass + try: + compatible_test_elements.append(test_element.m_as(element.units)) + except DimensionalityError: + # Incompatible unit test elements cannot be in element, but others in + # sequence may + pass + test_elements = compatible_test_elements + else: + # Consider non-quantity like dimensionless quantity + if not element.dimensionless: + # Unit do not match, so all false + return np.full(element.shape, False) + else: + # Convert to units of element + element._REGISTRY.Quantity(test_elements).m_as(element.units) + + return np.isin(element.m, test_elements, assume_unique=assume_unique, invert=invert) + + +@implements("pad", "function") +def _pad(array, pad_width, mode="constant", **kwargs): + def _recursive_convert(arg, unit): + if iterable(arg): + return tuple(_recursive_convert(a, unit=unit) for a in arg) + elif not _is_quantity(arg): + if arg == 0 or np.isnan(arg): + arg = unit._REGISTRY.Quantity(arg, unit) + else: + arg = unit._REGISTRY.Quantity(arg, "dimensionless") + + return arg.m_as(unit) + + # pad only dispatches on array argument, so we know it is a Quantity + units = array.units + + # Handle flexible constant_values and end_values, converting to units if Quantity + # and ignoring if not + for key in ("constant_values", "end_values"): + if key in kwargs: + kwargs[key] = _recursive_convert(kwargs[key], units) + + return units._REGISTRY.Quantity( + np.pad(array._magnitude, pad_width, mode=mode, **kwargs), units + ) + + +@implements("any", "function") +def _any(a, *args, **kwargs): + # Only valid when multiplicative unit/no offset + if a._is_multiplicative: + return np.any(a._magnitude, *args, **kwargs) + + raise ValueError("Boolean value of Quantity with offset unit is ambiguous.") + + +@implements("all", "function") +def _all(a, *args, **kwargs): + # Only valid when multiplicative unit/no offset + if a._is_multiplicative: + return np.all(a._magnitude, *args, **kwargs) + else: + raise ValueError("Boolean value of Quantity with offset unit is ambiguous.") + + +def implement_prod_func(name): + if np is None: + return + + func = getattr(np, name, None) + if func is None: + return + + @implements(name, "function") + def _prod(a, *args, **kwargs): + arg_names = ("axis", "dtype", "out", "keepdims", "initial", "where") + all_kwargs = dict(**dict(zip(arg_names, args)), **kwargs) + axis = all_kwargs.get("axis", None) + where = all_kwargs.get("where", None) + + registry = a.units._REGISTRY + + if axis is not None and where is not None: + _, where_ = np.broadcast_arrays(a._magnitude, where) + exponents = np.unique(np.sum(where_, axis=axis)) + if len(exponents) == 1 or (len(exponents) == 2 and 0 in exponents): + units = a.units ** np.max(exponents) + else: + units = registry.dimensionless + a = a.to(units) + elif axis is not None: + units = a.units ** a.shape[axis] + elif where is not None: + exponent = np.sum(where) + units = a.units**exponent + else: + exponent = ( + np.sum(np.logical_not(np.isnan(a))) if name == "nanprod" else a.size + ) + units = a.units**exponent + + result = func(a._magnitude, *args, **kwargs) + + return registry.Quantity(result, units) + + +for name in ("prod", "nanprod"): + implement_prod_func(name) + + +# Handle mutliplicative functions separately to deal with non-multiplicative units +def _base_unit_if_needed(a): + if a._is_multiplicative: + return a + else: + if a.units._REGISTRY.autoconvert_offset_to_baseunit: + return a.to_base_units() + else: + raise OffsetUnitCalculusError(a.units) + + +# NP2 Can remove trapz wrapping when we only support numpy>=2 +@implements("trapz", "function") +@implements("trapezoid", "function") +def _trapz(y, x=None, dx=1.0, **kwargs): + trapezoid = np.trapezoid if hasattr(np, "trapezoid") else np.trapz + y = _base_unit_if_needed(y) + units = y.units + if x is not None: + if hasattr(x, "units"): + x = _base_unit_if_needed(x) + units *= x.units + x = x._magnitude + ret = trapezoid(y._magnitude, x, **kwargs) + else: + if hasattr(dx, "units"): + dx = _base_unit_if_needed(dx) + units *= dx.units + dx = dx._magnitude + ret = trapezoid(y._magnitude, dx=dx, **kwargs) + + return y.units._REGISTRY.Quantity(ret, units) + + +@implements("correlate", "function") +def _correlate(a, v, mode="valid", **kwargs): + a = _base_unit_if_needed(a) + v = _base_unit_if_needed(v) + units = a.units * v.units + ret = np.correlate(a._magnitude, v._magnitude, mode=mode, **kwargs) + return a.units._REGISTRY.Quantity(ret, units) + + +def implement_mul_func(func): + # If NumPy is not available, do not attempt implement that which does not exist + if np is None: + return + + func = getattr(np, func_str) + + @implements(func_str, "function") + def implementation(a, b, **kwargs): + a = _base_unit_if_needed(a) + units = a.units + if hasattr(b, "units"): + b = _base_unit_if_needed(b) + units *= b.units + b = b._magnitude + + mag = func(a._magnitude, b, **kwargs) + return a.units._REGISTRY.Quantity(mag, units) + + +for func_str in ("cross", "dot"): + implement_mul_func(func_str) + + +# Implement simple matching-unit or stripped-unit functions based on signature + + +def implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output=True): + # If NumPy is not available, do not attempt implement that which does not exist + if np is None: + return + + if "." not in func_str: + func = getattr(np, func_str, None) + else: + parts = func_str.split(".") + module = np + for part in parts[:-1]: + module = getattr(module, part, None) + func = getattr(module, parts[-1], None) + + # if NumPy does not implement it, do not implement it either + if func is None: + return + + @implements(func_str, "function") + def implementation(*args, **kwargs): + # Bind given arguments to the NumPy function signature + bound_args = signature(func).bind(*args, **kwargs) + + # Skip unit arguments that are supplied as None + valid_unit_arguments = [ + label + for label in unit_arguments + if label in bound_args.arguments and bound_args.arguments[label] is not None + ] + + # Unwrap valid unit arguments, ensure consistency, and obtain output wrapper + unwrapped_unit_args, output_wrap = unwrap_and_wrap_consistent_units( + *(bound_args.arguments[label] for label in valid_unit_arguments) + ) + + # Call NumPy function with updated arguments + for i, unwrapped_unit_arg in enumerate(unwrapped_unit_args): + bound_args.arguments[valid_unit_arguments[i]] = unwrapped_unit_arg + ret = func(*bound_args.args, **bound_args.kwargs) + + # Conditionally wrap output + if wrap_output: + return output_wrap(ret) + + return ret + + +for func_str, unit_arguments, wrap_output in ( + ("expand_dims", "a", True), + ("squeeze", "a", True), + ("rollaxis", "a", True), + ("moveaxis", "a", True), + ("around", "a", True), + ("diagonal", "a", True), + ("mean", "a", True), + ("ptp", "a", True), + ("ravel", "a", True), + ("round_", "a", True), + ("round", "a", True), + ("sort", "a", True), + ("median", "a", True), + ("nanmedian", "a", True), + ("transpose", "a", True), + ("roll", "a", True), + ("copy", "a", True), + ("average", "a", True), + ("nanmean", "a", True), + ("swapaxes", "a", True), + ("nanmin", "a", True), + ("nanmax", "a", True), + ("percentile", "a", True), + ("nanpercentile", "a", True), + ("quantile", "a", True), + ("nanquantile", "a", True), + ("flip", "m", True), + ("fix", "x", True), + ("trim_zeros", ["filt"], True), + ("broadcast_to", ["array"], True), + ("amax", ["a", "initial"], True), + ("amin", ["a", "initial"], True), + ("max", ["a", "initial"], True), + ("min", ["a", "initial"], True), + ("searchsorted", ["a", "v"], False), + ("nan_to_num", ["x", "nan", "posinf", "neginf"], True), + ("clip", ["a", "a_min", "a_max"], True), + ("append", ["arr", "values"], True), + ("compress", "a", True), + ("linspace", ["start", "stop"], True), + ("tile", "A", True), + ("lib.stride_tricks.sliding_window_view", "x", True), + ("rot90", "m", True), + ("insert", ["arr", "values"], True), + ("delete", ["arr"], True), + ("resize", "a", True), + ("reshape", "a", True), + ("intersect1d", ["ar1", "ar2"], True), +): + implement_consistent_units_by_argument(func_str, unit_arguments, wrap_output) + + +# implement isclose and allclose +def implement_close(func_str): + if np is None: + return + + func = getattr(np, func_str) + + @implements(func_str, "function") + def implementation(*args, **kwargs): + bound_args = signature(func).bind(*args, **kwargs) + labels = ["a", "b"] + arrays = {label: bound_args.arguments[label] for label in labels} + if "atol" in bound_args.arguments: + atol = bound_args.arguments["atol"] + a = arrays["a"] + if not hasattr(atol, "_REGISTRY") and hasattr(a, "_REGISTRY"): + # always use the units of `a` + atol_ = a._REGISTRY.Quantity(atol, a.units) + else: + atol_ = atol + arrays["atol"] = atol_ + + args, _ = unwrap_and_wrap_consistent_units(*arrays.values()) + for label, value in zip(arrays.keys(), args): + bound_args.arguments[label] = value + + return func(*bound_args.args, **bound_args.kwargs) + + +for func_str in ("isclose", "allclose"): + implement_close(func_str) + +# Handle atleast_nd functions + + +def implement_atleast_nd(func_str): + # If NumPy is not available, do not attempt implement that which does not exist + if np is None: + return + + func = getattr(np, func_str) + + @implements(func_str, "function") + def implementation(*arrays): + stripped_arrays, _ = convert_to_consistent_units(*arrays) + arrays_magnitude = func(*stripped_arrays) + if len(arrays) > 1: + return [ + ( + array_magnitude + if not hasattr(original, "_REGISTRY") + else original._REGISTRY.Quantity(array_magnitude, original.units) + ) + for array_magnitude, original in zip(arrays_magnitude, arrays) + ] + else: + output_unit = arrays[0].units + return output_unit._REGISTRY.Quantity(arrays_magnitude, output_unit) + + +for func_str in ("atleast_1d", "atleast_2d", "atleast_3d"): + implement_atleast_nd(func_str) + + +# Handle cumulative products (which must be dimensionless for consistent units across +# output array) +def implement_single_dimensionless_argument_func(func_str): + # If NumPy is not available, do not attempt implement that which does not exist + if np is None: + return + + func = getattr(np, func_str) + + @implements(func_str, "function") + def implementation(a, *args, **kwargs): + (a_stripped,), _ = convert_to_consistent_units( + a, pre_calc_units=a._REGISTRY.parse_units("dimensionless") + ) + return a._REGISTRY.Quantity(func(a_stripped, *args, **kwargs)) + + +for func_str in ("cumprod", "nancumprod"): + implement_single_dimensionless_argument_func(func_str) + +# Handle single-argument consistent unit functions +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" + ) + +# Handle functions that ignore units on input and output +for func_str in ( + "size", + "isreal", + "iscomplex", + "shape", + "ones_like", + "zeros_like", + "empty_like", + "argsort", + "argmin", + "argmax", + "ndim", + "nanargmax", + "nanargmin", + "count_nonzero", + "nonzero", + "result_type", +): + implement_func("function", func_str, input_units=None, output_unit=None) + +# Handle functions with output unit defined by operation +for func_str in ( + "std", + "nanstd", + "sum", + "nansum", + "cumsum", + "nancumsum", + "linalg.norm", +): + implement_func("function", func_str, input_units=None, output_unit="sum") +for func_str in ("diff", "ediff1d"): + implement_func("function", func_str, input_units=None, output_unit="delta") +for func_str in ("gradient",): + implement_func("function", func_str, input_units=None, output_unit="delta,div") +for func_str in ("linalg.solve",): + implement_func("function", func_str, input_units=None, output_unit="invdiv") +for func_str in ("var", "nanvar"): + implement_func("function", func_str, input_units=None, output_unit="variance") + + +def numpy_wrap(func_type, func, args, kwargs, types): + """Return the result from a NumPy function/ufunc as wrapped by Pint.""" + + if func_type == "function": + handled = HANDLED_FUNCTIONS + # Need to handle functions in submodules + name = ".".join(func.__module__.split(".")[1:] + [func.__name__]) + elif func_type == "ufunc": + handled = HANDLED_UFUNCS + # ufuncs do not have func.__module__ + name = func.__name__ + else: + raise ValueError(f"Invalid func_type {func_type}") + + if name not in handled or any(is_upcast_type(t) for t in types): + return NotImplemented + return handled[name](*args, **kwargs) diff --git a/datasette/vendored/pint/facets/numpy/quantity.py b/datasette/vendored/pint/facets/numpy/quantity.py new file mode 100644 index 0000000000..75dccec544 --- /dev/null +++ b/datasette/vendored/pint/facets/numpy/quantity.py @@ -0,0 +1,306 @@ +""" + pint.facets.numpy.quantity + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import functools +import math +import warnings +from typing import Any, Generic + +from ..._typing import Shape +from ...compat import HAS_NUMPY, _to_magnitude, np +from ...errors import DimensionalityError, PintTypeError, UnitStrippedWarning +from ..plain import MagnitudeT, PlainQuantity +from .numpy_func import ( + HANDLED_UFUNCS, + copy_units_output_ufuncs, + get_op_output_unit, + matching_input_copy_units_output_ufuncs, + matching_input_set_units_output_ufuncs, + numpy_wrap, + op_units_output_ufuncs, + set_units_ufuncs, +) + +try: + import uncertainties.unumpy as unp + from uncertainties import UFloat, ufloat + + HAS_UNCERTAINTIES = True +except ImportError: + unp = np + ufloat = Ufloat = None + HAS_UNCERTAINTIES = False + + +def method_wraps(numpy_func): + if isinstance(numpy_func, str): + numpy_func = getattr(np, numpy_func, None) + + def wrapper(func): + func.__wrapped__ = numpy_func + + return func + + return wrapper + + +class NumpyQuantity(Generic[MagnitudeT], PlainQuantity[MagnitudeT]): + """ """ + + # NumPy function/ufunc support + __array_priority__ = 17 + + def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): + if method != "__call__": + # Only handle ufuncs as callables + return NotImplemented + + # Replicate types from __array_function__ + types = { + type(arg) + for arg in list(inputs) + list(kwargs.values()) + if hasattr(arg, "__array_ufunc__") + } + + return numpy_wrap("ufunc", ufunc, inputs, kwargs, types) + + def __array_function__(self, func, types, args, kwargs): + return numpy_wrap("function", func, args, kwargs, types) + + _wrapped_numpy_methods = ["flatten", "astype", "item"] + + def _numpy_method_wrap(self, func, *args, **kwargs): + """Convenience method to wrap on the fly NumPy ndarray methods taking + care of the units. + """ + + # Set input units if needed + if func.__name__ in set_units_ufuncs: + self.__ito_if_needed(set_units_ufuncs[func.__name__][0]) + + value = func(*args, **kwargs) + + # Set output units as needed + if func.__name__ in ( + matching_input_copy_units_output_ufuncs + + copy_units_output_ufuncs + + self._wrapped_numpy_methods + ): + output_unit = self._units + elif func.__name__ in set_units_ufuncs: + output_unit = set_units_ufuncs[func.__name__][1] + elif func.__name__ in matching_input_set_units_output_ufuncs: + output_unit = matching_input_set_units_output_ufuncs[func.__name__] + elif func.__name__ in op_units_output_ufuncs: + output_unit = get_op_output_unit( + op_units_output_ufuncs[func.__name__], + self.units, + list(args) + list(kwargs.values()), + self._magnitude.size, + ) + else: + output_unit = None + + if output_unit is not None: + return self.__class__(value, output_unit) + + return value + + def __array__(self, t=None) -> np.ndarray: + if HAS_NUMPY and isinstance(self._magnitude, np.ndarray): + warnings.warn( + "The unit of the quantity is stripped when downcasting to ndarray.", + UnitStrippedWarning, + stacklevel=2, + ) + 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 + elif self.dimensionless: + pass + else: + raise DimensionalityError("dimensionless", self._units) + + if max is not None: + if isinstance(max, self.__class__): + max = max.to(self).magnitude + elif self.dimensionless: + pass + else: + raise DimensionalityError("dimensionless", self._units) + return self.__class__(self.magnitude.clip(min, max, out, **kwargs), self._units) + + def fill(self: NumpyQuantity, value) -> None: + self._units = value._units + return self.magnitude.fill(value.magnitude) + + def put(self: NumpyQuantity, indices, values, mode="raise") -> None: + if isinstance(values, self.__class__): + values = values.to(self).magnitude + elif self.dimensionless: + values = self.__class__(values, "").to(self) + else: + raise DimensionalityError("dimensionless", self._units) + self.magnitude.put(indices, values, mode) + + @property + def real(self) -> NumpyQuantity: + return self.__class__(self._magnitude.real, self._units) + + @property + def imag(self) -> NumpyQuantity: + return self.__class__(self._magnitude.imag, self._units) + + @property + def T(self): + return self.__class__(self._magnitude.T, self._units) + + @property + def flat(self): + for v in self._magnitude.flat: + yield self.__class__(v, self._units) + + @property + def shape(self) -> Shape: + return self._magnitude.shape + + @property + def dtype(self): + return self._magnitude.dtype + + @shape.setter + def shape(self, value): + self._magnitude.shape = value + + def searchsorted(self, v, side="left", sorter=None): + if isinstance(v, self.__class__): + v = v.to(self).magnitude + elif self.dimensionless: + v = self.__class__(v, "").to(self) + else: + raise DimensionalityError("dimensionless", self._units) + return self.magnitude.searchsorted(v, side) + + def dot(self, b): + """Dot product of two arrays. + + Wraps np.dot(). + """ + + return np.dot(self, b) + + @method_wraps("prod") + def prod(self, *args, **kwargs): + """Return the product of quantity elements over a given axis + + Wraps np.prod(). + """ + return np.prod(self, *args, **kwargs) + + def __ito_if_needed(self, to_units): + if self.unitless and to_units == "radian": + return + + self.ito(to_units) + + def __len__(self) -> int: + return len(self._magnitude) + + def __getattr__(self, item) -> Any: + if item.startswith("__array_"): + # Handle array protocol attributes other than `__array__` + raise AttributeError(f"Array protocol attribute {item} not available.") + elif item in HANDLED_UFUNCS or item in self._wrapped_numpy_methods: + magnitude_as_duck_array = _to_magnitude( + self._magnitude, force_ndarray_like=True + ) + try: + attr = getattr(magnitude_as_duck_array, item) + return functools.partial(self._numpy_method_wrap, attr) + except AttributeError: + raise AttributeError( + f"NumPy method {item} not available on {type(magnitude_as_duck_array)}" + ) + except TypeError as exc: + if "not callable" in str(exc): + raise AttributeError( + f"NumPy method {item} not callable on {type(magnitude_as_duck_array)}" + ) + else: + raise exc + elif ( + HAS_UNCERTAINTIES and item == "ndim" and isinstance(self._magnitude, UFloat) + ): + # Dimensionality of a single UFloat is 0, like any other scalar + return 0 + + try: + return getattr(self._magnitude, item) + except AttributeError: + raise AttributeError( + "Neither Quantity object nor its magnitude ({}) " + "has attribute '{}'".format(self._magnitude, item) + ) + + def __getitem__(self, key): + try: + return type(self)(self._magnitude[key], self._units) + except PintTypeError: + raise + except TypeError: + raise TypeError( + "Neither Quantity object nor its magnitude ({})" + "supports indexing".format(self._magnitude) + ) + + def __setitem__(self, key, value): + try: + # If we're dealing with a masked single value or a nan, set it + if ( + isinstance(self._magnitude, np.ma.MaskedArray) + and np.ma.is_masked(value) + and getattr(value, "size", 0) == 1 + ) or (getattr(value, "ndim", 0) == 0 and math.isnan(value)): + self._magnitude[key] = value + return + except TypeError: + pass + + try: + if isinstance(value, self.__class__): + factor = self.__class__( + value.magnitude, value._units / self._units + ).to_root_units() + else: + factor = self.__class__(value, self._units ** (-1)).to_root_units() + + if isinstance(factor, self.__class__): + if not factor.dimensionless: + raise DimensionalityError( + value, + self.units, + extra_msg=". Assign a quantity with the same dimensionality " + "or access the magnitude directly as " + f"`obj.magnitude[{key}] = {value}`.", + ) + self._magnitude[key] = factor.magnitude + else: + self._magnitude[key] = factor + + except PintTypeError: + raise + except TypeError as exc: + raise TypeError( + f"Neither Quantity object nor its magnitude ({self._magnitude}) " + "supports indexing" + ) from exc diff --git a/datasette/vendored/pint/facets/numpy/registry.py b/datasette/vendored/pint/facets/numpy/registry.py new file mode 100644 index 0000000000..1c447da9c7 --- /dev/null +++ b/datasette/vendored/pint/facets/numpy/registry.py @@ -0,0 +1,27 @@ +""" + pint.facets.numpy.registry + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from typing import Any, Generic + +from ...compat import TypeAlias +from ..plain import GenericPlainRegistry, QuantityT, UnitT +from .quantity import NumpyQuantity +from .unit import NumpyUnit + + +class GenericNumpyRegistry( + Generic[QuantityT, UnitT], GenericPlainRegistry[QuantityT, UnitT] +): + pass + + +class NumpyRegistry(GenericPlainRegistry[NumpyQuantity[Any], NumpyUnit]): + Quantity: TypeAlias = NumpyQuantity[Any] + Unit: TypeAlias = NumpyUnit diff --git a/datasette/vendored/pint/facets/numpy/unit.py b/datasette/vendored/pint/facets/numpy/unit.py new file mode 100644 index 0000000000..d6bf140a2f --- /dev/null +++ b/datasette/vendored/pint/facets/numpy/unit.py @@ -0,0 +1,43 @@ +""" + pint.facets.numpy.unit + ~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from ...compat import is_upcast_type +from ..plain import PlainUnit + + +class NumpyUnit(PlainUnit): + __array_priority__ = 17 + + def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): + if method != "__call__": + # Only handle ufuncs as callables + return NotImplemented + + # Check types and return NotImplemented when upcast type encountered + types = { + type(arg) + for arg in list(inputs) + list(kwargs.values()) + if hasattr(arg, "__array_ufunc__") + } + if any(is_upcast_type(other) for other in types): + return NotImplemented + + # Act on limited implementations by conversion to multiplicative identity + # Quantity + if ufunc.__name__ in ("true_divide", "divide", "floor_divide", "multiply"): + return ufunc( + *tuple( + self._REGISTRY.Quantity(1, self._units) if arg is self else arg + for arg in inputs + ), + **kwargs, + ) + + return NotImplemented diff --git a/datasette/vendored/pint/facets/plain/__init__.py b/datasette/vendored/pint/facets/plain/__init__.py new file mode 100644 index 0000000000..f84dd68f35 --- /dev/null +++ b/datasette/vendored/pint/facets/plain/__init__.py @@ -0,0 +1,39 @@ +""" + pint.facets.plain + ~~~~~~~~~~~~~~~~~ + + Base implementation for registry, units and quantities. + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from .definitions import ( + AliasDefinition, + DefaultsDefinition, + DimensionDefinition, + PrefixDefinition, + ScaleConverter, + UnitDefinition, +) +from .objects import PlainQuantity, PlainUnit +from .quantity import MagnitudeT +from .registry import GenericPlainRegistry, PlainRegistry, QuantityT, UnitT + +__all__ = [ + "GenericPlainRegistry", + "PlainUnit", + "PlainQuantity", + "PlainRegistry", + "AliasDefinition", + "DefaultsDefinition", + "DimensionDefinition", + "PrefixDefinition", + "ScaleConverter", + "UnitDefinition", + "QuantityT", + "UnitT", + "MagnitudeT", +] diff --git a/datasette/vendored/pint/facets/plain/definitions.py b/datasette/vendored/pint/facets/plain/definitions.py new file mode 100644 index 0000000000..a43ce0dbc6 --- /dev/null +++ b/datasette/vendored/pint/facets/plain/definitions.py @@ -0,0 +1,302 @@ +""" + pint.facets.plain.definitions + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import itertools +import numbers +import typing as ty +from dataclasses import dataclass +from functools import cached_property +from typing import Any + +from ... import errors +from ..._typing import Magnitude +from ...converters import Converter +from ...util import UnitsContainer + + +class NotNumeric(Exception): + """Internal exception. Do not expose outside Pint""" + + def __init__(self, value: Any): + self.value = value + + +######################## +# Convenience functions +######################## + + +@dataclass(frozen=True) +class Equality: + """An equality statement contains a left and right hand separated + by and equal (=) sign. + + lhs = rhs + + lhs and rhs are space stripped. + """ + + lhs: str + rhs: str + + +@dataclass(frozen=True) +class CommentDefinition: + """A comment""" + + comment: str + + +@dataclass(frozen=True) +class DefaultsDefinition: + """Directive to store default values.""" + + group: ty.Optional[str] + system: ty.Optional[str] + + def items(self): + if self.group is not None: + yield "group", self.group + if self.system is not None: + yield "system", self.system + + +@dataclass(frozen=True) +class NamedDefinition: + #: name of the prefix + name: str + + +@dataclass(frozen=True) +class PrefixDefinition(NamedDefinition, errors.WithDefErr): + """Definition of a prefix.""" + + #: scaling value for this prefix + value: numbers.Number + #: canonical symbol + defined_symbol: str | None = "" + #: 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) -> ScaleConverter: + return ScaleConverter(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 + ) + + 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 + ) + + +@dataclass(frozen=True) +class UnitDefinition(NamedDefinition, errors.WithDefErr): + """Definition of a unit.""" + + #: canonical symbol + defined_symbol: str | None + #: additional names for the same unit + aliases: tuple[str, ...] + #: A functiont that converts a value in these units into the reference units + # TODO: this has changed as converter is now annotated as converter. + # Briefly, in several places converter attributes like as_multiplicative were + # accesed. So having a generic function is a no go. + # I guess this was never used as errors where not raised. + converter: Converter | None + #: Reference units. + reference: UnitsContainer | None + + def __post_init__(self): + if not errors.is_valid_unit_name(self.name): + raise self.def_err(errors.MSG_INVALID_UNIT_NAME) + + # TODO: check why reference: Optional[UnitsContainer] + assert isinstance(self.reference, UnitsContainer) + + 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 + + elif all(map(errors.is_dim, self.reference.keys())): + invalid = tuple( + itertools.filterfalse( + errors.is_valid_dimension_name, self.reference.keys() + ) + ) + if invalid: + raise self.def_err( + f"refers to {', '.join(invalid)} that " + + errors.MSG_INVALID_DIMENSION_NAME + ) + + 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 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." + ) + + super.__setattr__(self, "_is_base", is_base) + + 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 + ) + + 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 + ) + + @property + def is_base(self) -> bool: + """Indicates if it is a base unit.""" + + # TODO: This is set in __post_init__ + return self._is_base + + @property + def is_multiplicative(self) -> bool: + # TODO: Check how to avoid this check + assert isinstance(self.converter, Converter) + return self.converter.is_multiplicative + + @property + def is_logarithmic(self) -> bool: + # TODO: Check how to avoid this check + assert isinstance(self.converter, Converter) + return self.converter.is_logarithmic + + @property + def symbol(self) -> str: + return self.defined_symbol or self.name + + @property + def has_symbol(self) -> bool: + return bool(self.defined_symbol) + + +@dataclass(frozen=True) +class DimensionDefinition(NamedDefinition, errors.WithDefErr): + """Definition of a root dimension""" + + @property + def is_base(self) -> bool: + return True + + def __post_init__(self) -> None: + if not errors.is_valid_dimension_name(self.name): + raise self.def_err(errors.MSG_INVALID_DIMENSION_NAME) + + +@dataclass(frozen=True) +class DerivedDimensionDefinition(DimensionDefinition): + """Definition of a derived dimension.""" + + #: reference dimensions. + reference: UnitsContainer + + @property + def is_base(self) -> bool: + return False + + 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" + ) + + invalid = tuple( + itertools.filterfalse(errors.is_valid_dimension_name, self.reference.keys()) + ) + + if invalid: + raise self.def_err( + f"refers to {', '.join(invalid)} that " + + errors.MSG_INVALID_DIMENSION_NAME + ) + + +@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 without offset.""" + + scale: float + + def to_reference(self, value: Magnitude, inplace: bool = False) -> Magnitude: + if inplace: + value *= self.scale + else: + value = value * self.scale + + return value + + def from_reference(self, value: Magnitude, inplace: bool = False) -> Magnitude: + if inplace: + value /= self.scale + else: + value = value / self.scale + + return value diff --git a/datasette/vendored/pint/facets/plain/objects.py b/datasette/vendored/pint/facets/plain/objects.py new file mode 100644 index 0000000000..a868c7f95a --- /dev/null +++ b/datasette/vendored/pint/facets/plain/objects.py @@ -0,0 +1,14 @@ +""" + pint.facets.plain.objects + ~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from .quantity import PlainQuantity +from .unit import PlainUnit, UnitsContainer + +__all__ = ["PlainUnit", "PlainQuantity", "UnitsContainer"] diff --git a/datasette/vendored/pint/facets/plain/qto.py b/datasette/vendored/pint/facets/plain/qto.py new file mode 100644 index 0000000000..22176491df --- /dev/null +++ b/datasette/vendored/pint/facets/plain/qto.py @@ -0,0 +1,424 @@ +from __future__ import annotations + +import bisect +import math +import numbers +import warnings +from typing import TYPE_CHECKING + +from ...compat import ( + mip_INF, + mip_INTEGER, + mip_Model, + mip_model, + mip_OptimizationStatus, + mip_xsum, +) +from ...errors import UndefinedBehavior +from ...util import infer_base_unit + +if TYPE_CHECKING: + from ..._typing import UnitLike + from ...util import UnitsContainer + from .quantity import PlainQuantity + + +def _get_reduced_units( + quantity: PlainQuantity, units: UnitsContainer +) -> UnitsContainer: + # loop through individual units and compare to each other unit + # can we do better than a nested loop here? + for unit1, exp in units.items(): + # make sure it wasn't already reduced to zero exponent on prior pass + if unit1 not in units: + continue + for unit2 in units: + # get exponent after reduction + exp = units[unit1] + if unit1 != unit2: + power = quantity._REGISTRY._get_dimensionality_ratio(unit1, unit2) + if power: + units = units.add(unit2, exp / power).remove([unit1]) + break + return units + + +def ito_reduced_units(quantity: PlainQuantity) -> None: + """Return PlainQuantity scaled in place to reduced units, i.e. one unit per + dimension. This will not reduce compound units (e.g., 'J/kg' will not + be reduced to m**2/s**2), nor can it make use of contexts at this time. + """ + + # shortcuts in case we're dimensionless or only a single unit + if quantity.dimensionless: + return quantity.ito({}) + if len(quantity._units) == 1: + return None + + units = quantity._units.copy() + new_units = _get_reduced_units(quantity, units) + + return quantity.ito(new_units) + + +def to_reduced_units( + quantity: PlainQuantity, +) -> PlainQuantity: + """Return PlainQuantity scaled in place to reduced units, i.e. one unit per + dimension. This will not reduce compound units (intentionally), nor + can it make use of contexts at this time. + """ + + # shortcuts in case we're dimensionless or only a single unit + if quantity.dimensionless: + return quantity.to({}) + if len(quantity._units) == 1: + return quantity + + units = quantity._units.copy() + new_units = _get_reduced_units(quantity, units) + + return quantity.to(new_units) + + +def to_compact( + quantity: PlainQuantity, unit: UnitsContainer | None = None +) -> PlainQuantity: + """ "Return PlainQuantity rescaled to compact, human-readable units. + + To get output in terms of a different unit, use the unit parameter. + + + Examples + -------- + + >>> import pint + >>> ureg = pint.UnitRegistry() + >>> (200e-9*ureg.s).to_compact() + + >>> (1e-2*ureg('kg m/s^2')).to_compact('N') + + """ + + if not isinstance(quantity.magnitude, numbers.Number) and not hasattr( + quantity.magnitude, "nominal_value" + ): + warnings.warn( + "to_compact applied to non numerical types has an undefined behavior.", + UndefinedBehavior, + stacklevel=2, + ) + return quantity + + if ( + quantity.unitless + or quantity.magnitude == 0 + or math.isnan(quantity.magnitude) + or math.isinf(quantity.magnitude) + ): + return quantity + + SI_prefixes: dict[int, str] = {} + for prefix in quantity._REGISTRY._prefixes.values(): + try: + scale = prefix.converter.scale + # Kludgy way to check if this is an SI prefix + log10_scale = int(math.log10(scale)) + if log10_scale == math.log10(scale): + SI_prefixes[log10_scale] = prefix.name + except Exception: + SI_prefixes[0] = "" + + SI_prefixes_list = sorted(SI_prefixes.items()) + SI_powers = [item[0] for item in SI_prefixes_list] + SI_bases = [item[1] for item in SI_prefixes_list] + + if unit is None: + unit = infer_base_unit(quantity, registry=quantity._REGISTRY) + else: + unit = infer_base_unit(quantity.__class__(1, unit), registry=quantity._REGISTRY) + + q_base = quantity.to(unit) + + magnitude = q_base.magnitude + # Support uncertainties + if hasattr(magnitude, "nominal_value"): + magnitude = magnitude.nominal_value + + units = list(q_base._units.items()) + units_numerator = [a for a in units if a[1] > 0] + + if len(units_numerator) > 0: + unit_str, unit_power = units_numerator[0] + else: + unit_str, unit_power = units[0] + + if unit_power > 0: + power = math.floor(math.log10(abs(magnitude)) / float(unit_power) / 3) * 3 + else: + power = math.ceil(math.log10(abs(magnitude)) / float(unit_power) / 3) * 3 + + index = bisect.bisect_left(SI_powers, power) + + if index >= len(SI_bases): + index = -1 + + prefix_str = SI_bases[index] + + new_unit_str = prefix_str + unit_str + new_unit_container = q_base._units.rename(unit_str, new_unit_str) + + return quantity.to(new_unit_container) + + +def to_preferred( + quantity: PlainQuantity, preferred_units: list[UnitLike] | None = None +) -> PlainQuantity: + """Return Quantity converted to a unit composed of the preferred units. + + Examples + -------- + + >>> import pint + >>> ureg = pint.UnitRegistry() + >>> (1*ureg.acre).to_preferred([ureg.meters]) + + >>> (1*(ureg.force_pound*ureg.m)).to_preferred([ureg.W]) + + """ + + units = _get_preferred(quantity, preferred_units) + return quantity.to(units) + + +def ito_preferred( + quantity: PlainQuantity, preferred_units: list[UnitLike] | None = None +) -> PlainQuantity: + """Return Quantity converted to a unit composed of the preferred units. + + Examples + -------- + + >>> import pint + >>> ureg = pint.UnitRegistry() + >>> (1*ureg.acre).to_preferred([ureg.meters]) + + >>> (1*(ureg.force_pound*ureg.m)).to_preferred([ureg.W]) + + """ + + units = _get_preferred(quantity, preferred_units) + return quantity.ito(units) + + +def _get_preferred( + quantity: PlainQuantity, preferred_units: list[UnitLike] | None = None +) -> PlainQuantity: + if preferred_units is None: + preferred_units = quantity._REGISTRY.default_preferred_units + + if not quantity.dimensionality: + return quantity._units.copy() + + # The optimizer isn't perfect, and will sometimes miss obvious solutions. + # This sub-algorithm is less powerful, but always finds the very simple solutions. + def find_simple(): + best_ratio = None + best_unit = None + self_dims = sorted(quantity.dimensionality) + self_exps = [quantity.dimensionality[d] for d in self_dims] + s_exps_head, *s_exps_tail = self_exps + n = len(s_exps_tail) + for preferred_unit in preferred_units: + dims = sorted(preferred_unit.dimensionality) + if dims == self_dims: + p_exps_head, *p_exps_tail = ( + preferred_unit.dimensionality[d] for d in dims + ) + if all( + s_exps_tail[i] * p_exps_head == p_exps_tail[i] ** s_exps_head + for i in range(n) + ): + ratio = p_exps_head / s_exps_head + ratio = max(ratio, 1 / ratio) + if best_ratio is None or ratio < best_ratio: + best_ratio = ratio + best_unit = preferred_unit ** (s_exps_head / p_exps_head) + return best_unit + + simple = find_simple() + if simple is not None: + return simple + + # For each dimension (e.g. T(ime), L(ength), M(ass)), assign a default base unit from + # the collection of base units + + unit_selections = { + base_unit.dimensionality: base_unit + for base_unit in map(quantity._REGISTRY.Unit, quantity._REGISTRY._base_units) + } + + # Override the default unit of each dimension with the 1D-units used in this Quantity + unit_selections.update( + { + unit.dimensionality: unit + for unit in map(quantity._REGISTRY.Unit, quantity._units.keys()) + } + ) + + # Determine the preferred unit for each dimensionality from the preferred_units + # (A prefered unit doesn't have to be only one dimensional, e.g. Watts) + preferred_dims = { + preferred_unit.dimensionality: preferred_unit + for preferred_unit in map(quantity._REGISTRY.Unit, preferred_units) + } + + # Combine the defaults and preferred, favoring the preferred + unit_selections.update(preferred_dims) + + # This algorithm has poor asymptotic time complexity, so first reduce the considered + # dimensions and units to only those that are useful to the problem + + # The dimensions (without powers) of this Quantity + dimension_set = set(quantity.dimensionality) + + # Getting zero exponents in dimensions not in dimension_set can be facilitated + # by units that interact with that dimension and one or more dimension_set members. + # For example MT^1 * LT^-1 lets you get MLT^0 when T is not in dimension_set. + # For each candidate unit that interacts with a dimension_set member, add the + # candidate unit's other dimensions to dimension_set, and repeat until no more + # dimensions are selected. + + discovery_done = False + while not discovery_done: + discovery_done = True + for d in unit_selections: + unit_dimensions = set(d) + intersection = unit_dimensions.intersection(dimension_set) + if 0 < len(intersection) < len(unit_dimensions): + # there are dimensions in this unit that are in dimension set + # and others that are not in dimension set + dimension_set = dimension_set.union(unit_dimensions) + discovery_done = False + break + + # filter out dimensions and their unit selections that don't interact with any + # dimension_set members + unit_selections = { + dimensionality: unit + for dimensionality, unit in unit_selections.items() + if set(dimensionality).intersection(dimension_set) + } + + # update preferred_units with the selected units that were originally preferred + preferred_units = list( + {u for d, u in unit_selections.items() if d in preferred_dims} + ) + preferred_units.sort(key=str) # for determinism + + # and unpreferred_units are the selected units that weren't originally preferred + unpreferred_units = list( + {u for d, u in unit_selections.items() if d not in preferred_dims} + ) + unpreferred_units.sort(key=str) # for determinism + + # for indexability + dimensions = list(dimension_set) + dimensions.sort() # for determinism + + # the powers for each elemet of dimensions (the list) for this Quantity + dimensionality = [quantity.dimensionality[dimension] for dimension in dimensions] + + # Now that the input data is minimized, setup the optimization problem + + # use mip to select units from preferred units + + model = mip_Model() + model.verbose = 0 + + # Make one variable for each candidate unit + + vars = [ + model.add_var(str(unit), lb=-mip_INF, ub=mip_INF, var_type=mip_INTEGER) + for unit in (preferred_units + unpreferred_units) + ] + + # where [u1 ... uN] are powers of N candidate units (vars) + # and [d1(uI) ... dK(uI)] are the K dimensional exponents of candidate unit I + # and [t1 ... tK] are the dimensional exponents of the quantity (quantity) + # create the following constraints + # + # ⎡ d1(u1) ⋯ dK(u1) ⎤ + # [ u1 ⋯ uN ] * ⎢ ⋮ ⋱ ⎢ = [ t1 ⋯ tK ] + # ⎣ d1(uN) dK(uN) ⎦ + # + # in English, the units we choose, and their exponents, when combined, must have the + # target dimensionality + + matrix = [ + [preferred_unit.dimensionality[dimension] for dimension in dimensions] + for preferred_unit in (preferred_units + unpreferred_units) + ] + + # Do the matrix multiplication with mip_model.xsum for performance and create constraints + for i in range(len(dimensions)): + dot = mip_model.xsum([var * vector[i] for var, vector in zip(vars, matrix)]) + # add constraint to the model + model += dot == dimensionality[i] + + # where [c1 ... cN] are costs, 1 when a preferred variable, and a large value when not + # minimize sum(abs(u1) * c1 ... abs(uN) * cN) + + # linearize the optimization variable via a proxy + objective = model.add_var("objective", lb=0, ub=mip_INF, var_type=mip_INTEGER) + + # Constrain the objective to be equal to the sums of the absolute values of the preferred + # unit powers. Do this by making a separate constraint for each permutation of signedness. + # Also apply the cost coefficient, which causes the output to prefer the preferred units + + # prefer units that interact with fewer dimensions + cost = [len(p.dimensionality) for p in preferred_units] + + # set the cost for non preferred units to a higher number + bias = ( + max(map(abs, dimensionality)) * max((1, *cost)) * 10 + ) # arbitrary, just needs to be larger + cost.extend([bias] * len(unpreferred_units)) + + for i in range(1 << len(vars)): + sum = mip_xsum( + [ + (-1 if i & 1 << (len(vars) - j - 1) else 1) * cost[j] * var + for j, var in enumerate(vars) + ] + ) + model += objective >= sum + + model.objective = objective + + # run the mips minimizer and extract the result if successful + if model.optimize() == mip_OptimizationStatus.OPTIMAL: + optimal_units = [] + min_objective = float("inf") + for i in range(model.num_solutions): + if model.objective_values[i] < min_objective: + min_objective = model.objective_values[i] + optimal_units.clear() + elif model.objective_values[i] > min_objective: + continue + + temp_unit = quantity._REGISTRY.Unit("") + for var in vars: + if var.xi(i): + temp_unit *= quantity._REGISTRY.Unit(var.name) ** var.xi(i) + optimal_units.append(temp_unit) + + sorting_keys = {tuple(sorted(unit._units)): unit for unit in optimal_units} + min_key = sorted(sorting_keys)[0] + result_unit = sorting_keys[min_key] + + return result_unit + + # for whatever reason, a solution wasn't found + # return the original quantity + return quantity._units.copy() diff --git a/datasette/vendored/pint/facets/plain/quantity.py b/datasette/vendored/pint/facets/plain/quantity.py new file mode 100644 index 0000000000..83d1f3613f --- /dev/null +++ b/datasette/vendored/pint/facets/plain/quantity.py @@ -0,0 +1,1480 @@ +""" + pint.facets.plain.quantity + ~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import copy +import datetime +import locale +import numbers +import operator +from collections.abc import Callable, Iterator, Sequence +from typing import ( + TYPE_CHECKING, + Any, + Generic, + Iterable, + TypeVar, + overload, +) + +from ..._typing import Magnitude, QuantityOrUnitLike, Scalar, UnitLike +from ...compat import ( + HAS_NUMPY, + _to_magnitude, + deprecated, + eq, + is_duck_array_type, + is_upcast_type, + np, + zero_or_nan, +) +from ...errors import DimensionalityError, OffsetUnitCalculusError, PintTypeError +from ...util import ( + PrettyIPython, + SharedRegistryObject, + UnitsContainer, + logger, + to_units_container, +) +from . import qto +from .definitions import UnitDefinition + +if TYPE_CHECKING: + from ..context import Context + from .unit import PlainUnit as Unit + from .unit import UnitsContainer as UnitsContainerT + + if HAS_NUMPY: + import numpy as np # noqa + +try: + import uncertainties.unumpy as unp + from uncertainties import UFloat, ufloat + + HAS_UNCERTAINTIES = True +except ImportError: + unp = np + ufloat = Ufloat = None + HAS_UNCERTAINTIES = False + + +MagnitudeT = TypeVar("MagnitudeT", bound=Magnitude) +ScalarT = TypeVar("ScalarT", bound=Scalar) + +T = TypeVar("T", bound=Magnitude) + + +def ireduce_dimensions(f): + def wrapped(self, *args, **kwargs): + result = f(self, *args, **kwargs) + try: + if result._REGISTRY.autoconvert_to_preferred: + result.ito_preferred() + except AttributeError: + pass + + try: + if result._REGISTRY.auto_reduce_dimensions: + result.ito_reduced_units() + except AttributeError: + pass + return result + + return wrapped + + +def check_implemented(f): + def wrapped(self, *args, **kwargs): + other = args[0] + if is_upcast_type(type(other)): + return NotImplemented + # pandas often gets to arrays of quantities [ Q_(1,"m"), Q_(2,"m")] + # and expects PlainQuantity * array[PlainQuantity] should return NotImplemented + elif isinstance(other, list) and other and isinstance(other[0], type(self)): + return NotImplemented + return f(self, *args, **kwargs) + + return wrapped + + +def method_wraps(numpy_func): + if isinstance(numpy_func, str): + numpy_func = getattr(np, numpy_func, None) + + def wrapper(func): + func.__wrapped__ = numpy_func + + return func + + return wrapper + + +# TODO: remove all nonmultiplicative remnants + + +class PlainQuantity(Generic[MagnitudeT], PrettyIPython, SharedRegistryObject): + """Implements a class to describe a physical quantity: + the product of a numerical value and a unit of measurement. + + Parameters + ---------- + value : str, pint.PlainQuantity or any numeric type + Value of the physical quantity to be created. + units : UnitsContainer, str or pint.PlainQuantity + Units of the physical quantity to be created. + + Returns + ------- + + """ + + _magnitude: MagnitudeT + + @property + def ndim(self) -> int: + if isinstance(self.magnitude, numbers.Number): + return 0 + if str(type(self.magnitude)) == "NAType": + return 0 + return self.magnitude.ndim + + @property + def force_ndarray(self) -> bool: + return self._REGISTRY.force_ndarray + + @property + def force_ndarray_like(self) -> bool: + return self._REGISTRY.force_ndarray_like + + def __reduce__(self) -> tuple[type, Magnitude, UnitsContainer]: + """Allow pickling quantities. Since UnitRegistries are not pickled, upon + unpickling the new object is always attached to the application registry. + """ + from datasette.vendored.pint import _unpickle_quantity + + # Note: type(self) would be a mistake as subclasses built by + # dinamically can't be pickled + # TODO: Check if this is still the case. + return _unpickle_quantity, (PlainQuantity, self.magnitude, self._units) + + @overload + def __new__( + cls, value: MagnitudeT, units: UnitLike | None = None + ) -> PlainQuantity[MagnitudeT]: ... + + @overload + def __new__( + cls, value: str, units: UnitLike | None = None + ) -> PlainQuantity[Any]: ... + + @overload + def __new__( # type: ignore[misc] + cls, value: Sequence[ScalarT], units: UnitLike | None = None + ) -> PlainQuantity[Any]: ... + + @overload + def __new__( + cls, value: PlainQuantity[Any], units: UnitLike | None = None + ) -> PlainQuantity[Any]: ... + + def __new__(cls, value, units=None): + if is_upcast_type(type(value)): + raise TypeError(f"PlainQuantity cannot wrap upcast type {type(value)}") + + if units is None and isinstance(value, str) and value == "": + raise ValueError( + "Expression to parse as PlainQuantity cannot be an empty string." + ) + + if units is None and isinstance(value, str): + ureg = SharedRegistryObject.__new__(cls)._REGISTRY + inst = ureg.parse_expression(value) + return cls.__new__(cls, inst) + + if units is None and isinstance(value, cls): + return copy.copy(value) + + inst = SharedRegistryObject().__new__(cls) + if units is None: + units = inst.UnitsContainer() + else: + if isinstance(units, (UnitsContainer, UnitDefinition)): + units = units + elif isinstance(units, str): + units = inst._REGISTRY.parse_units(units)._units + elif isinstance(units, SharedRegistryObject): + if isinstance(units, PlainQuantity) and units.magnitude != 1: + units = copy.copy(units)._units + logger.warning( + "Creating new PlainQuantity using a non unity PlainQuantity as units." + ) + else: + units = units._units + else: + raise TypeError( + "units must be of type str, PlainQuantity or " + "UnitsContainer; not {}.".format(type(units)) + ) + if isinstance(value, cls): + magnitude = value.to(units)._magnitude + else: + magnitude = _to_magnitude( + value, inst.force_ndarray, inst.force_ndarray_like + ) + inst._magnitude = magnitude + inst._units = units + + return inst + + def __iter__(self: PlainQuantity[MagnitudeT]) -> Iterator[Any]: + # Make sure that, if self.magnitude is not iterable, we raise TypeError as soon + # as one calls iter(self) without waiting for the first element to be drawn from + # the iterator + it_magnitude = iter(self.magnitude) + + def it_outer(): + for element in it_magnitude: + yield self.__class__(element, self._units) + + return it_outer() + + def __copy__(self) -> PlainQuantity[MagnitudeT]: + ret = self.__class__(copy.copy(self._magnitude), self._units) + return ret + + def __deepcopy__(self, memo) -> PlainQuantity[MagnitudeT]: + ret = self.__class__( + copy.deepcopy(self._magnitude, memo), copy.deepcopy(self._units, memo) + ) + return ret + + @deprecated( + "This function will be removed in future versions of pint.\n" + "Use ureg.formatter.format_quantity_babel" + ) + def format_babel(self, spec: str = "", **kwspec: Any) -> str: + return self._REGISTRY.formatter.format_quantity_babel(self, spec, **kwspec) + + def __format__(self, spec: str) -> str: + return self._REGISTRY.formatter.format_quantity(self, spec) + + def __str__(self) -> str: + return self._REGISTRY.formatter.format_quantity(self) + + def __bytes__(self) -> bytes: + return str(self).encode(locale.getpreferredencoding()) + + def __repr__(self) -> str: + if HAS_UNCERTAINTIES: + if isinstance(self._magnitude, UFloat): + return f"" + else: + return f"" + elif isinstance(self._magnitude, float): + return f"" + + return f"" + + def __hash__(self) -> int: + self_base = self.to_base_units() + if self_base.dimensionless: + return hash(self_base.magnitude) + + return hash((self_base.__class__, self_base.magnitude, self_base.units)) + + @property + def magnitude(self) -> MagnitudeT: + """PlainQuantity's magnitude. Long form for `m`""" + return self._magnitude + + @property + def m(self) -> MagnitudeT: + """PlainQuantity's magnitude. Short form for `magnitude`""" + return self._magnitude + + def m_as(self, units) -> MagnitudeT: + """PlainQuantity's magnitude expressed in particular units. + + Parameters + ---------- + units : pint.PlainQuantity, str or dict + destination units + + Returns + ------- + + """ + return self.to(units).magnitude + + @property + def units(self) -> Unit: + """PlainQuantity's units. Long form for `u`""" + return self._REGISTRY.Unit(self._units) + + @property + def u(self) -> Unit: + """PlainQuantity's units. Short form for `units`""" + return self._REGISTRY.Unit(self._units) + + @property + def unitless(self) -> bool: + """ """ + return not bool(self.to_root_units()._units) + + def unit_items(self) -> Iterable[tuple[str, Scalar]]: + """A view of the unit items.""" + return self._units.unit_items() + + @property + def dimensionless(self) -> bool: + """ """ + tmp = self.to_root_units() + + return not bool(tmp.dimensionality) + + _dimensionality: UnitsContainerT | None = None + + @property + def dimensionality(self) -> UnitsContainerT: + """ + Returns + ------- + dict + Dimensionality of the PlainQuantity, e.g. ``{length: 1, time: -1}`` + """ + if self._dimensionality is None: + self._dimensionality = self._REGISTRY._get_dimensionality(self._units) + + return self._dimensionality + + def check(self, dimension: UnitLike) -> bool: + """Return true if the quantity's dimension matches passed dimension.""" + return self.dimensionality == self._REGISTRY.get_dimensionality(dimension) + + @classmethod + def from_list( + cls, quant_list: list[PlainQuantity[MagnitudeT]], units=None + ) -> PlainQuantity[MagnitudeT]: + """Transforms a list of Quantities into an numpy.array quantity. + If no units are specified, the unit of the first element will be used. + Same as from_sequence. + + If units is not specified and list is empty, the unit cannot be determined + and a ValueError is raised. + + Parameters + ---------- + quant_list : list of pint.PlainQuantity + list of pint.PlainQuantity + units : UnitsContainer, str or pint.PlainQuantity + units of the physical quantity to be created (Default value = None) + + Returns + ------- + pint.PlainQuantity + """ + return cls.from_sequence(quant_list, units=units) + + @classmethod + def from_sequence( + cls, seq: Sequence[PlainQuantity[MagnitudeT]], units=None + ) -> PlainQuantity[MagnitudeT]: + """Transforms a sequence of Quantities into an numpy.array quantity. + If no units are specified, the unit of the first element will be used. + + If units is not specified and sequence is empty, the unit cannot be determined + and a ValueError is raised. + + Parameters + ---------- + seq : sequence of pint.PlainQuantity + sequence of pint.PlainQuantity + units : UnitsContainer, str or pint.PlainQuantity + units of the physical quantity to be created (Default value = None) + + Returns + ------- + pint.PlainQuantity + """ + + len_seq = len(seq) + if units is None: + if len_seq: + units = seq[0].u + else: + raise ValueError("Cannot determine units from empty sequence!") + + a = np.empty(len_seq) + + for i, seq_i in enumerate(seq): + a[i] = seq_i.m_as(units) + # raises DimensionalityError if incompatible units are used in the sequence + + return cls(a, units) + + @classmethod + def from_tuple(cls, tup): + return cls(tup[0], cls._REGISTRY.UnitsContainer(tup[1])) + + def to_tuple(self) -> tuple[MagnitudeT, tuple[tuple[str, ...]]]: + return self.m, tuple(self._units.items()) + + def compatible_units(self, *contexts): + if contexts: + with self._REGISTRY.context(*contexts): + return self._REGISTRY.get_compatible_units(self._units) + + return self._REGISTRY.get_compatible_units(self._units) + + def is_compatible_with( + self, other: Any, *contexts: str | Context, **ctx_kwargs: Any + ) -> bool: + """check if the other object is compatible + + Parameters + ---------- + other + The object to check. Treated as dimensionless if not a + PlainQuantity, Unit or str. + *contexts : str or pint.Context + Contexts to use in the transformation. + **ctx_kwargs : + Values for the Context/s + + Returns + ------- + bool + """ + from .unit import PlainUnit + + if contexts or self._REGISTRY._active_ctx: + try: + self.to(other, *contexts, **ctx_kwargs) + return True + except DimensionalityError: + return False + + if isinstance(other, (PlainQuantity, PlainUnit)): + return self.dimensionality == other.dimensionality + + if isinstance(other, str): + return ( + self.dimensionality == self._REGISTRY.parse_units(other).dimensionality + ) + + return self.dimensionless + + def _convert_magnitude_not_inplace(self, other, *contexts, **ctx_kwargs): + if contexts: + with self._REGISTRY.context(*contexts, **ctx_kwargs): + return self._REGISTRY.convert(self._magnitude, self._units, other) + + return self._REGISTRY.convert(self._magnitude, self._units, other) + + def _convert_magnitude(self, other, *contexts, **ctx_kwargs): + if contexts: + with self._REGISTRY.context(*contexts, **ctx_kwargs): + return self._REGISTRY.convert(self._magnitude, self._units, other) + + return self._REGISTRY.convert( + self._magnitude, + self._units, + other, + inplace=is_duck_array_type(type(self._magnitude)), + ) + + def ito( + self, other: QuantityOrUnitLike | None = None, *contexts, **ctx_kwargs + ) -> None: + """Inplace rescale to different units. + + Parameters + ---------- + other : pint.PlainQuantity, str or dict + Destination units. (Default value = None) + *contexts : str or pint.Context + Contexts to use in the transformation. + **ctx_kwargs : + Values for the Context/s + """ + + other = to_units_container(other, self._REGISTRY) + + self._magnitude = self._convert_magnitude(other, *contexts, **ctx_kwargs) + self._units = other + + return None + + def to( + self, other: QuantityOrUnitLike | None = None, *contexts, **ctx_kwargs + ) -> PlainQuantity: + """Return PlainQuantity rescaled to different units. + + Parameters + ---------- + other : pint.PlainQuantity, str or dict + destination units. (Default value = None) + *contexts : str or pint.Context + Contexts to use in the transformation. + **ctx_kwargs : + Values for the Context/s + + Returns + ------- + pint.PlainQuantity + """ + other = to_units_container(other, self._REGISTRY) + + magnitude = self._convert_magnitude_not_inplace(other, *contexts, **ctx_kwargs) + + return self.__class__(magnitude, other) + + def ito_root_units(self) -> None: + """Return PlainQuantity rescaled to root units.""" + + _, other = self._REGISTRY._get_root_units(self._units) + + self._magnitude = self._convert_magnitude(other) + self._units = other + + return None + + def to_root_units(self) -> PlainQuantity[MagnitudeT]: + """Return PlainQuantity rescaled to root units.""" + + _, other = self._REGISTRY._get_root_units(self._units) + + magnitude = self._convert_magnitude_not_inplace(other) + + return self.__class__(magnitude, other) + + def ito_base_units(self) -> None: + """Return PlainQuantity rescaled to plain units.""" + + _, other = self._REGISTRY._get_base_units(self._units) + + self._magnitude = self._convert_magnitude(other) + self._units = other + + return None + + def to_base_units(self) -> PlainQuantity[MagnitudeT]: + """Return PlainQuantity rescaled to plain units.""" + + _, other = self._REGISTRY._get_base_units(self._units) + + magnitude = self._convert_magnitude_not_inplace(other) + + return self.__class__(magnitude, other) + + # Functions not essential to a Quantity but it is + # convenient that they live in PlainQuantity. + # They are implemented elsewhere to keep Quantity class clean. + to_compact = qto.to_compact + to_preferred = qto.to_preferred + ito_preferred = qto.ito_preferred + to_reduced_units = qto.to_reduced_units + ito_reduced_units = qto.ito_reduced_units + + # Mathematical operations + def __int__(self) -> int: + if self.dimensionless: + return int(self._convert_magnitude_not_inplace(UnitsContainer())) + raise DimensionalityError(self._units, "dimensionless") + + def __float__(self) -> float: + if self.dimensionless: + return float(self._convert_magnitude_not_inplace(UnitsContainer())) + raise DimensionalityError(self._units, "dimensionless") + + def __complex__(self) -> complex: + if self.dimensionless: + return complex(self._convert_magnitude_not_inplace(UnitsContainer())) + raise DimensionalityError(self._units, "dimensionless") + + @check_implemented + def _iadd_sub(self, other, op): + """Perform addition or subtraction operation in-place and return the result. + + Parameters + ---------- + other : pint.PlainQuantity or any type accepted by :func:`_to_magnitude` + object to be added to / subtracted from self + op : function + operator function (e.g. operator.add, operator.isub) + + """ + if not self._check(other): + # other not from same Registry or not a PlainQuantity + try: + other_magnitude = _to_magnitude( + other, self.force_ndarray, self.force_ndarray_like + ) + except PintTypeError: + raise + except TypeError: + return NotImplemented + if zero_or_nan(other, True): + # If the other value is 0 (but not PlainQuantity 0) + # do the operation without checking units. + # We do the calculation instead of just returning the same + # value to enforce any shape checking and type casting due to + # the operation. + self._magnitude = op(self._magnitude, other_magnitude) + elif self.dimensionless: + self.ito(self.UnitsContainer()) + self._magnitude = op(self._magnitude, other_magnitude) + else: + raise DimensionalityError(self._units, "dimensionless") + return self + + if not self.dimensionality == other.dimensionality: + raise DimensionalityError( + self._units, other._units, self.dimensionality, other.dimensionality + ) + + # Next we define some variables to make if-clauses more readable. + self_non_mul_units = self._get_non_multiplicative_units() + is_self_multiplicative = len(self_non_mul_units) == 0 + if len(self_non_mul_units) == 1: + self_non_mul_unit = self_non_mul_units[0] + other_non_mul_units = other._get_non_multiplicative_units() + is_other_multiplicative = len(other_non_mul_units) == 0 + if len(other_non_mul_units) == 1: + other_non_mul_unit = other_non_mul_units[0] + + # Presence of non-multiplicative units gives rise to several cases. + if is_self_multiplicative and is_other_multiplicative: + if self._units == other._units: + self._magnitude = op(self._magnitude, other._magnitude) + # If only self has a delta unit, other determines unit of result. + elif self._get_delta_units() and not other._get_delta_units(): + self._magnitude = op( + self._convert_magnitude(other._units), other._magnitude + ) + self._units = other._units + else: + self._magnitude = op(self._magnitude, other.to(self._units)._magnitude) + + elif ( + op == operator.isub + and len(self_non_mul_units) == 1 + and self._units[self_non_mul_unit] == 1 + and not other._has_compatible_delta(self_non_mul_unit) + ): + if self._units == other._units: + self._magnitude = op(self._magnitude, other._magnitude) + else: + self._magnitude = op(self._magnitude, other.to(self._units)._magnitude) + self._units = self._units.rename( + self_non_mul_unit, "delta_" + self_non_mul_unit + ) + + elif ( + op == operator.isub + and len(other_non_mul_units) == 1 + and other._units[other_non_mul_unit] == 1 + and not self._has_compatible_delta(other_non_mul_unit) + ): + # we convert to self directly since it is multiplicative + self._magnitude = op(self._magnitude, other.to(self._units)._magnitude) + + elif ( + len(self_non_mul_units) == 1 + # order of the dimension of offset unit == 1 ? + and self._units[self_non_mul_unit] == 1 + and other._has_compatible_delta(self_non_mul_unit) + ): + # Replace offset unit in self by the corresponding delta unit. + # This is done to prevent a shift by offset in the to()-call. + tu = self._units.rename(self_non_mul_unit, "delta_" + self_non_mul_unit) + self._magnitude = op(self._magnitude, other.to(tu)._magnitude) + elif ( + len(other_non_mul_units) == 1 + # order of the dimension of offset unit == 1 ? + and other._units[other_non_mul_unit] == 1 + and self._has_compatible_delta(other_non_mul_unit) + ): + # Replace offset unit in other by the corresponding delta unit. + # This is done to prevent a shift by offset in the to()-call. + tu = other._units.rename(other_non_mul_unit, "delta_" + other_non_mul_unit) + self._magnitude = op(self._convert_magnitude(tu), other._magnitude) + self._units = other._units + else: + raise OffsetUnitCalculusError(self._units, other._units) + + return self + + @check_implemented + def _add_sub(self, other, op): + """Perform addition or subtraction operation and return the result. + + Parameters + ---------- + other : pint.PlainQuantity or any type accepted by :func:`_to_magnitude` + object to be added to / subtracted from self + op : function + operator function (e.g. operator.add, operator.isub) + """ + if not self._check(other): + # other not from same Registry or not a PlainQuantity + if zero_or_nan(other, True): + # If the other value is 0 or NaN (but not a PlainQuantity) + # do the operation without checking units. + # We do the calculation instead of just returning the same + # value to enforce any shape checking and type casting due to + # the operation. + units = self._units + magnitude = op( + self._magnitude, + _to_magnitude(other, self.force_ndarray, self.force_ndarray_like), + ) + elif self.dimensionless: + units = self.UnitsContainer() + magnitude = op( + self.to(units)._magnitude, + _to_magnitude(other, self.force_ndarray, self.force_ndarray_like), + ) + else: + raise DimensionalityError(self._units, "dimensionless") + return self.__class__(magnitude, units) + + if not self.dimensionality == other.dimensionality: + raise DimensionalityError( + self._units, other._units, self.dimensionality, other.dimensionality + ) + + # Next we define some variables to make if-clauses more readable. + self_non_mul_units = self._get_non_multiplicative_units() + is_self_multiplicative = len(self_non_mul_units) == 0 + if len(self_non_mul_units) == 1: + self_non_mul_unit = self_non_mul_units[0] + other_non_mul_units = other._get_non_multiplicative_units() + is_other_multiplicative = len(other_non_mul_units) == 0 + if len(other_non_mul_units) == 1: + other_non_mul_unit = other_non_mul_units[0] + + # Presence of non-multiplicative units gives rise to several cases. + if is_self_multiplicative and is_other_multiplicative: + if self._units == other._units: + magnitude = op(self._magnitude, other._magnitude) + units = self._units + # If only self has a delta unit, other determines unit of result. + elif self._get_delta_units() and not other._get_delta_units(): + magnitude = op( + self._convert_magnitude_not_inplace(other._units), other._magnitude + ) + units = other._units + else: + units = self._units + magnitude = op(self._magnitude, other.to(self._units).magnitude) + + elif ( + op == operator.sub + and len(self_non_mul_units) == 1 + and self._units[self_non_mul_unit] == 1 + and not other._has_compatible_delta(self_non_mul_unit) + ): + if self._units == other._units: + magnitude = op(self._magnitude, other._magnitude) + else: + magnitude = op(self._magnitude, other.to(self._units)._magnitude) + units = self._units.rename(self_non_mul_unit, "delta_" + self_non_mul_unit) + + elif ( + op == operator.sub + and len(other_non_mul_units) == 1 + and other._units[other_non_mul_unit] == 1 + and not self._has_compatible_delta(other_non_mul_unit) + ): + # we convert to self directly since it is multiplicative + magnitude = op(self._magnitude, other.to(self._units)._magnitude) + units = self._units + + elif ( + len(self_non_mul_units) == 1 + # order of the dimension of offset unit == 1 ? + and self._units[self_non_mul_unit] == 1 + and other._has_compatible_delta(self_non_mul_unit) + ): + # Replace offset unit in self by the corresponding delta unit. + # This is done to prevent a shift by offset in the to()-call. + tu = self._units.rename(self_non_mul_unit, "delta_" + self_non_mul_unit) + magnitude = op(self._magnitude, other.to(tu).magnitude) + units = self._units + elif ( + len(other_non_mul_units) == 1 + # order of the dimension of offset unit == 1 ? + and other._units[other_non_mul_unit] == 1 + and self._has_compatible_delta(other_non_mul_unit) + ): + # Replace offset unit in other by the corresponding delta unit. + # This is done to prevent a shift by offset in the to()-call. + tu = other._units.rename(other_non_mul_unit, "delta_" + other_non_mul_unit) + magnitude = op(self._convert_magnitude_not_inplace(tu), other._magnitude) + units = other._units + else: + raise OffsetUnitCalculusError(self._units, other._units) + + return self.__class__(magnitude, units) + + @overload + def __iadd__(self, other: datetime.datetime) -> datetime.timedelta: # type: ignore[misc] + ... + + @overload + def __iadd__(self, other) -> PlainQuantity[MagnitudeT]: ... + + def __iadd__(self, other): + if isinstance(other, datetime.datetime): + return self.to_timedelta() + other + elif is_duck_array_type(type(self._magnitude)): + return self._iadd_sub(other, operator.iadd) + + return self._add_sub(other, operator.add) + + def __add__(self, other): + if isinstance(other, datetime.datetime): + return self.to_timedelta() + other + + return self._add_sub(other, operator.add) + + __radd__ = __add__ + + def __isub__(self, other): + if is_duck_array_type(type(self._magnitude)): + return self._iadd_sub(other, operator.isub) + + return self._add_sub(other, operator.sub) + + def __sub__(self, other): + return self._add_sub(other, operator.sub) + + def __rsub__(self, other): + if isinstance(other, datetime.datetime): + return other - self.to_timedelta() + + return -self._add_sub(other, operator.sub) + + @check_implemented + @ireduce_dimensions + def _imul_div(self, other, magnitude_op, units_op=None): + """Perform multiplication or division operation in-place and return the + result. + + Parameters + ---------- + other : pint.PlainQuantity or any type accepted by :func:`_to_magnitude` + object to be multiplied/divided with self + magnitude_op : function + operator function to perform on the magnitudes + (e.g. operator.mul) + units_op : function or None + operator function to perform on the units; if None, + *magnitude_op* is used (Default value = None) + + Returns + ------- + + """ + if units_op is None: + units_op = magnitude_op + + offset_units_self = self._get_non_multiplicative_units() + 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: + if self._units[offset_units_self[0]] != 1 or magnitude_op not in ( + operator.mul, + operator.imul, + ): + raise OffsetUnitCalculusError( + self._units, getattr(other, "units", "") + ) + try: + other_magnitude = _to_magnitude( + other, self.force_ndarray, self.force_ndarray_like + ) + except PintTypeError: + raise + except TypeError: + return NotImplemented + self._magnitude = magnitude_op(self._magnitude, other_magnitude) + self._units = units_op(self._units, self.UnitsContainer()) + return self + + if isinstance(other, self._REGISTRY.Unit): + other = 1 * other + + if not self._ok_for_muldiv(no_offset_units_self): + raise OffsetUnitCalculusError(self._units, other._units) + elif no_offset_units_self == len(self._units) == 1: + self.ito_root_units() + + no_offset_units_other = len(other._get_non_multiplicative_units()) + + if not other._ok_for_muldiv(no_offset_units_other): + raise OffsetUnitCalculusError(self._units, other._units) + elif no_offset_units_other == len(other._units) == 1: + other.ito_root_units() + + self._magnitude = magnitude_op(self._magnitude, other._magnitude) + self._units = units_op(self._units, other._units) + + return self + + @check_implemented + @ireduce_dimensions + def _mul_div(self, other, magnitude_op, units_op=None): + """Perform multiplication or division operation and return the result. + + Parameters + ---------- + other : pint.PlainQuantity or any type accepted by :func:`_to_magnitude` + object to be multiplied/divided with self + magnitude_op : function + operator function to perform on the magnitudes + (e.g. operator.mul) + units_op : function or None + operator function to perform on the units; if None, + *magnitude_op* is used (Default value = None) + + Returns + ------- + + """ + if units_op is None: + units_op = magnitude_op + + offset_units_self = self._get_non_multiplicative_units() + 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: + if self._units[offset_units_self[0]] != 1 or magnitude_op not in ( + operator.mul, + operator.imul, + ): + raise OffsetUnitCalculusError( + self._units, getattr(other, "units", "") + ) + try: + other_magnitude = _to_magnitude( + other, self.force_ndarray, self.force_ndarray_like + ) + except PintTypeError: + raise + except TypeError: + return NotImplemented + + magnitude = magnitude_op(self._magnitude, other_magnitude) + units = units_op(self._units, self.UnitsContainer()) + + return self.__class__(magnitude, units) + + if isinstance(other, self._REGISTRY.Unit): + other = 1 * other + + new_self = self + + if not self._ok_for_muldiv(no_offset_units_self): + raise OffsetUnitCalculusError(self._units, other._units) + elif no_offset_units_self == len(self._units) == 1: + new_self = self.to_root_units() + + no_offset_units_other = len(other._get_non_multiplicative_units()) + + if not other._ok_for_muldiv(no_offset_units_other): + raise OffsetUnitCalculusError(self._units, other._units) + elif no_offset_units_other == len(other._units) == 1: + other = other.to_root_units() + + magnitude = magnitude_op(new_self._magnitude, other._magnitude) + units = units_op(new_self._units, other._units) + + return self.__class__(magnitude, units) + + def __imul__(self, other): + if is_duck_array_type(type(self._magnitude)): + return self._imul_div(other, operator.imul) + + return self._mul_div(other, operator.mul) + + def __mul__(self, other): + return self._mul_div(other, operator.mul) + + __rmul__ = __mul__ + + def __matmul__(self, other): + return np.matmul(self, other) + + __rmatmul__ = __matmul__ + + def _truedivide_cast_int(self, a, b): + t = self._REGISTRY.non_int_type + if isinstance(a, int): + a = t(a) + if isinstance(b, int): + b = t(b) + return operator.truediv(a, b) + + def __itruediv__(self, other): + if is_duck_array_type(type(self._magnitude)): + return self._imul_div(other, operator.itruediv) + + return self._mul_div(other, operator.truediv) + + def __truediv__(self, other): + if isinstance(self.m, int) or isinstance(getattr(other, "m", None), int): + return self._mul_div(other, self._truedivide_cast_int, operator.truediv) + return self._mul_div(other, operator.truediv) + + def __rtruediv__(self, other): + try: + other_magnitude = _to_magnitude( + other, self.force_ndarray, self.force_ndarray_like + ) + except PintTypeError: + raise + except TypeError: + return NotImplemented + + no_offset_units_self = len(self._get_non_multiplicative_units()) + if not self._ok_for_muldiv(no_offset_units_self): + raise OffsetUnitCalculusError(self._units, "") + elif no_offset_units_self == len(self._units) == 1: + self = self.to_root_units() + + return self.__class__(other_magnitude / self._magnitude, 1 / self._units) + + __div__ = __truediv__ + __rdiv__ = __rtruediv__ + __idiv__ = __itruediv__ + + def __ifloordiv__(self, other): + if self._check(other): + self._magnitude //= other.to(self._units)._magnitude + elif self.dimensionless: + self._magnitude = self.to("")._magnitude // other + else: + raise DimensionalityError(self._units, "dimensionless") + self._units = self.UnitsContainer({}) + return self + + @check_implemented + def __floordiv__(self, other): + if self._check(other): + magnitude = self._magnitude // other.to(self._units)._magnitude + elif self.dimensionless: + magnitude = self.to("")._magnitude // other + else: + raise DimensionalityError(self._units, "dimensionless") + return self.__class__(magnitude, self.UnitsContainer({})) + + @check_implemented + def __rfloordiv__(self, other): + if self._check(other): + magnitude = other._magnitude // self.to(other._units)._magnitude + elif self.dimensionless: + magnitude = other // self.to("")._magnitude + else: + raise DimensionalityError(self._units, "dimensionless") + return self.__class__(magnitude, self.UnitsContainer({})) + + @check_implemented + def __imod__(self, other): + if not self._check(other): + other = self.__class__(other, self.UnitsContainer({})) + self._magnitude %= other.to(self._units)._magnitude + return self + + @check_implemented + def __mod__(self, other): + if not self._check(other): + other = self.__class__(other, self.UnitsContainer({})) + magnitude = self._magnitude % other.to(self._units)._magnitude + return self.__class__(magnitude, self._units) + + @check_implemented + def __rmod__(self, other): + if self._check(other): + magnitude = other._magnitude % self.to(other._units)._magnitude + return self.__class__(magnitude, other._units) + elif self.dimensionless: + magnitude = other % self.to("")._magnitude + return self.__class__(magnitude, self.UnitsContainer({})) + else: + raise DimensionalityError(self._units, "dimensionless") + + @check_implemented + def __divmod__(self, other): + if not self._check(other): + other = self.__class__(other, self.UnitsContainer({})) + q, r = divmod(self._magnitude, other.to(self._units)._magnitude) + return ( + self.__class__(q, self.UnitsContainer({})), + self.__class__(r, self._units), + ) + + @check_implemented + def __rdivmod__(self, other): + if self._check(other): + q, r = divmod(other._magnitude, self.to(other._units)._magnitude) + unit = other._units + elif self.dimensionless: + q, r = divmod(other, self.to("")._magnitude) + unit = self.UnitsContainer({}) + else: + raise DimensionalityError(self._units, "dimensionless") + return (self.__class__(q, self.UnitsContainer({})), self.__class__(r, unit)) + + @check_implemented + def __ipow__(self, other): + if not is_duck_array_type(type(self._magnitude)): + return self.__pow__(other) + + try: + _to_magnitude(other, self.force_ndarray, self.force_ndarray_like) + except PintTypeError: + raise + except TypeError: + return NotImplemented + else: + if not self._ok_for_muldiv: + raise OffsetUnitCalculusError(self._units) + + if is_duck_array_type(type(getattr(other, "_magnitude", other))): + # arrays are refused as exponent, because they would create + # len(array) quantities of len(set(array)) different units + # unless the plain is dimensionless. Ensure dimensionless + # units are reduced to "dimensionless". + # Note: this will strip Units of degrees or radians from PlainQuantity + if self.dimensionless: + if getattr(other, "dimensionless", False): + self._magnitude = self.m_as("") ** other.m_as("") + self._units = self.UnitsContainer() + return self + elif not getattr(other, "dimensionless", True): + raise DimensionalityError(other._units, "dimensionless") + else: + self._magnitude = self.m_as("") ** other + self._units = self.UnitsContainer() + return self + elif np.size(other) > 1: + raise DimensionalityError( + self._units, + "dimensionless", + extra_msg=". PlainQuantity array exponents are only allowed if the " + "plain is dimensionless", + ) + + if other == 1: + return self + elif other == 0: + self._units = self.UnitsContainer() + else: + if not self._is_multiplicative: + if self._REGISTRY.autoconvert_offset_to_baseunit: + self.ito_base_units() + else: + raise OffsetUnitCalculusError(self._units) + + if getattr(other, "dimensionless", False): + other = other.to_base_units().magnitude + self._units **= other + elif not getattr(other, "dimensionless", True): + raise DimensionalityError(self._units, "dimensionless") + else: + self._units **= other + + self._magnitude **= _to_magnitude( + other, self.force_ndarray, self.force_ndarray_like + ) + return self + + @check_implemented + def __pow__(self, other) -> PlainQuantity[MagnitudeT]: + try: + _to_magnitude(other, self.force_ndarray, self.force_ndarray_like) + except PintTypeError: + raise + except TypeError: + return NotImplemented + else: + if not self._ok_for_muldiv: + raise OffsetUnitCalculusError(self._units) + + if is_duck_array_type(type(getattr(other, "_magnitude", other))): + # arrays are refused as exponent, because they would create + # len(array) quantities of len(set(array)) different units + # unless the plain is dimensionless. + # Note: this will strip Units of degrees or radians from PlainQuantity + if self.dimensionless: + if getattr(other, "dimensionless", False): + return self.__class__( + self._convert_magnitude_not_inplace(self.UnitsContainer()) + ** other.m_as("") + ) + elif not getattr(other, "dimensionless", True): + raise DimensionalityError(other._units, "dimensionless") + else: + return self.__class__( + self._convert_magnitude_not_inplace(self.UnitsContainer()) + ** other + ) + elif np.size(other) > 1: + raise DimensionalityError( + self._units, + "dimensionless", + extra_msg=". PlainQuantity array exponents are only allowed if the " + "plain is dimensionless", + ) + + new_self = self + if other == 1: + return self + elif other == 0: + exponent = 0 + units = self.UnitsContainer() + else: + if not self._is_multiplicative: + if self._REGISTRY.autoconvert_offset_to_baseunit: + new_self = self.to_root_units() + else: + raise OffsetUnitCalculusError(self._units) + + if getattr(other, "dimensionless", False): + exponent = other.to_root_units().magnitude + units = new_self._units**exponent + elif not getattr(other, "dimensionless", True): + raise DimensionalityError(other._units, "dimensionless") + else: + exponent = _to_magnitude( + other, force_ndarray=False, force_ndarray_like=False + ) + units = new_self._units**exponent + + magnitude = new_self._magnitude**exponent + return self.__class__(magnitude, units) + + @check_implemented + def __rpow__(self, other) -> PlainQuantity[MagnitudeT]: + try: + _to_magnitude(other, self.force_ndarray, self.force_ndarray_like) + except PintTypeError: + raise + except TypeError: + return NotImplemented + else: + if not self.dimensionless: + raise DimensionalityError(self._units, "dimensionless") + new_self = self.to_root_units() + return other**new_self._magnitude + + def __abs__(self) -> PlainQuantity[MagnitudeT]: + return self.__class__(abs(self._magnitude), self._units) + + def __round__(self, ndigits: int | None = 0) -> PlainQuantity[MagnitudeT]: + return self.__class__(round(self._magnitude, ndigits=ndigits), self._units) + + def __pos__(self) -> PlainQuantity[MagnitudeT]: + return self.__class__(operator.pos(self._magnitude), self._units) + + def __neg__(self) -> PlainQuantity[MagnitudeT]: + return self.__class__(operator.neg(self._magnitude), self._units) + + @check_implemented + def __eq__(self, other): + def bool_result(value): + nonlocal other + + if not is_duck_array_type(type(self._magnitude)): + return value + + if isinstance(other, PlainQuantity): + other = other._magnitude + + template, _ = np.broadcast_arrays(self._magnitude, other) + return np.full_like(template, fill_value=value, dtype=np.bool_) + + # We compare to the plain class of PlainQuantity because + # each PlainQuantity class is unique. + if not isinstance(other, PlainQuantity): + if other is None: + # A loop in pandas-dev/pandas/core/common.py(86)consensus_name_attr() can result in OTHER being None + return bool_result(False) + if zero_or_nan(other, True): + # Handle the special case in which we compare to zero or NaN + # (or an array of zeros or NaNs) + if self._is_multiplicative: + # compare magnitude + return eq(self._magnitude, other, False) + else: + # compare the magnitude after converting the + # non-multiplicative quantity to plain units + if self._REGISTRY.autoconvert_offset_to_baseunit: + return eq(self.to_base_units()._magnitude, other, False) + else: + raise OffsetUnitCalculusError(self._units) + + if self.dimensionless: + return eq( + self._convert_magnitude_not_inplace(self.UnitsContainer()), + other, + False, + ) + + return bool_result(False) + + # TODO: this might be expensive. Do we even need it? + if eq(self._magnitude, 0, True) and eq(other._magnitude, 0, True): + return bool_result(self.dimensionality == other.dimensionality) + + if self._units == other._units: + return eq(self._magnitude, other._magnitude, False) + + try: + return eq( + self._convert_magnitude_not_inplace(other._units), + other._magnitude, + False, + ) + except DimensionalityError: + return bool_result(False) + + @check_implemented + def __ne__(self, other): + out = self.__eq__(other) + if is_duck_array_type(type(out)): + return np.logical_not(out) + return not out + + @check_implemented + def compare(self, other, op): + if not isinstance(other, PlainQuantity): + if self.dimensionless: + return op( + self._convert_magnitude_not_inplace(self.UnitsContainer()), other + ) + elif zero_or_nan(other, True): + # Handle the special case in which we compare to zero or NaN + # (or an array of zeros or NaNs) + if self._is_multiplicative: + # compare magnitude + return op(self._magnitude, other) + else: + # compare the magnitude after converting the + # non-multiplicative quantity to plain units + if self._REGISTRY.autoconvert_offset_to_baseunit: + return op(self.to_base_units()._magnitude, other) + else: + raise OffsetUnitCalculusError(self._units) + else: + raise ValueError(f"Cannot compare PlainQuantity and {type(other)}") + + # Registry equality check based on util.SharedRegistryObject + if self._REGISTRY is not other._REGISTRY: + mess = "Cannot operate with {} and {} of different registries." + raise ValueError( + mess.format(self.__class__.__name__, other.__class__.__name__) + ) + + if self._units == other._units: + return op(self._magnitude, other._magnitude) + if self.dimensionality != other.dimensionality: + raise DimensionalityError( + self._units, other._units, self.dimensionality, other.dimensionality + ) + return op(self.to_root_units().magnitude, other.to_root_units().magnitude) + + __lt__ = lambda self, other: self.compare(other, op=operator.lt) + __le__ = lambda self, other: self.compare(other, op=operator.le) + __ge__ = lambda self, other: self.compare(other, op=operator.ge) + __gt__ = lambda self, other: self.compare(other, op=operator.gt) + + def __bool__(self) -> bool: + # Only cast when non-ambiguous (when multiplicative unit) + if self._is_multiplicative: + return bool(self._magnitude) + else: + raise ValueError( + "Boolean value of PlainQuantity with offset unit is ambiguous." + ) + + __nonzero__ = __bool__ + + def tolist(self): + units = self._units + + try: + values = self._magnitude.tolist() + if not isinstance(values, list): + return self.__class__(values, units) + + return [ + ( + self.__class__(value, units).tolist() + if isinstance(value, list) + else self.__class__(value, units) + ) + for value in self._magnitude.tolist() + ] + except AttributeError: + raise AttributeError( + f"Magnitude '{type(self._magnitude).__name__}' does not support tolist." + ) + + def _get_unit_definition(self, unit: str) -> UnitDefinition: + try: + return self._REGISTRY._units[unit] + except KeyError: + # pint#1062: The __init__ method of this object added the unit to + # UnitRegistry._units (e.g. units with prefix are added on the fly the + # first time they're used) but the key was later removed, e.g. because + # a Context with unit redefinitions was deactivated. + self._REGISTRY.parse_units(unit) + return self._REGISTRY._units[unit] + + # methods/properties that help for math operations with offset units + @property + def _is_multiplicative(self) -> bool: + """Check if the PlainQuantity object has only multiplicative units.""" + return True + + def _get_non_multiplicative_units(self) -> list[str]: + """Return a list of the of non-multiplicative units of the PlainQuantity object.""" + return [] + + def _get_delta_units(self) -> list[str]: + """Return list of delta units ot the PlainQuantity object.""" + return [u for u in self._units if u.startswith("delta_")] + + def _has_compatible_delta(self, unit: str) -> bool: + """ "Check if PlainQuantity object has a delta_unit that is compatible with unit""" + return False + + def _ok_for_muldiv(self, no_offset_units=None) -> bool: + return True + + def to_timedelta(self: PlainQuantity[MagnitudeT]) -> datetime.timedelta: + return datetime.timedelta(microseconds=self.to("microseconds").magnitude) + + # We put this last to avoid overriding UnitsContainer + # and I do not want to rename it. + # TODO: Maybe in the future we need to change it to a more meaningful + # non-colliding name. + + @property + def UnitsContainer(self) -> Callable[..., UnitsContainerT]: + return self._REGISTRY.UnitsContainer diff --git a/datasette/vendored/pint/facets/plain/registry.py b/datasette/vendored/pint/facets/plain/registry.py new file mode 100644 index 0000000000..be70a2ca8c --- /dev/null +++ b/datasette/vendored/pint/facets/plain/registry.py @@ -0,0 +1,1424 @@ +""" + pint.facets.plain.registry + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. + + The registry contains the following important methods: + + - parse_unit_name: Parse a unit to identify prefix, unit name and suffix + by walking the list of prefix and suffix. + Result is cached: NO + - parse_units: Parse a units expression and returns a UnitContainer with + the canonical names. + The expression can only contain products, ratios and powers of units; + prefixed units and pluralized units. + Result is cached: YES + - parse_expression: Parse a mathematical expression including units and + return a quantity object. + Result is cached: NO + +""" + +from __future__ import annotations + +import copy +import functools +import inspect +import itertools +import pathlib +import re +from collections import defaultdict +from collections.abc import Callable, Generator, Iterable, Iterator +from decimal import Decimal +from fractions import Fraction +from token import NAME, NUMBER +from tokenize import TokenInfo +from typing import ( + TYPE_CHECKING, + Any, + Generic, + TypeVar, + Union, +) + +if TYPE_CHECKING: + from ...compat import Locale + from ..context import Context + + # from ..._typing import Quantity, Unit + +import platformdirs + +from ... import pint_eval +from ..._typing import ( + Handler, + QuantityArgument, + QuantityOrUnitLike, + Scalar, + UnitLike, +) +from ...compat import Self, TypeAlias, deprecated +from ...errors import ( + DimensionalityError, + OffsetUnitCalculusError, + RedefinitionError, + UndefinedUnitError, +) +from ...pint_eval import build_eval_tree +from ...util import ( + ParserHelper, + _is_dim, + create_class_with_registry, + getattr_maybe_raise, + logger, + solve_dependencies, + string_preprocessor, + to_units_container, +) +from ...util import UnitsContainer as UnitsContainer +from .definitions import ( + AliasDefinition, + CommentDefinition, + DefaultsDefinition, + DerivedDimensionDefinition, + DimensionDefinition, + NamedDefinition, + PrefixDefinition, + UnitDefinition, +) +from .objects import PlainQuantity, PlainUnit + +T = TypeVar("T") + +_BLOCK_RE = re.compile(r"[ (]") + + +@functools.lru_cache +def pattern_to_regex(pattern: str | re.Pattern[str]) -> re.Pattern[str]: + # TODO: This has been changed during typing improvements. + # if hasattr(pattern, "finditer"): + if not isinstance(pattern, str): + pattern = pattern.pattern + + # Replace "{unit_name}" match string with float regex with unit_name as group + pattern = re.sub( + r"{(\w+)}", r"(?P<\1>[+-]?[0-9]+(?:.[0-9]+)?(?:[Ee][+-]?[0-9]+)?)", pattern + ) + + return re.compile(pattern) + + +NON_INT_TYPE = type[Union[float, Decimal, Fraction]] +PreprocessorType = Callable[[str], str] + + +class RegistryCache: + """Cache to speed up unit registries""" + + def __init__(self) -> None: + #: Maps dimensionality (UnitsContainer) to Units (str) + self.dimensional_equivalents: dict[UnitsContainer, frozenset[str]] = {} + + #: Maps dimensionality (UnitsContainer) to Dimensionality (UnitsContainer) + # TODO: this description is not right. + self.root_units: dict[UnitsContainer, tuple[Scalar, UnitsContainer]] = {} + + #: Maps dimensionality (UnitsContainer) to Units (UnitsContainer) + self.dimensionality: dict[UnitsContainer, UnitsContainer] = {} + + #: Cache the unit name associated to user input. ('mV' -> 'millivolt') + self.parse_unit: dict[str, UnitsContainer] = {} + + self.conversion_factor: dict[ + tuple[UnitsContainer, UnitsContainer], Scalar | DimensionalityError + ] = {} + + def __eq__(self, other: Any): + if not isinstance(other, self.__class__): + return False + attrs = ( + "dimensional_equivalents", + "root_units", + "dimensionality", + "parse_unit", + "conversion_factor", + ) + return all(getattr(self, attr) == getattr(other, attr) for attr in attrs) + + +class RegistryMeta(type): + """This is just to call after_init at the right time + instead of asking the developer to do it when subclassing. + """ + + def __call__(self, *args: Any, **kwargs: Any): + obj = super().__call__(*args, **kwargs) + obj._after_init() + return obj + + +# Generic types used to mark types associated to Registries. +QuantityT = TypeVar("QuantityT", bound=PlainQuantity[Any]) +UnitT = TypeVar("UnitT", bound=PlainUnit) + + +class GenericPlainRegistry(Generic[QuantityT, UnitT], metaclass=RegistryMeta): + """Base class for all registries. + + Capabilities: + + - Register units, prefixes, and dimensions, and their relations. + - Convert between units. + - Find dimensionality of a unit. + - Parse units with prefix and/or suffix. + - Parse expressions. + - Parse a definition file. + - Allow extending the definition file parser by registering @ directives. + + Parameters + ---------- + filename : str or None + path of the units definition file to load or line iterable object. Empty to load + the default definition file. None to leave the UnitRegistry empty. + force_ndarray : bool + convert any input, scalar or not to a numpy.ndarray. + force_ndarray_like : bool + convert all inputs other than duck arrays to a numpy.ndarray. + on_redefinition : str + action to take in case a unit is redefined: 'warn', 'raise', 'ignore' + auto_reduce_dimensions : + If True, reduce dimensionality on appropriate operations. + autoconvert_to_preferred : + If True, converts preferred units on appropriate operations. + preprocessors : + list of callables which are iteratively ran on any input expression or unit + string + fmt_locale : + locale identifier string, used in `format_babel` + non_int_type : type + numerical type used for non integer values. (Default: float) + case_sensitive : bool, optional + Control default case sensitivity of unit parsing. (Default: True) + cache_folder : str or pathlib.Path or None, optional + Specify the folder in which cache files are saved and loaded from. + If None, the cache is disabled. (default) + separate_format_defaults : bool, optional + Separate the default format into magnitude and unit formats as soon as + possible. The deprecated default is not to separate. This will change in a + future release. + """ + + Quantity: type[QuantityT] + Unit: type[UnitT] + + _diskcache = None + _def_parser = None + + def __init__( + self, + filename="", + force_ndarray: bool = False, + force_ndarray_like: bool = False, + on_redefinition: str = "warn", + auto_reduce_dimensions: bool = False, + autoconvert_to_preferred: bool = False, + preprocessors: list[PreprocessorType] | None = None, + fmt_locale: str | None = None, + non_int_type: NON_INT_TYPE = float, + case_sensitive: bool = True, + cache_folder: str | pathlib.Path | None = None, + separate_format_defaults: bool | None = None, + mpl_formatter: str = "{:P}", + ): + #: Map a definition class to a adder methods. + self._adders: Handler = {} + self._register_definition_adders() + self._init_dynamic_classes() + + if cache_folder == ":auto:": + cache_folder = platformdirs.user_cache_path(appname="pint", appauthor=False) + + from ... import delegates # TODO: change thiss + + if cache_folder is not None: + 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.formatter = delegates.Formatter(self) + 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 ")) + + # use a default preprocessor to support permille "‰" + self.preprocessors.insert(0, lambda string: string.replace("‰", " permille ")) + + #: mode used to fill in the format defaults + self.separate_format_defaults = separate_format_defaults + + #: Action to take in case a unit is redefined. 'warn', 'raise', 'ignore' + self._on_redefinition = on_redefinition + + #: Determines if dimensionality should be reduced on appropriate operations. + self.auto_reduce_dimensions = auto_reduce_dimensions + + #: Determines if units will be converted to preffered on appropriate operations. + self.autoconvert_to_preferred = autoconvert_to_preferred + + #: Default locale identifier string, used when calling format_babel without explicit locale. + self.formatter.set_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 + + #: Default unit case sensitivity + self.case_sensitive = case_sensitive + + #: Map between name (string) and value (string) of defaults stored in the + #: definitions file. + self._defaults: dict[str, str] = {} + + #: Map dimension name (string) to its definition (DimensionDefinition). + self._dimensions: dict[ + str, DimensionDefinition | DerivedDimensionDefinition + ] = {} + + #: Map unit name (string) to its definition (UnitDefinition). + #: Might contain prefixed units. + self._units: dict[str, UnitDefinition] = {} + + #: List base unit names + self._base_units: list[str] = [] + + #: Map unit name in lower case (string) to a set of unit names with the right + #: case. + #: Does not contain prefixed units. + #: e.g: 'hz' - > set('Hz', ) + self._units_casei: dict[str, set[str]] = defaultdict(set) + + #: Map prefix name (string) to its definition (PrefixDefinition). + 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": ""} + + #: Map contexts to RegistryCache + self._cache = RegistryCache() + + self._initialized = False + + 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) + + def _after_init(self) -> None: + """This should be called after all __init__""" + + if self._filename == "": + 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: + loaded_files = None + + self._build_cache(loaded_files) + self._initialized = True + + def _register_adder( + self, + definition_class: type[T], + adder_func: Callable[ + [ + T, + ], + None, + ], + ) -> None: + """Register a block definition.""" + self._adders[definition_class] = adder_func + + 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: Self, memo) -> type[Self]: + new = object.__new__(type(self)) + new.__dict__ = copy.deepcopy(self.__dict__, memo) + new._init_dynamic_classes() + return new + + def __getattr__(self, item: str) -> UnitT: + getattr_maybe_raise(self, item) + + # self.Unit will call parse_units + return self.Unit(item) + + def __getitem__(self, item: str) -> UnitT: + logger.warning( + "Calling the getitem method from a UnitRegistry is deprecated. " + "use `parse_expression` method or use the registry as a callable." + ) + return self.parse_expression(item) + + def __contains__(self, item: str) -> bool: + """Support checking prefixed units with the `in` operator""" + try: + self.__getattr__(item) + return True + except UndefinedUnitError: + return False + + def __dir__(self) -> list[str]: + #: Calling dir(registry) gives all units, methods, and attributes. + #: Also used for autocompletion in IPython. + return list(self._units.keys()) + list(object.__dir__(self)) + + def __iter__(self) -> Iterator[str]: + """Allows for listing all units in registry with `list(ureg)`. + + Returns + ------- + Iterator over names of all units in registry, ordered alphabetically. + """ + return iter(sorted(self._units.keys())) + + @property + @deprecated( + "This function will be removed in future versions of pint.\n" + "Use ureg.formatter.fmt_locale" + ) + def fmt_locale(self) -> Locale | None: + return self.formatter.locale + + @fmt_locale.setter + @deprecated( + "This function will be removed in future versions of pint.\n" + "Use ureg.formatter.set_locale" + ) + def fmt_locale(self, loc: str | None): + self.formatter.set_locale(loc) + + @deprecated( + "This function will be removed in future versions of pint.\n" + "Use ureg.formatter.set_locale" + ) + def set_fmt_locale(self, loc: str | None) -> None: + """Change the locale used by default by `format_babel`. + + Parameters + ---------- + loc : str or None + None` (do not translate), 'sys' (detect the system locale) or a locale id string. + """ + + self.formatter.set_locale(loc) + + @property + @deprecated( + "This function will be removed in future versions of pint.\n" + "Use ureg.formatter.default_format" + ) + def default_format(self) -> str: + """Default formatting string for quantities.""" + return self.formatter.default_format + + @default_format.setter + @deprecated( + "This function will be removed in future versions of pint.\n" + "Use ureg.formatter.default_format" + ) + def default_format(self, value: str) -> None: + self.formatter.default_format = value + + @property + def cache_folder(self) -> pathlib.Path | None: + if self._diskcache: + return self._diskcache.cache_folder + return None + + @property + def non_int_type(self): + return self._non_int_type + + def define(self, definition: str | type) -> None: + """Add unit to the registry. + + Parameters + ---------- + definition : str or Definition + a dimension, unit or prefix definition. + """ + + if isinstance(definition, str): + parsed_project = self._def_parser.parse_string(definition) + + for definition in self._def_parser.iter_parsed_project(parsed_project): + self._helper_dispatch_adder(definition) + else: + self._helper_dispatch_adder(definition) + + ############ + # 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: Any) -> None: + """Helper function to add a single definition, + choosing the appropiate method by class. + """ + for cls in inspect.getmro(definition.__class__): + if cls in self._adders: + adder_func = self._adders[cls] + break + else: + raise TypeError( + f"No loader function defined " f"for {definition.__class__.__name__}" + ) + + adder_func(definition) + + def _helper_adder( + self, + definition: NamedDefinition, + target_dict: dict[str, Any], + casei_target_dict: dict[str, Any] | None, + ) -> None: + """Helper function to store a definition in the internal dictionaries. + It stores the definition under its name, symbol and aliases. + """ + self._helper_single_adder( + definition.name, definition, target_dict, casei_target_dict + ) + + # TODO: Not sure why but using hasattr does not work here. + if getattr(definition, "has_symbol", ""): + self._helper_single_adder( + definition.symbol, definition, target_dict, casei_target_dict + ) + + for alias in getattr(definition, "aliases", ()): + if " " in alias: + logger.warn("Alias cannot contain a space: " + alias) + + self._helper_single_adder(alias, definition, target_dict, casei_target_dict) + + def _helper_single_adder( + self, + key: str, + value: NamedDefinition, + target_dict: dict[str, Any], + casei_target_dict: dict[str, Any] | None, + ) -> None: + """Helper function to store a definition in the internal dictionaries. + + It warns or raise error on redefinition. + """ + if key in target_dict: + if self._on_redefinition == "raise": + raise RedefinitionError(key, type(value)) + elif self._on_redefinition == "warn": + logger.warning(f"Redefining '{key}' ({type(value)})") + + target_dict[key] = value + if casei_target_dict is not None: + casei_target_dict[key.lower()].add(key) + + def _add_defaults(self, defaults_definition: DefaultsDefinition) -> None: + for k, v in defaults_definition.items(): + self._defaults[k] = v + + def _add_alias(self, definition: AliasDefinition) -> None: + 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._helper_single_adder(alias, unit, self._units, self._units_casei) + + def _add_dimension(self, definition: DimensionDefinition) -> None: + self._helper_adder(definition, self._dimensions, None) + + def _add_derived_dimension(self, definition: DerivedDimensionDefinition) -> None: + 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) -> None: + self._helper_adder(definition, self._prefixes, None) + + def _add_unit(self, definition: UnitDefinition) -> None: + 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: Iterable[str] | str | pathlib.Path, is_resource: bool = False + ): + """Add units and prefixes defined in a definition text file. + + Parameters + ---------- + file : + can be a filename or a line iterable. + is_resource : + used to indicate that the file is a resource file + and therefore should be loaded from the package. (Default value = False) + """ + + 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_project = self._def_parser.parse_file(file) + + for definition in self._def_parser.iter_parsed_project(parsed_project): + self._helper_dispatch_adder(definition) + + return parsed_project + + def _build_cache(self, loaded_files=None) -> None: + """Build a cache of dimensionality and plain units.""" + + diskcache = self._diskcache + if loaded_files and diskcache: + cache, cache_basename = diskcache.load(loaded_files, "build_cache") + if cache is None: + self._build_cache() + diskcache.save(self._cache, loaded_files, "build_cache") + return + + self._cache = RegistryCache() + + deps: dict[str, set[str]] = { + name: set(definition.reference.keys()) if definition.reference else set() + for name, definition in self._units.items() + } + + for unit_names in solve_dependencies(deps): + for unit_name in unit_names: + if "[" in unit_name: + continue + parsed_names = self.parse_unit_name(unit_name) + if parsed_names: + prefix, base_name, _ = parsed_names[0] + else: + prefix, base_name = "", unit_name + + try: + uc = ParserHelper.from_word(base_name, self.non_int_type) + + bu = self._get_root_units(uc) + di = self._get_dimensionality(uc) + + self._cache.root_units[uc] = bu + self._cache.dimensionality[uc] = di + + if not prefix: + dimeq_set = self._cache.dimensional_equivalents.setdefault( + di, set() + ) + dimeq_set.add(self._units[base_name].name) + + except Exception as exc: + logger.warning(f"Could not resolve {unit_name}: {exc!r}") + return self._cache + + def get_name(self, name_or_alias: str, case_sensitive: bool | None = None) -> str: + """Return the canonical name of a unit.""" + + if name_or_alias == "dimensionless": + return "" + + try: + return self._units[name_or_alias].name + except KeyError: + pass + + candidates = self.parse_unit_name(name_or_alias, case_sensitive) + if not candidates: + raise UndefinedUnitError(name_or_alias) + + prefix, unit_name, _ = candidates[0] + if len(candidates) > 1: + logger.warning( + f"Parsing {name_or_alias} yield multiple results. Options are: {candidates!r}" + ) + + if prefix: + if not self._units[unit_name].is_multiplicative: + raise OffsetUnitCalculusError( + "Prefixing a unit requires multiplying the unit." + ) + + name = prefix + unit_name + symbol = self.get_symbol(name, case_sensitive) + prefix_def = self._prefixes[prefix] + self._units[name] = UnitDefinition( + name, + symbol, + tuple(), + prefix_def.converter, + self.UnitsContainer({unit_name: 1}), + ) + return prefix + unit_name + + return unit_name + + def get_symbol(self, name_or_alias: str, case_sensitive: bool | None = None) -> str: + """Return the preferred alias for a unit.""" + candidates = self.parse_unit_name(name_or_alias, case_sensitive) + if not candidates: + raise UndefinedUnitError(name_or_alias) + + prefix, unit_name, _ = candidates[0] + if len(candidates) > 1: + logger.warning( + f"Parsing {name_or_alias} yield multiple results. Options are: {candidates!r}" + ) + + return self._prefixes[prefix].symbol + self._units[unit_name].symbol + + def _get_symbol(self, name: str) -> str: + return self._units[name].symbol + + def get_dimensionality(self, input_units: UnitLike) -> UnitsContainer: + """Convert unit or dict of units or dimensions to a dict of plain dimensions + dimensions + """ + + # TODO: This should be to_units_container(input_units, self) + # but this tries to reparse and fail for dimensions. + input_units = to_units_container(input_units) + + return self._get_dimensionality(input_units) + + def _get_dimensionality(self, input_units: UnitsContainer | None) -> UnitsContainer: + """Convert a UnitsContainer to plain dimensions.""" + if not input_units: + return self.UnitsContainer() + + cache = self._cache.dimensionality + + try: + return cache[input_units] + except KeyError: + pass + + accumulator: dict[str, int] = defaultdict(int) + self._get_dimensionality_recurse(input_units, 1, accumulator) + + if "[]" in accumulator: + del accumulator["[]"] + + dims = self.UnitsContainer({k: v for k, v in accumulator.items() if v != 0}) + + cache[input_units] = dims + + return dims + + def _get_dimensionality_recurse( + self, ref: UnitsContainer, exp: Scalar, accumulator: dict[str, int] + ) -> None: + for key in ref: + exp2 = exp * ref[key] + if _is_dim(key): + try: + reg = self._dimensions[key] + except KeyError: + raise ValueError( + f"{key} is not defined as dimension in the pint UnitRegistry" + ) + if isinstance(reg, DerivedDimensionDefinition): + self._get_dimensionality_recurse(reg.reference, exp2, accumulator) + else: + # DimensionDefinition. + accumulator[key] += exp2 + + else: + reg = self._units[self.get_name(key)] + if reg.reference is not None: + self._get_dimensionality_recurse(reg.reference, exp2, accumulator) + + def _get_dimensionality_ratio( + self, unit1: UnitLike, unit2: UnitLike + ) -> Scalar | None: + """Get the exponential ratio between two units, i.e. solve unit2 = unit1**x for x. + + Parameters + ---------- + unit1 : UnitsContainer compatible (str, Unit, UnitsContainer, dict) + first unit + unit2 : UnitsContainer compatible (str, Unit, UnitsContainer, dict) + second unit + + Returns + ------- + number or None + exponential proportionality or None if the units cannot be converted + + """ + # shortcut in case of equal units + if unit1 == unit2: + return 1 + + dim1, dim2 = (self.get_dimensionality(unit) for unit in (unit1, unit2)) + if dim1 == dim2: + return 1 + elif not dim1 or not dim2 or dim1.keys() != dim2.keys(): # not comparable + return None + + ratios = (dim2[key] / val for key, val in dim1.items()) + first = next(ratios) + if all(r == first for r in ratios): # all are same, we're good + return first + return None + + def get_root_units( + self, input_units: UnitLike, check_nonmult: bool = True + ) -> tuple[Scalar, UnitT]: + """Convert unit or dict of units to the root units. + + If any unit is non multiplicative and check_converter is True, + then None is returned as the multiplicative factor. + + Parameters + ---------- + input_units : UnitsContainer or str + units + check_nonmult : bool + if True, None will be returned as the + multiplicative factor if a non-multiplicative + units is found in the final Units. (Default value = True) + + Returns + ------- + Number, pint.Unit + multiplicative factor, plain units + + """ + input_units = to_units_container(input_units, self) + + f, units = self._get_root_units(input_units, check_nonmult) + + return f, self.Unit(units) + + def _get_conversion_factor( + self, src: UnitsContainer, dst: UnitsContainer + ) -> Scalar | DimensionalityError: + """Get conversion factor in non-multiplicative units. + + Parameters + ---------- + src + Source units + dst + Target units + + Returns + ------- + Conversion factor or DimensionalityError + """ + cache = self._cache.conversion_factor + try: + return cache[(src, dst)] + except KeyError: + pass + + src_dim = self._get_dimensionality(src) + dst_dim = self._get_dimensionality(dst) + + # If the source and destination dimensionality are different, + # then the conversion cannot be performed. + if src_dim != dst_dim: + return DimensionalityError(src, dst, src_dim, dst_dim) + + # Here src and dst have only multiplicative units left. Thus we can + # convert with a factor. + factor, _ = self._get_root_units(src / dst) + + cache[(src, dst)] = factor + return factor + + def _get_root_units( + self, input_units: UnitsContainer, check_nonmult: bool = True + ) -> tuple[Scalar, UnitsContainer]: + """Convert unit or dict of units to the root units. + + If any unit is non multiplicative and check_converter is True, + then None is returned as the multiplicative factor. + + Parameters + ---------- + input_units : UnitsContainer or dict + units + check_nonmult : bool + if True, None will be returned as the + multiplicative factor if a non-multiplicative + units is found in the final Units. (Default value = True) + + Returns + ------- + number, Unit + multiplicative factor, plain units + + """ + if not input_units: + return 1, self.UnitsContainer() + + cache = self._cache.root_units + try: + return cache[input_units] + except KeyError: + pass + + accumulators: dict[str | None, int] = defaultdict(int) + accumulators[None] = 1 + self._get_root_units_recurse(input_units, 1, accumulators) + + factor = accumulators[None] + units = self.UnitsContainer( + {k: v for k, v in accumulators.items() if k is not None and v != 0} + ) + + # Check if any of the final units is non multiplicative and return None instead. + if check_nonmult: + if any(not self._units[unit].converter.is_multiplicative for unit in units): + factor = None + + cache[input_units] = factor, units + return factor, units + + def get_base_units( + self, + input_units: UnitsContainer | str, + check_nonmult: bool = True, + system=None, + ) -> tuple[Scalar, UnitT]: + """Convert unit or dict of units to the plain units. + + If any unit is non multiplicative and check_converter is True, + then None is returned as the multiplicative factor. + + Parameters + ---------- + input_units : UnitsContainer or str + units + check_nonmult : bool + If True, None will be returned as the multiplicative factor if + non-multiplicative units are found in the final Units. + (Default value = True) + system : + (Default value = None) + + Returns + ------- + Number, pint.Unit + multiplicative factor, plain units + + """ + + return self.get_root_units(input_units, check_nonmult) + + # TODO: accumulators breaks typing list[int, dict[str, int]] + # So we have changed the behavior here + def _get_root_units_recurse( + self, ref: UnitsContainer, exp: Scalar, accumulators: dict[str | None, int] + ) -> None: + """ + + accumulators None keeps the scalar prefactor not associated with a specific unit. + + """ + for key in ref: + exp2 = exp * ref[key] + key = self.get_name(key) + reg = self._units[key] + if reg.is_base: + accumulators[key] += exp2 + else: + accumulators[None] *= reg.converter.scale**exp2 + if reg.reference is not None: + self._get_root_units_recurse(reg.reference, exp2, accumulators) + + def get_compatible_units(self, input_units: QuantityOrUnitLike) -> frozenset[UnitT]: + """ """ + input_units = to_units_container(input_units) + + equiv = self._get_compatible_units(input_units) + + return frozenset(self.Unit(eq) for eq in equiv) + + def _get_compatible_units( + self, input_units: UnitsContainer, *args, **kwargs + ) -> frozenset[str]: + """ """ + if not input_units: + return frozenset() + + src_dim = self._get_dimensionality(input_units) + return self._cache.dimensional_equivalents.setdefault(src_dim, frozenset()) + + # TODO: remove context from here + def is_compatible_with( + self, obj1: Any, obj2: Any, *contexts: str | Context, **ctx_kwargs + ) -> bool: + """check if the other object is compatible + + Parameters + ---------- + obj1, obj2 + The objects to check against each other. Treated as + dimensionless if not a Quantity, Unit or str. + *contexts : str or pint.Context + Contexts to use in the transformation. + **ctx_kwargs : + Values for the Context/s + + Returns + ------- + bool + """ + if isinstance(obj1, (self.Quantity, self.Unit)): + return obj1.is_compatible_with(obj2, *contexts, **ctx_kwargs) + + if isinstance(obj1, str): + return self.parse_expression(obj1).is_compatible_with( + obj2, *contexts, **ctx_kwargs + ) + + return not isinstance(obj2, (self.Quantity, self.Unit)) + + def convert( + self, + value: T, + src: QuantityOrUnitLike, + dst: QuantityOrUnitLike, + inplace: bool = False, + ) -> T: + """Convert value from some source to destination units. + + Parameters + ---------- + value : + value + src : pint.Quantity or str + source units. + dst : pint.Quantity or str + destination units. + inplace : + (Default value = False) + + Returns + ------- + type + converted value + + """ + src = to_units_container(src, self) + + dst = to_units_container(dst, self) + + if src == dst: + return value + + return self._convert(value, src, dst, inplace) + + def _convert( + self, + value: T, + src: UnitsContainer, + dst: UnitsContainer, + inplace: bool = False, + check_dimensionality: bool = True, + ) -> T: + """Convert value from some source to destination units. + + Parameters + ---------- + value : + value + src : UnitsContainer + source units. + dst : UnitsContainer + destination units. + inplace : + (Default value = False) + check_dimensionality : + (Default value = True) + + Returns + ------- + type + converted value + + """ + + factor = self._get_conversion_factor(src, dst) + + if isinstance(factor, DimensionalityError): + raise factor + + # factor is type float and if our magnitude is type Decimal then + # must first convert to Decimal before we can '*' the values + if isinstance(value, Decimal): + factor = Decimal(str(factor)) + elif isinstance(value, Fraction): + factor = Fraction(str(factor)) + + if inplace: + value *= factor + else: + value = value * factor + + return value + + def parse_unit_name( + self, unit_name: str, case_sensitive: bool | None = None + ) -> tuple[tuple[str, str, str], ...]: + """Parse a unit to identify prefix, unit name and suffix + by walking the list of prefix and suffix. + In case of equivalent combinations (e.g. ('kilo', 'gram', '') and + ('', 'kilogram', ''), prefer those with prefix. + + Parameters + ---------- + unit_name : + + case_sensitive : bool or None + Control if unit lookup is case sensitive. Defaults to None, which uses the + registry's case_sensitive setting + + Returns + ------- + tuple of tuples (str, str, str) + all non-equivalent combinations of (prefix, unit name, suffix) + """ + + case_sensitive = ( + self.case_sensitive if case_sensitive is None else case_sensitive + ) + return self._dedup_candidates( + self._yield_unit_triplets(unit_name, case_sensitive) + ) + + def _yield_unit_triplets( + self, unit_name: str, case_sensitive: bool + ) -> Generator[tuple[str, str, str], None, None]: + """Helper of parse_unit_name.""" + + stw = unit_name.startswith + edw = unit_name.endswith + for suffix, prefix in itertools.product(self._suffixes, self._prefixes): + if stw(prefix) and edw(suffix): + name = unit_name[len(prefix) :] + if suffix: + name = name[: -len(suffix)] + if len(name) == 1: + continue + if case_sensitive: + if name in self._units: + yield ( + self._prefixes[prefix].name, + self._units[name].name, + self._suffixes[suffix], + ) + else: + for real_name in self._units_casei.get(name.lower(), ()): + yield ( + self._prefixes[prefix].name, + self._units[real_name].name, + self._suffixes[suffix], + ) + + # TODO: keep this for backward compatibility + _parse_unit_name = _yield_unit_triplets + + @staticmethod + def _dedup_candidates( + candidates: Iterable[tuple[str, str, str]], + ) -> tuple[tuple[str, str, str], ...]: + """Helper of parse_unit_name. + + Given an iterable of unit triplets (prefix, name, suffix), remove those with + different names but equal value, preferring those with a prefix. + + e.g. ('kilo', 'gram', '') and ('', 'kilogram', '') + """ + candidates = dict.fromkeys(candidates) # ordered set + for cp, cu, cs in list(candidates): + assert isinstance(cp, str) + assert isinstance(cu, str) + if cs != "": + raise NotImplementedError("non-empty suffix") + if cp: + candidates.pop(("", cp + cu, ""), None) + return tuple(candidates) + + def parse_units( + self, + input_string: str, + as_delta: bool | None = None, + case_sensitive: bool | None = None, + ) -> UnitT: + """Parse a units expression and returns a UnitContainer with + the canonical names. + + The expression can only contain products, ratios and powers of units. + + Parameters + ---------- + input_string : str + as_delta : bool or None + if the expression has multiple units, the parser will + interpret non multiplicative units as their `delta_` counterparts. (Default value = None) + case_sensitive : bool or None + Control if unit parsing is case sensitive. Defaults to None, which uses the + registry's setting. + + Returns + ------- + pint.Unit + + """ + + return self.Unit( + self.parse_units_as_container(input_string, as_delta, case_sensitive) + ) + + def parse_units_as_container( + self, + input_string: str, + as_delta: bool | None = None, + case_sensitive: bool | None = None, + ) -> UnitsContainer: + as_delta = ( + as_delta if as_delta is not None else True + ) # TODO This only exists in nonmultiplicative + case_sensitive = ( + case_sensitive if case_sensitive is not None else self.case_sensitive + ) + return self._parse_units_as_container(input_string, as_delta, case_sensitive) + + def _parse_units_as_container( + self, + input_string: str, + as_delta: bool = True, + case_sensitive: bool = True, + ) -> UnitsContainer: + """Parse a units expression and returns a UnitContainer with + the canonical names. + """ + + cache = self._cache.parse_unit + # Issue #1097: it is possible, when a unit was defined while a different context + # was active, that the unit is in self._cache.parse_unit but not in self._units. + # If this is the case, force self._units to be repopulated. + if as_delta and input_string in cache and input_string in self._units: + return cache[input_string] + + for p in self.preprocessors: + input_string = p(input_string) + + if not input_string: + return self.UnitsContainer() + + # Sanitize input_string with whitespaces. + input_string = input_string.strip() + + units = ParserHelper.from_string(input_string, self.non_int_type) + if units.scale != 1: + raise ValueError("Unit expression cannot have a scaling factor.") + + ret = self.UnitsContainer({}) + many = len(units) > 1 + for name in units: + cname = self.get_name(name, case_sensitive=case_sensitive) + value = units[name] + if not cname: + continue + if as_delta and (many or (not many and value != 1)): + definition = self._units[cname] + if not definition.is_multiplicative: + cname = "delta_" + cname + ret = ret.add(cname, value) + + if as_delta: + cache[input_string] = ret + + return ret + + def _eval_token( + self, + token: TokenInfo, + case_sensitive: bool | None = None, + **values: QuantityArgument, + ): + """Evaluate a single token using the following rules: + + 1. numerical values as strings are replaced by their numeric counterparts + - integers are parsed as integers + - other numeric values are parses of non_int_type + 2. strings in (inf, infinity, nan, dimensionless) with their numerical value. + 3. strings in values.keys() are replaced by Quantity(values[key]) + 4. in other cases, the values are parsed as units and replaced by their canonical name. + + Parameters + ---------- + token + Token to evaluate. + case_sensitive, optional + If true, a case sensitive matching of the unit name will be done in the registry. + If false, a case INsensitive matching of the unit name will be done in the registry. + (Default value = None, which uses registry setting) + **values + Other string that will be parsed using the Quantity constructor on their corresponding value. + """ + token_type = token[0] + token_text = token[1] + if token_type == NAME: + if token_text == "dimensionless": + return self.Quantity(1) + elif token_text.lower() in ("inf", "infinity"): + return self.non_int_type("inf") + elif token_text.lower() == "nan": + return self.non_int_type("nan") + elif token_text in values: + return self.Quantity(values[token_text]) + else: + return self.Quantity( + 1, + self.UnitsContainer( + {self.get_name(token_text, case_sensitive=case_sensitive): 1} + ), + ) + elif token_type == NUMBER: + return ParserHelper.eval_token(token, non_int_type=self.non_int_type) + else: + raise Exception("unknown token type") + + def parse_pattern( + self, + input_string: str, + pattern: str, + case_sensitive: bool | None = None, + many: bool = False, + ) -> list[str] | str | None: + """Parse a string with a given regex pattern and returns result. + + Parameters + ---------- + input_string + + pattern_string: + The regex parse string + case_sensitive, optional + If true, a case sensitive matching of the unit name will be done in the registry. + If false, a case INsensitive matching of the unit name will be done in the registry. + (Default value = None, which uses registry setting) + many, optional + Match many results + (Default value = False) + """ + + if not input_string: + return [] if many else None + + # Parse string + regex = pattern_to_regex(pattern) + matched = re.finditer(regex, input_string) + + # Extract result(s) + results = [] + for match in matched: + # Extract units from result + match = match.groupdict() + + # Parse units + units = [ + float(value) * self.parse_expression(unit, case_sensitive) + for unit, value in match.items() + ] + + # Add to results + results.append(units) + + # Return first match only + if not many: + return results[0] + + return results + + def parse_expression( + self: Self, + input_string: str, + case_sensitive: bool | None = None, + **values: QuantityArgument, + ) -> QuantityT: + """Parse a mathematical expression including units and return a quantity object. + + Numerical constants can be specified as keyword arguments and will take precedence + over the names defined in the registry. + + Parameters + ---------- + input_string + + case_sensitive, optional + If true, a case sensitive matching of the unit name will be done in the registry. + If false, a case INsensitive matching of the unit name will be done in the registry. + (Default value = None, which uses registry setting) + **values + Other string that will be parsed using the Quantity constructor on their corresponding value. + """ + if not input_string: + return self.Quantity(1) + + for p in self.preprocessors: + input_string = p(input_string) + input_string = string_preprocessor(input_string) + gen = pint_eval.tokenizer(input_string) + + def _define_op(s: str): + return self._eval_token(s, case_sensitive=case_sensitive, **values) + + return build_eval_tree(gen).evaluate(_define_op) + + # We put this last to avoid overriding UnitsContainer + # and I do not want to rename it. + # TODO: Maybe in the future we need to change it to a more meaningful + # non-colliding name. + def UnitsContainer(self, *args: Any, **kwargs: Any) -> UnitsContainer: + return UnitsContainer(*args, non_int_type=self.non_int_type, **kwargs) + + __call__ = parse_expression + + +class PlainRegistry(GenericPlainRegistry[PlainQuantity[Any], PlainUnit]): + Quantity: TypeAlias = PlainQuantity[Any] + Unit: TypeAlias = PlainUnit diff --git a/datasette/vendored/pint/facets/plain/unit.py b/datasette/vendored/pint/facets/plain/unit.py new file mode 100644 index 0000000000..d491a731fb --- /dev/null +++ b/datasette/vendored/pint/facets/plain/unit.py @@ -0,0 +1,289 @@ +""" + pint.facets.plain.unit + ~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2016 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import copy +import locale +import operator +from numbers import Number +from typing import TYPE_CHECKING, Any + +from ..._typing import UnitLike +from ...compat import NUMERIC_TYPES, deprecated +from ...errors import DimensionalityError +from ...util import PrettyIPython, SharedRegistryObject, UnitsContainer +from .definitions import UnitDefinition + +if TYPE_CHECKING: + from ..context import Context + + +class PlainUnit(PrettyIPython, SharedRegistryObject): + """Implements a class to describe a unit supporting math operations.""" + + def __reduce__(self): + # See notes in Quantity.__reduce__ + from datasette.vendored.pint import _unpickle_unit + + return _unpickle_unit, (PlainUnit, self._units) + + def __init__(self, units: UnitLike) -> None: + super().__init__() + if isinstance(units, (UnitsContainer, UnitDefinition)): + self._units = units + elif isinstance(units, str): + self._units = self._REGISTRY.parse_units(units)._units + elif isinstance(units, PlainUnit): + self._units = units._units + else: + raise TypeError( + "units must be of type str, Unit or " + "UnitsContainer; not {}.".format(type(units)) + ) + + def __copy__(self) -> PlainUnit: + ret = self.__class__(self._units) + return ret + + def __deepcopy__(self, memo) -> PlainUnit: + ret = self.__class__(copy.deepcopy(self._units, memo)) + return ret + + @deprecated( + "This function will be removed in future versions of pint.\n" + "Use ureg.formatter.format_unit_babel" + ) + def format_babel(self, spec: str = "", **kwspec: Any) -> str: + return self._REGISTRY.formatter.format_unit_babel(self, spec, **kwspec) + + def __format__(self, spec: str) -> str: + return self._REGISTRY.formatter.format_unit(self, spec) + + def __str__(self) -> str: + return self._REGISTRY.formatter.format_unit(self) + + def __bytes__(self) -> bytes: + return str(self).encode(locale.getpreferredencoding()) + + def __repr__(self) -> str: + return f"" + + @property + def dimensionless(self) -> bool: + """Return True if the PlainUnit is dimensionless; False otherwise.""" + return not bool(self.dimensionality) + + @property + def dimensionality(self) -> UnitsContainer: + """ + Returns + ------- + dict + Dimensionality of the PlainUnit, e.g. ``{length: 1, time: -1}`` + """ + try: + return self._dimensionality + except AttributeError: + dim = self._REGISTRY._get_dimensionality(self._units) + self._dimensionality = dim + + return self._dimensionality + + def compatible_units(self, *contexts): + if contexts: + with self._REGISTRY.context(*contexts): + return self._REGISTRY.get_compatible_units(self) + + return self._REGISTRY.get_compatible_units(self) + + def is_compatible_with( + self, other: Any, *contexts: str | Context, **ctx_kwargs: Any + ) -> bool: + """check if the other object is compatible + + Parameters + ---------- + other + The object to check. Treated as dimensionless if not a + Quantity, PlainUnit or str. + *contexts : str or pint.Context + Contexts to use in the transformation. + **ctx_kwargs : + Values for the Context/s + + Returns + ------- + bool + """ + from .quantity import PlainQuantity + + if contexts or self._REGISTRY._active_ctx: + try: + (1 * self).to(other, *contexts, **ctx_kwargs) + return True + except DimensionalityError: + return False + + if isinstance(other, (PlainQuantity, PlainUnit)): + return self.dimensionality == other.dimensionality + + if isinstance(other, str): + return ( + self.dimensionality == self._REGISTRY.parse_units(other).dimensionality + ) + + return self.dimensionless + + def __mul__(self, other): + if self._check(other): + if isinstance(other, self.__class__): + return self.__class__(self._units * other._units) + else: + qself = self._REGISTRY.Quantity(1, self._units) + return qself * other + + if isinstance(other, Number) and other == 1: + return self._REGISTRY.Quantity(other, self._units) + + return self._REGISTRY.Quantity(1, self._units) * other + + __rmul__ = __mul__ + + def __truediv__(self, other): + if self._check(other): + if isinstance(other, self.__class__): + return self.__class__(self._units / other._units) + else: + qself = 1 * self + return qself / other + + return self._REGISTRY.Quantity(1 / other, self._units) + + def __rtruediv__(self, other): + # As PlainUnit and Quantity both handle truediv with each other rtruediv can + # only be called for something different. + if isinstance(other, NUMERIC_TYPES): + return self._REGISTRY.Quantity(other, 1 / self._units) + elif isinstance(other, UnitsContainer): + return self.__class__(other / self._units) + + return NotImplemented + + __div__ = __truediv__ + __rdiv__ = __rtruediv__ + + def __pow__(self, other) -> PlainUnit: + if isinstance(other, NUMERIC_TYPES): + return self.__class__(self._units**other) + + else: + mess = f"Cannot power PlainUnit by {type(other)}" + raise TypeError(mess) + + def __hash__(self) -> int: + return self._units.__hash__() + + def __eq__(self, other) -> bool: + # We compare to the plain class of PlainUnit because each PlainUnit class is + # unique. + if self._check(other): + if isinstance(other, self.__class__): + return self._units == other._units + else: + return other == self._REGISTRY.Quantity(1, self._units) + + elif isinstance(other, NUMERIC_TYPES): + return other == self._REGISTRY.Quantity(1, self._units) + + else: + return self._units == other + + def __ne__(self, other) -> bool: + return not (self == other) + + def compare(self, other, op) -> bool: + self_q = self._REGISTRY.Quantity(1, self) + + if isinstance(other, NUMERIC_TYPES): + return self_q.compare(other, op) + elif isinstance(other, (PlainUnit, UnitsContainer, dict)): + return self_q.compare(self._REGISTRY.Quantity(1, other), op) + + return NotImplemented + + __lt__ = lambda self, other: self.compare(other, op=operator.lt) + __le__ = lambda self, other: self.compare(other, op=operator.le) + __ge__ = lambda self, other: self.compare(other, op=operator.ge) + __gt__ = lambda self, other: self.compare(other, op=operator.gt) + + def __int__(self) -> int: + return int(self._REGISTRY.Quantity(1, self._units)) + + def __float__(self) -> float: + return float(self._REGISTRY.Quantity(1, self._units)) + + def __complex__(self) -> complex: + return complex(self._REGISTRY.Quantity(1, self._units)) + + @property + def systems(self): + out = set() + for uname in self._units.keys(): + for sname, sys in self._REGISTRY._systems.items(): + if uname in sys.members: + out.add(sname) + return frozenset(out) + + def from_(self, value, strict=True, name="value"): + """Converts a numerical value or quantity to this unit + + Parameters + ---------- + value : + a Quantity (or numerical value if strict=False) to convert + strict : + boolean to indicate that only quantities are accepted (Default value = True) + name : + descriptive name to use if an exception occurs (Default value = "value") + + Returns + ------- + type + The converted value as this unit + + """ + if self._check(value): + if not isinstance(value, self._REGISTRY.Quantity): + value = self._REGISTRY.Quantity(1, value) + return value.to(self) + elif strict: + raise ValueError("%s must be a Quantity" % value) + else: + return value * self + + def m_from(self, value, strict=True, name="value"): + """Converts a numerical value or quantity to this unit, then returns + the magnitude of the converted value + + Parameters + ---------- + value : + a Quantity (or numerical value if strict=False) to convert + strict : + boolean to indicate that only quantities are accepted (Default value = True) + name : + descriptive name to use if an exception occurs (Default value = "value") + + Returns + ------- + type + The magnitude of the converted value + + """ + return self.from_(value, strict=strict, name=name).magnitude diff --git a/datasette/vendored/pint/facets/system/__init__.py b/datasette/vendored/pint/facets/system/__init__.py new file mode 100644 index 0000000000..b9cbc95935 --- /dev/null +++ b/datasette/vendored/pint/facets/system/__init__.py @@ -0,0 +1,17 @@ +""" + pint.facets.system + ~~~~~~~~~~~~~~~~~~ + + Adds pint the capability to system of units. + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from .definitions import SystemDefinition +from .objects import System +from .registry import GenericSystemRegistry, SystemRegistry + +__all__ = ["SystemDefinition", "System", "SystemRegistry", "GenericSystemRegistry"] diff --git a/datasette/vendored/pint/facets/system/definitions.py b/datasette/vendored/pint/facets/system/definitions.py new file mode 100644 index 0000000000..f47a23fd87 --- /dev/null +++ b/datasette/vendored/pint/facets/system/definitions.py @@ -0,0 +1,86 @@ +""" + pint.facets.systems.definitions + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass + +from ... import errors +from ...compat import Self + + +@dataclass(frozen=True) +class BaseUnitRule: + """A rule to define a base unit within a system.""" + + #: 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: str | None = None + + # 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. + + +@dataclass(frozen=True) +class SystemDefinition(errors.WithDefErr): + """Definition of a System.""" + + #: name of the system + name: str + #: unit groups that will be included within the system + using_group_names: tuple[str, ...] + #: rules to define new base unit within the system. + rules: tuple[BaseUnitRule, ...] + + @classmethod + def from_lines( + cls: type[Self], lines: Iterable[str], non_int_type: type + ) -> Self | None: + # TODO: this is to keep it backwards compatible + # TODO: check when is None returned. + 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) -> tuple[tuple[str, str | None], ...]: + # TODO: check if None can be dropped. + 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/datasette/vendored/pint/facets/system/objects.py b/datasette/vendored/pint/facets/system/objects.py new file mode 100644 index 0000000000..4a13ad65a7 --- /dev/null +++ b/datasette/vendored/pint/facets/system/objects.py @@ -0,0 +1,215 @@ +""" + pint.facets.systems.objects + ~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import numbers +from collections.abc import Callable, Iterable +from numbers import Number +from typing import Any, Generic + +from ..._typing import UnitLike +from ...babel_names import _babel_systems +from ...compat import babel_parse +from ...util import ( + SharedRegistryObject, + getattr_maybe_raise, + logger, + to_units_container, +) +from .. import group +from ..plain import MagnitudeT +from .definitions import SystemDefinition + +GetRootUnits = Callable[[UnitLike, bool], tuple[Number, UnitLike]] + + +class SystemQuantity(Generic[MagnitudeT], group.GroupQuantity[MagnitudeT]): + pass + + +class SystemUnit(group.GroupUnit): + pass + + +class System(SharedRegistryObject): + """A system is a Group plus a set of plain units. + + Members are computed dynamically, that is if a unit is added to a group X + all groups that include X are affected. + + The System belongs to one Registry. + + See SystemDefinition for the definition file syntax. + + Parameters + ---------- + name + Name of the group. + """ + + def __init__(self, name: str): + #: Name of the system + #: :type: str + self.name = name + + #: Maps root unit names to a dict indicating the new unit and its exponent. + self.base_units: dict[str, dict[str, numbers.Number]] = {} + + #: Derived unit names. + self.derived_units: set[str] = set() + + #: Names of the _used_groups in used by this system. + self._used_groups: set[str] = set() + + self._computed_members: frozenset[str] | None = None + + # Add this system to the system dictionary + self._REGISTRY._systems[self.name] = self + + def __dir__(self): + return list(self.members) + + def __getattr__(self, item: str) -> Any: + getattr_maybe_raise(self, item) + u = getattr(self._REGISTRY, self.name + "_" + item, None) + if u is not None: + return u + return getattr(self._REGISTRY, item) + + @property + def members(self): + d = self._REGISTRY._groups + if self._computed_members is None: + tmp: set[str] = set() + + for group_name in self._used_groups: + try: + tmp |= d[group_name].members + except KeyError: + logger.warning( + "Could not resolve {} in System {}".format( + group_name, self.name + ) + ) + + self._computed_members = frozenset(tmp) + + return self._computed_members + + def invalidate_members(self): + """Invalidate computed members in this Group and all parent nodes.""" + self._computed_members = None + + def add_groups(self, *group_names: str) -> None: + """Add groups to group.""" + self._used_groups |= set(group_names) + + self.invalidate_members() + + def remove_groups(self, *group_names: str) -> None: + """Remove groups from group.""" + self._used_groups -= set(group_names) + + self.invalidate_members() + + def format_babel(self, locale: str) -> str: + """translate the name of the system.""" + if locale and self.name in _babel_systems: + name = _babel_systems[self.name] + locale = babel_parse(locale) + return locale.measurement_systems[name] + return self.name + + # TODO: When 3.11 is minimal version, use Self + + @classmethod + def from_lines( + cls: type[System], + lines: Iterable[str], + get_root_func: GetRootUnits, + non_int_type: type = float, + ) -> System: + # TODO: we changed something here it used to be + # system_definition = SystemDefinition.from_lines(lines, get_root_func) + system_definition = SystemDefinition.from_lines(lines, non_int_type) + + if system_definition is None: + raise ValueError(f"Could not define System from from {lines}") + + return cls.from_definition(system_definition, get_root_func) + + @classmethod + def from_definition( + cls: type[System], + system_definition: SystemDefinition, + get_root_func: GetRootUnits | None = None, + ) -> System: + 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 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 unit must be a root dimension if not discarded unit is specified." + ) + + old_unit, value = dict(old_unit_dict).popitem() + + base_unit_names[old_unit] = {new_unit: 1 / value} + else: + # 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( + 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 + new_unit_expanded = to_units_container( + get_root_func(new_unit)[1], cls._REGISTRY + ) + + # We require that the old unit is present in the new_unit expanded + if old_unit not in new_unit_expanded: + raise ValueError("Old unit must be a component of new unit") + + # Here we invert the equation, in other words + # we write old units in terms new unit and expansion + new_unit_dict = { + new_unit: -1 / value + for new_unit, value in new_unit_expanded.items() + if new_unit != old_unit + } + new_unit_dict[new_unit] = 1 / new_unit_expanded[old_unit] + + base_unit_names[old_unit] = new_unit_dict + + system = cls(system_definition.name) + system.add_groups(*system_definition.using_group_names) + system.base_units.update(**base_unit_names) + system.derived_units |= set(derived_unit_names) + + return system + + +class Lister: + def __init__(self, d: dict[str, Any]): + self.d = d + + def __dir__(self) -> list[str]: + return list(self.d.keys()) + + def __getattr__(self, item: str) -> Any: + getattr_maybe_raise(self, item) + return self.d[item] diff --git a/datasette/vendored/pint/facets/system/registry.py b/datasette/vendored/pint/facets/system/registry.py new file mode 100644 index 0000000000..e5235a4cbb --- /dev/null +++ b/datasette/vendored/pint/facets/system/registry.py @@ -0,0 +1,265 @@ +""" + pint.facets.systems.registry + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from numbers import Number +from typing import TYPE_CHECKING, Any, Generic + +from ... import errors +from ...compat import TypeAlias +from ..plain import QuantityT, UnitT + +if TYPE_CHECKING: + from ..._typing import Quantity, Unit + +from ..._typing import UnitLike +from ...util import UnitsContainer as UnitsContainerT +from ...util import ( + create_class_with_registry, + to_units_container, +) +from ..group import GenericGroupRegistry +from . import objects +from .definitions import SystemDefinition + + +class GenericSystemRegistry( + Generic[QuantityT, UnitT], GenericGroupRegistry[QuantityT, UnitT] +): + """Handle of Systems. + + Conversion between units with different dimensions according + to previously established relations (contexts). + (e.g. in the spectroscopy, conversion between frequency and energy is possible) + + Capabilities: + + - Register systems. + - List systems + - Get or get the default system. + - 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: type[objects.System] + + def __init__(self, system: str | None = None, **kwargs): + super().__init__(**kwargs) + + #: Map system name to system. + self._systems: dict[str, objects.System] = {} + + #: Maps dimensionality (UnitsContainer) to Dimensionality (UnitsContainer) + self._base_units_cache: dict[UnitsContainerT, UnitsContainerT] = {} + + self._default_system_name: str | None = system + + def _init_dynamic_classes(self) -> None: + """Generate subclasses on the fly and attach them to self""" + super()._init_dynamic_classes() + self.System = create_class_with_registry(self, objects.System) + + def _after_init(self) -> None: + """Invoked at the end of ``__init__``. + + - Create default group and add all orphan units to it + - Set default system + """ + super()._after_init() + + #: System name to be used by default. + self._default_system_name = self._default_system_name or self._defaults.get( + "system", None + ) + + def _register_definition_adders(self) -> None: + super()._register_definition_adders() + self._register_adder(SystemDefinition, self._add_system) + + def _add_system(self, sd: SystemDefinition) -> None: + 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): + return objects.Lister(self._systems) + + @property + def default_system(self) -> str | None: + return self._default_system_name + + @default_system.setter + def default_system(self, name: str) -> None: + if name: + if name not in self._systems: + raise ValueError("Unknown system %s" % name) + + self._base_units_cache = {} + + self._default_system_name = name + + def get_system(self, name: str, create_if_needed: bool = True) -> objects.System: + """Return a Group. + + Parameters + ---------- + name : str + Name of the group to be. + create_if_needed : bool + If True, create a group if not found. If False, raise an Exception. + (Default value = True) + + Returns + ------- + type + System + + """ + if name in self._systems: + return self._systems[name] + + if not create_if_needed: + raise ValueError("Unknown system %s" % name) + + return self.System(name) + + def get_base_units( + self, + input_units: UnitLike | Quantity, + check_nonmult: bool = True, + system: str | objects.System | None = None, + ) -> tuple[Number, Unit]: + """Convert unit or dict of units to the plain units. + + If any unit is non multiplicative and check_converter is True, + then None is returned as the multiplicative factor. + + Unlike PlainRegistry, in this registry root_units might be different + from base_units + + Parameters + ---------- + input_units : UnitsContainer or str + units + check_nonmult : bool + if True, None will be returned as the + multiplicative factor if a non-multiplicative + units is found in the final Units. (Default value = True) + system : + (Default value = None) + + Returns + ------- + type + multiplicative factor, plain units + + """ + + input_units = to_units_container(input_units) + + f, units = self._get_base_units(input_units, check_nonmult, system) + + return f, self.Unit(units) + + def _get_base_units( + self, + input_units: UnitsContainerT, + check_nonmult: bool = True, + system: str | objects.System | None = None, + ): + if system is None: + system = self._default_system_name + + # The cache is only done for check_nonmult=True and the current system. + if ( + check_nonmult + and system == self._default_system_name + and input_units in self._base_units_cache + ): + return self._base_units_cache[input_units] + + factor, units = self.get_root_units(input_units, check_nonmult) + + if not system: + return factor, units + + # This will not be necessary after integration with the registry + # as it has a UnitsContainer intermediate + units = to_units_container(units, self) + + destination_units = self.UnitsContainer() + + bu = self.get_system(system, False).base_units + + for unit, value in units.items(): + if unit in bu: + new_unit = bu[unit] + new_unit = to_units_container(new_unit, self) + destination_units *= new_unit**value + else: + destination_units *= self.UnitsContainer({unit: value}) + + base_factor = self.convert(factor, units, destination_units) + + if check_nonmult: + self._base_units_cache[input_units] = base_factor, destination_units + + return base_factor, destination_units + + def get_compatible_units( + self, input_units: UnitsContainerT, group_or_system: str | None = None + ) -> frozenset[Unit]: + """ """ + + group_or_system = group_or_system or self._default_system_name + + if group_or_system is None: + return super().get_compatible_units(input_units) + + input_units = to_units_container(input_units) + + equiv = self._get_compatible_units(input_units, group_or_system) + + return frozenset(self.Unit(eq) for eq in equiv) + + def _get_compatible_units( + self, input_units: UnitsContainerT, group_or_system: str | None = None + ) -> frozenset[Unit]: + if group_or_system and group_or_system in self._systems: + members = self._systems[group_or_system].members + # group_or_system has been handled by System + return frozenset(members & super()._get_compatible_units(input_units)) + + try: + # This will be handled by groups + return super()._get_compatible_units(input_units, group_or_system) + except ValueError as ex: + # It might be also a system + if "Unknown Group" in str(ex): + raise ValueError( + "Unknown Group o System with name '%s'" % group_or_system + ) from ex + raise ex + + +class SystemRegistry( + GenericSystemRegistry[objects.SystemQuantity[Any], objects.SystemUnit] +): + Quantity: TypeAlias = objects.SystemQuantity[Any] + Unit: TypeAlias = objects.SystemUnit diff --git a/datasette/vendored/pint/formatting.py b/datasette/vendored/pint/formatting.py new file mode 100644 index 0000000000..9b880ae0e6 --- /dev/null +++ b/datasette/vendored/pint/formatting.py @@ -0,0 +1,169 @@ +""" + pint.formatter + ~~~~~~~~~~~~~~ + + Format units for pint. + + :copyright: 2016 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from numbers import Number +from typing import Iterable + +from .delegates.formatter._format_helpers import ( + _PRETTY_EXPONENTS, # noqa: F401 +) +from .delegates.formatter._format_helpers import ( + join_u as _join, # noqa: F401 +) +from .delegates.formatter._format_helpers import ( + pretty_fmt_exponent as _pretty_fmt_exponent, # noqa: F401 +) +from .delegates.formatter._spec_helpers import ( + _BASIC_TYPES, # noqa: F401 + FORMATTER, # noqa: F401 + REGISTERED_FORMATTERS, + extract_custom_flags, # noqa: F401 + remove_custom_flags, # noqa: F401 +) +from .delegates.formatter._spec_helpers import ( + parse_spec as _parse_spec, # noqa: F401 +) +from .delegates.formatter._spec_helpers import ( + split_format as split_format, # noqa: F401 +) + +# noqa +from .delegates.formatter._to_register import register_unit_format # noqa: F401 + +# Backwards compatiblity stuff +from .delegates.formatter.latex import ( + _EXP_PATTERN, # noqa: F401 + latex_escape, # noqa: F401 + matrix_to_latex, # noqa: F401 + ndarray_to_latex, # noqa: F401 + ndarray_to_latex_parts, # noqa: F401 + siunitx_format_unit, # noqa: F401 + vector_to_latex, # noqa: F401 +) + + +def formatter( + items: Iterable[tuple[str, Number]], + as_ratio: bool = True, + single_denominator: bool = False, + product_fmt: str = " * ", + division_fmt: str = " / ", + power_fmt: str = "{} ** {}", + parentheses_fmt: str = "({0})", + exp_call: FORMATTER = "{:n}".format, + sort: bool = True, +) -> str: + """Format a list of (name, exponent) pairs. + + Parameters + ---------- + items : list + a list of (name, exponent) pairs. + as_ratio : bool, optional + True to display as ratio, False as negative powers. (Default value = True) + single_denominator : bool, optional + all with terms with negative exponents are + collected together. (Default value = False) + product_fmt : str + the format used for multiplication. (Default value = " * ") + division_fmt : str + the format used for division. (Default value = " / ") + power_fmt : str + the format used for exponentiation. (Default value = "{} ** {}") + parentheses_fmt : str + the format used for parenthesis. (Default value = "({0})") + exp_call : callable + (Default value = lambda x: f"{x:n}") + sort : bool, optional + True to sort the formatted units alphabetically (Default value = True) + + Returns + ------- + str + the formula as a string. + + """ + + join_u = _join + + if sort is False: + items = tuple(items) + else: + items = sorted(items) + + if not items: + return "" + + if as_ratio: + fun = lambda x: exp_call(abs(x)) + else: + fun = exp_call + + pos_terms, neg_terms = [], [] + + for key, value in items: + if value == 1: + pos_terms.append(key) + elif value > 0: + pos_terms.append(power_fmt.format(key, fun(value))) + elif value == -1 and as_ratio: + neg_terms.append(key) + else: + neg_terms.append(power_fmt.format(key, fun(value))) + + if not as_ratio: + # Show as Product: positive * negative terms ** -1 + return _join(product_fmt, pos_terms + neg_terms) + + # Show as Ratio: positive terms / negative terms + pos_ret = _join(product_fmt, pos_terms) or "1" + + if not neg_terms: + return pos_ret + + if single_denominator: + neg_ret = join_u(product_fmt, neg_terms) + if len(neg_terms) > 1: + neg_ret = parentheses_fmt.format(neg_ret) + else: + neg_ret = join_u(division_fmt, neg_terms) + + # TODO: first or last pos_ret should be pluralized + + return _join(division_fmt, [pos_ret, neg_ret]) + + +def format_unit(unit, spec: str, registry=None, **options): + # registry may be None to allow formatting `UnitsContainer` objects + # in that case, the spec may not be "Lx" + + if not unit: + if spec.endswith("%"): + return "" + else: + return "dimensionless" + + if not spec: + spec = "D" + + if registry is None: + _formatter = REGISTERED_FORMATTERS.get(spec, None) + else: + try: + _formatter = registry.formatter._formatters[spec] + except Exception: + _formatter = registry.formatter._formatters.get(spec, None) + + if _formatter is None: + raise ValueError(f"Unknown conversion specified: {spec}") + + return _formatter.format_unit(unit) diff --git a/datasette/vendored/pint/matplotlib.py b/datasette/vendored/pint/matplotlib.py new file mode 100644 index 0000000000..2ca43fa333 --- /dev/null +++ b/datasette/vendored/pint/matplotlib.py @@ -0,0 +1,86 @@ +""" + pint.matplotlib + ~~~~~~~~~~~~~~~ + + Functions and classes related to working with Matplotlib's support + for plotting with units. + + :copyright: 2017 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import matplotlib.units + +from .util import iterable, sized + + +class PintAxisInfo(matplotlib.units.AxisInfo): + """Support default axis and tick labeling and default limits.""" + + def __init__(self, units): + """Set the default label to the pretty-print of the unit.""" + formatter = units._REGISTRY.mpl_formatter + super().__init__(label=formatter.format(units)) + + +class PintConverter(matplotlib.units.ConversionInterface): + """Implement support for pint within matplotlib's unit conversion framework.""" + + def __init__(self, registry): + super().__init__() + self._reg = registry + + def convert(self, value, unit, axis): + """Convert :`Quantity` instances for matplotlib to use.""" + # Short circuit for arrays + if hasattr(value, "units"): + return value.to(unit).magnitude + if iterable(value): + return [self._convert_value(v, unit, axis) for v in value] + + return self._convert_value(value, unit, axis) + + def _convert_value(self, value, unit, axis): + """Handle converting using attached unit or falling back to axis units.""" + if hasattr(value, "units"): + return value.to(unit).magnitude + + return self._reg.Quantity(value, axis.get_units()).to(unit).magnitude + + @staticmethod + def axisinfo(unit, axis): + """Return axis information for this particular unit.""" + + return PintAxisInfo(unit) + + @staticmethod + def default_units(x, axis): + """Get the default unit to use for the given combination of unit and axis.""" + if iterable(x) and sized(x): + return getattr(x[0], "units", None) + return getattr(x, "units", None) + + +def setup_matplotlib_handlers(registry, enable): + """Set up matplotlib's unit support to handle units from a registry. + + Parameters + ---------- + registry : pint.UnitRegistry + The registry that will be used. + enable : bool + Whether support should be enabled or disabled. + + Returns + ------- + + """ + if matplotlib.__version__ < "2.0": + raise RuntimeError("Matplotlib >= 2.0 required to work with pint.") + + if enable: + matplotlib.units.registry[registry.Quantity] = PintConverter(registry) + else: + matplotlib.units.registry.pop(registry.Quantity, None) diff --git a/datasette/vendored/pint/pint_convert.py b/datasette/vendored/pint/pint_convert.py new file mode 100644 index 0000000000..d8d96032dd --- /dev/null +++ b/datasette/vendored/pint/pint_convert.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 + +""" + pint-convert + ~~~~~~~~~~~~ + + :copyright: 2020 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import argparse +import contextlib +import re + +from datasette.vendored.pint import UnitRegistry + +parser = argparse.ArgumentParser(description="Unit converter.", usage=argparse.SUPPRESS) +parser.add_argument( + "-s", + "--system", + metavar="sys", + default="SI", + help="unit system to convert to (default: SI)", +) +parser.add_argument( + "-p", + "--prec", + metavar="n", + type=int, + default=12, + help="number of maximum significant figures (default: 12)", +) +parser.add_argument( + "-u", + "--prec-unc", + metavar="n", + type=int, + default=2, + help="number of maximum uncertainty digits (default: 2)", +) +parser.add_argument( + "-U", + "--with-unc", + dest="unc", + action="store_true", + help="consider uncertainties in constants", +) +parser.add_argument( + "-C", + "--no-corr", + dest="corr", + action="store_false", + help="ignore correlations between constants", +) +parser.add_argument( + "fr", metavar="from", type=str, help="unit or quantity to convert from" +) +parser.add_argument("to", type=str, nargs="?", help="unit to convert to") +try: + args = parser.parse_args() +except SystemExit: + parser.print_help() + raise + +ureg = UnitRegistry() +ureg.auto_reduce_dimensions = True +ureg.autoconvert_offset_to_baseunit = True +ureg.enable_contexts("Gau", "ESU", "sp", "energy", "boltzmann") +ureg.default_system = args.system + + +def _set(key: str, value): + obj = ureg._units[key].converter + object.__setattr__(obj, "scale", value) + + +if args.unc: + try: + import uncertainties + except ImportError: + raise Exception( + "Failed to import uncertainties library!\n Please install uncertainties package" + ) + + # Measured constants subject to correlation + # R_i: Rydberg constant + # g_e: Electron g factor + # m_u: Atomic mass constant + # m_e: Electron mass + # m_p: Proton mass + # m_n: Neutron mass + R_i = (ureg._units["R_inf"].converter.scale, 0.0000000000021e7) + g_e = (ureg._units["g_e"].converter.scale, 0.00000000000035) + m_u = (ureg._units["m_u"].converter.scale, 0.00000000050e-27) + m_e = (ureg._units["m_e"].converter.scale, 0.00000000028e-30) + m_p = (ureg._units["m_p"].converter.scale, 0.00000000051e-27) + m_n = (ureg._units["m_n"].converter.scale, 0.00000000095e-27) + if args.corr: + # Correlation matrix between measured constants (to be completed below) + # R_i g_e m_u m_e m_p m_n + corr = [ + [1.0, -0.00206, 0.00369, 0.00436, 0.00194, 0.00233], # R_i + [-0.00206, 1.0, 0.99029, 0.99490, 0.97560, 0.52445], # g_e + [0.00369, 0.99029, 1.0, 0.99536, 0.98516, 0.52959], # m_u + [0.00436, 0.99490, 0.99536, 1.0, 0.98058, 0.52714], # m_e + [0.00194, 0.97560, 0.98516, 0.98058, 1.0, 0.51521], # m_p + [0.00233, 0.52445, 0.52959, 0.52714, 0.51521, 1.0], + ] # m_n + try: + (R_i, g_e, m_u, m_e, m_p, m_n) = uncertainties.correlated_values_norm( + [R_i, g_e, m_u, m_e, m_p, m_n], corr + ) + except AttributeError: + raise Exception( + "Correlation cannot be calculated!\n Please install numpy package" + ) + else: + R_i = uncertainties.ufloat(*R_i) + g_e = uncertainties.ufloat(*g_e) + m_u = uncertainties.ufloat(*m_u) + m_e = uncertainties.ufloat(*m_e) + m_p = uncertainties.ufloat(*m_p) + m_n = uncertainties.ufloat(*m_n) + + _set("R_inf", R_i) + _set("g_e", g_e) + _set("m_u", m_u) + _set("m_e", m_e) + _set("m_p", m_p) + _set("m_n", m_n) + + # Measured constants with zero correlation + _set( + "gravitational_constant", + uncertainties.ufloat( + ureg._units["gravitational_constant"].converter.scale, 0.00015e-11 + ), + ) + + _set( + "d_220", + uncertainties.ufloat(ureg._units["d_220"].converter.scale, 0.000000032e-10), + ) + + _set( + "K_alpha_Cu_d_220", + uncertainties.ufloat( + ureg._units["K_alpha_Cu_d_220"].converter.scale, 0.00000022 + ), + ) + + _set( + "K_alpha_Mo_d_220", + uncertainties.ufloat( + ureg._units["K_alpha_Mo_d_220"].converter.scale, 0.00000019 + ), + ) + + _set( + "K_alpha_W_d_220", + uncertainties.ufloat( + ureg._units["K_alpha_W_d_220"].converter.scale, 0.000000098 + ), + ) + + ureg._root_units_cache = {} + ureg._build_cache() + + +def convert(u_from, u_to=None, unc=None, factor=None): + prec_unc = 0 + q = ureg.Quantity(u_from) + fmt = f".{args.prec}g" + if unc: + q = q.plus_minus(unc) + if u_to: + nq = q.to(u_to) + else: + nq = q.to_base_units() + if factor: + q *= ureg.Quantity(factor) + nq *= ureg.Quantity(factor).to_base_units() + if args.unc: + prec_unc = use_unc(nq.magnitude, fmt, args.prec_unc) + if prec_unc > 0: + fmt = f".{prec_unc}uS" + else: + with contextlib.suppress(Exception): + nq = nq.magnitude.n * nq.units + + fmt = "{:" + fmt + "} {:~P}" + print(("{:} = " + fmt).format(q, nq.magnitude, nq.units)) + + +def use_unc(num, fmt, prec_unc): + unc = 0 + with contextlib.suppress(Exception): + if isinstance(num, uncertainties.UFloat): + full = ("{:" + fmt + "}").format(num) + unc = re.search(r"\+/-[0.]*([\d.]*)", full).group(1) + unc = len(unc.replace(".", "")) + + return max(0, min(prec_unc, unc)) + + +def main(): + convert(args.fr, args.to) + + +if __name__ == "__main__": + main() diff --git a/datasette/vendored/pint/pint_eval.py b/datasette/vendored/pint/pint_eval.py new file mode 100644 index 0000000000..fa132f5966 --- /dev/null +++ b/datasette/vendored/pint/pint_eval.py @@ -0,0 +1,568 @@ +""" + pint.pint_eval + ~~~~~~~~~~~~~~ + + An expression evaluator to be used as a safe replacement for builtin eval. + + :copyright: 2016 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import operator +import token as tokenlib +import tokenize +from io import BytesIO +from tokenize import TokenInfo +from typing import Any + +try: + from uncertainties import ufloat + + HAS_UNCERTAINTIES = True +except ImportError: + HAS_UNCERTAINTIES = False + ufloat = None + +from .errors import DefinitionSyntaxError + +# For controlling order of operations +_OP_PRIORITY = { + "+/-": 4, + "**": 3, + "^": 3, + "unary": 2, + "*": 1, + "": 1, # operator for implicit ops + "//": 1, + "/": 1, + "%": 1, + "+": 0, + "-": 0, +} + + +def _ufloat(left, right): + if HAS_UNCERTAINTIES: + return ufloat(left, right) + raise TypeError("Could not import support for uncertainties") + + +def _power(left: Any, right: Any) -> Any: + from . import Quantity + from .compat import is_duck_array + + if ( + isinstance(left, Quantity) + and is_duck_array(left.magnitude) + and left.dtype.kind not in "cf" + and right < 0 + ): + left = left.astype(float) + + return operator.pow(left, right) + + +# https://stackoverflow.com/a/1517965/1291237 +class tokens_with_lookahead: + def __init__(self, iter): + self.iter = iter + self.buffer = [] + + def __iter__(self): + return self + + def __next__(self): + if self.buffer: + return self.buffer.pop(0) + else: + return self.iter.__next__() + + def lookahead(self, n): + """Return an item n entries ahead in the iteration.""" + while n >= len(self.buffer): + try: + self.buffer.append(self.iter.__next__()) + except StopIteration: + return None + return self.buffer[n] + + +def _plain_tokenizer(input_string): + for tokinfo in tokenize.tokenize(BytesIO(input_string.encode("utf-8")).readline): + if tokinfo.type != tokenlib.ENCODING: + yield tokinfo + + +def uncertainty_tokenizer(input_string): + def _number_or_nan(token): + if token.type == tokenlib.NUMBER or ( + token.type == tokenlib.NAME and token.string == "nan" + ): + return True + return False + + def _get_possible_e(toklist, e_index): + possible_e_token = toklist.lookahead(e_index) + if ( + possible_e_token.string[0] == "e" + and len(possible_e_token.string) > 1 + and possible_e_token.string[1].isdigit() + ): + end = possible_e_token.end + possible_e = tokenize.TokenInfo( + type=tokenlib.STRING, + string=possible_e_token.string, + start=possible_e_token.start, + end=end, + line=possible_e_token.line, + ) + elif ( + possible_e_token.string[0] in ["e", "E"] + and toklist.lookahead(e_index + 1).string in ["+", "-"] + and toklist.lookahead(e_index + 2).type == tokenlib.NUMBER + ): + # Special case: Python allows a leading zero for exponents (i.e., 042) but not for numbers + if ( + toklist.lookahead(e_index + 2).string == "0" + and toklist.lookahead(e_index + 3).type == tokenlib.NUMBER + ): + exp_number = toklist.lookahead(e_index + 3).string + end = toklist.lookahead(e_index + 3).end + else: + exp_number = toklist.lookahead(e_index + 2).string + end = toklist.lookahead(e_index + 2).end + possible_e = tokenize.TokenInfo( + type=tokenlib.STRING, + string=f"e{toklist.lookahead(e_index+1).string}{exp_number}", + start=possible_e_token.start, + end=end, + line=possible_e_token.line, + ) + else: + possible_e = None + return possible_e + + def _apply_e_notation(mantissa, exponent): + if mantissa.string == "nan": + return mantissa + if float(mantissa.string) == 0.0: + return mantissa + return tokenize.TokenInfo( + type=tokenlib.NUMBER, + string=f"{mantissa.string}{exponent.string}", + start=mantissa.start, + end=exponent.end, + line=exponent.line, + ) + + def _finalize_e(nominal_value, std_dev, toklist, possible_e): + nominal_value = _apply_e_notation(nominal_value, possible_e) + std_dev = _apply_e_notation(std_dev, possible_e) + next(toklist) # consume 'e' and positive exponent value + if possible_e.string[1] in ["+", "-"]: + next(toklist) # consume "+" or "-" in exponent + exp_number = next(toklist) # consume exponent value + if ( + exp_number.string == "0" + and toklist.lookahead(0).type == tokenlib.NUMBER + ): + exp_number = next(toklist) + assert exp_number.end == end + # We've already applied the number, we're just consuming all the tokens + return nominal_value, std_dev + + # when tokenize encounters whitespace followed by an unknown character, + # (such as ±) it proceeds to mark every character of the whitespace as ERRORTOKEN, + # in addition to marking the unknown character as ERRORTOKEN. Rather than + # wading through all that vomit, just eliminate the problem + # in the input by rewriting ± as +/-. + input_string = input_string.replace("±", "+/-") + toklist = tokens_with_lookahead(_plain_tokenizer(input_string)) + for tokinfo in toklist: + line = tokinfo.line + start = tokinfo.start + if ( + tokinfo.string == "+" + and toklist.lookahead(0).string == "/" + and toklist.lookahead(1).string == "-" + ): + plus_minus_op = tokenize.TokenInfo( + type=tokenlib.OP, + string="+/-", + start=start, + end=toklist.lookahead(1).end, + line=line, + ) + for i in range(-1, 1): + next(toklist) + yield plus_minus_op + elif ( + tokinfo.string == "(" + and ((seen_minus := 1 if toklist.lookahead(0).string == "-" else 0) or True) + and _number_or_nan(toklist.lookahead(seen_minus)) + and toklist.lookahead(seen_minus + 1).string == "+" + and toklist.lookahead(seen_minus + 2).string == "/" + and toklist.lookahead(seen_minus + 3).string == "-" + and _number_or_nan(toklist.lookahead(seen_minus + 4)) + and toklist.lookahead(seen_minus + 5).string == ")" + ): + # ( NUM_OR_NAN +/- NUM_OR_NAN ) POSSIBLE_E_NOTATION + possible_e = _get_possible_e(toklist, seen_minus + 6) + if possible_e: + end = possible_e.end + else: + end = toklist.lookahead(seen_minus + 5).end + if seen_minus: + minus_op = next(toklist) + yield minus_op + nominal_value = next(toklist) + tokinfo = next(toklist) # consume '+' + next(toklist) # consume '/' + plus_minus_op = tokenize.TokenInfo( + type=tokenlib.OP, + string="+/-", + start=tokinfo.start, + end=next(toklist).end, # consume '-' + line=line, + ) + std_dev = next(toklist) + next(toklist) # consume final ')' + if possible_e: + nominal_value, std_dev = _finalize_e( + nominal_value, std_dev, toklist, possible_e + ) + yield nominal_value + yield plus_minus_op + yield std_dev + elif ( + tokinfo.type == tokenlib.NUMBER + and toklist.lookahead(0).string == "(" + and toklist.lookahead(1).type == tokenlib.NUMBER + and toklist.lookahead(2).string == ")" + ): + # NUM_OR_NAN ( NUM_OR_NAN ) POSSIBLE_E_NOTATION + possible_e = _get_possible_e(toklist, 3) + if possible_e: + end = possible_e.end + else: + end = toklist.lookahead(2).end + nominal_value = tokinfo + tokinfo = next(toklist) # consume '(' + plus_minus_op = tokenize.TokenInfo( + type=tokenlib.OP, + string="+/-", + start=tokinfo.start, + end=tokinfo.end, # this is funky because there's no "+/-" in nominal(std_dev) notation + line=line, + ) + std_dev = next(toklist) + if "." not in std_dev.string: + std_dev = tokenize.TokenInfo( + type=std_dev.type, + string="0." + std_dev.string, + start=std_dev.start, + end=std_dev.end, + line=line, + ) + next(toklist) # consume final ')' + if possible_e: + nominal_value, std_dev = _finalize_e( + nominal_value, std_dev, toklist, possible_e + ) + yield nominal_value + yield plus_minus_op + yield std_dev + else: + yield tokinfo + + +if HAS_UNCERTAINTIES: + tokenizer = uncertainty_tokenizer +else: + tokenizer = _plain_tokenizer + +import typing + +UnaryOpT = typing.Callable[ + [ + Any, + ], + Any, +] +BinaryOpT = typing.Callable[[Any, Any], Any] + +_UNARY_OPERATOR_MAP: dict[str, UnaryOpT] = {"+": lambda x: x, "-": lambda x: x * -1} + +_BINARY_OPERATOR_MAP: dict[str, BinaryOpT] = { + "+/-": _ufloat, + "**": _power, + "*": operator.mul, + "": operator.mul, # operator for implicit ops + "/": operator.truediv, + "+": operator.add, + "-": operator.sub, + "%": operator.mod, + "//": operator.floordiv, +} + + +class EvalTreeNode: + """Single node within an evaluation tree + + left + operator + right --> binary op + left + operator --> unary op + left + right --> implicit op + left --> single value + """ + + def __init__( + self, + left: EvalTreeNode | TokenInfo, + operator: TokenInfo | None = None, + right: EvalTreeNode | None = None, + ): + self.left = left + self.operator = operator + self.right = right + + def to_string(self) -> str: + # For debugging purposes + if self.right: + assert isinstance(self.left, EvalTreeNode), "self.left not EvalTreeNode (1)" + comps = [self.left.to_string()] + if self.operator: + comps.append(self.operator.string) + comps.append(self.right.to_string()) + elif self.operator: + assert isinstance(self.left, EvalTreeNode), "self.left not EvalTreeNode (2)" + comps = [self.operator.string, self.left.to_string()] + else: + assert isinstance(self.left, TokenInfo), "self.left not TokenInfo (1)" + return self.left.string + return "(%s)" % " ".join(comps) + + def evaluate( + self, + define_op: typing.Callable[ + [ + Any, + ], + Any, + ], + bin_op: dict[str, BinaryOpT] | None = None, + un_op: dict[str, UnaryOpT] | None = None, + ): + """Evaluate node. + + Parameters + ---------- + define_op : callable + Translates tokens into objects. + bin_op : dict or None, optional + (Default value = _BINARY_OPERATOR_MAP) + un_op : dict or None, optional + (Default value = _UNARY_OPERATOR_MAP) + + Returns + ------- + + """ + + bin_op = bin_op or _BINARY_OPERATOR_MAP + un_op = un_op or _UNARY_OPERATOR_MAP + + if self.right: + assert isinstance(self.left, EvalTreeNode), "self.left not EvalTreeNode (3)" + # binary or implicit operator + op_text = self.operator.string if self.operator else "" + if op_text not in bin_op: + raise DefinitionSyntaxError(f"missing binary operator '{op_text}'") + + return bin_op[op_text]( + self.left.evaluate(define_op, bin_op, un_op), + self.right.evaluate(define_op, bin_op, un_op), + ) + elif self.operator: + assert isinstance(self.left, EvalTreeNode), "self.left not EvalTreeNode (4)" + # unary operator + op_text = self.operator.string + if op_text not in un_op: + raise DefinitionSyntaxError(f"missing unary operator '{op_text}'") + return un_op[op_text](self.left.evaluate(define_op, bin_op, un_op)) + + # single value + return define_op(self.left) + + +from collections.abc import Iterable + + +def _build_eval_tree( + tokens: list[TokenInfo], + op_priority: dict[str, int], + index: int = 0, + depth: int = 0, + prev_op: str = "", +) -> tuple[EvalTreeNode, int]: + """Build an evaluation tree from a set of tokens. + + Params: + Index, depth, and prev_op used recursively, so don't touch. + Tokens is an iterable of tokens from an expression to be evaluated. + + Transform the tokens from an expression into a recursive parse tree, following order + of operations. Operations can include binary ops (3 + 4), implicit ops (3 kg), or + unary ops (-1). + + General Strategy: + 1) Get left side of operator + 2) If no tokens left, return final result + 3) Get operator + 4) Use recursion to create tree starting at token on right side of operator (start at step #1) + 4.1) If recursive call encounters an operator with lower or equal priority to step #2, exit recursion + 5) Combine left side, operator, and right side into a new left side + 6) Go back to step #2 + + Raises + ------ + DefinitionSyntaxError + If there is a syntax error. + + """ + + result = None + + while True: + current_token = tokens[index] + token_type = current_token.type + token_text = current_token.string + + if token_type == tokenlib.OP: + if token_text == ")": + if prev_op == "": + raise DefinitionSyntaxError( + f"unopened parentheses in tokens: {current_token}" + ) + elif prev_op == "(": + # close parenthetical group + assert result is not None + return result, index + else: + # parenthetical group ending, but we need to close sub-operations within group + assert result is not None + return result, index - 1 + elif token_text == "(": + # gather parenthetical group + right, index = _build_eval_tree( + tokens, op_priority, index + 1, 0, token_text + ) + if not tokens[index][1] == ")": + raise DefinitionSyntaxError("weird exit from parentheses") + if result: + # implicit op with a parenthetical group, i.e. "3 (kg ** 2)" + result = EvalTreeNode(left=result, right=right) + else: + # get first token + result = right + elif token_text in op_priority: + if result: + # equal-priority operators are grouped in a left-to-right order, + # unless they're exponentiation, in which case they're grouped + # right-to-left this allows us to get the expected behavior for + # multiple exponents + # (2^3^4) --> (2^(3^4)) + # (2 * 3 / 4) --> ((2 * 3) / 4) + if op_priority[token_text] <= op_priority.get( + prev_op, -1 + ) and token_text not in ("**", "^"): + # previous operator is higher priority, so end previous binary op + return result, index - 1 + # get right side of binary op + right, index = _build_eval_tree( + tokens, op_priority, index + 1, depth + 1, token_text + ) + result = EvalTreeNode( + left=result, operator=current_token, right=right + ) + else: + # unary operator + right, index = _build_eval_tree( + tokens, op_priority, index + 1, depth + 1, "unary" + ) + result = EvalTreeNode(left=right, operator=current_token) + elif token_type in (tokenlib.NUMBER, tokenlib.NAME): + if result: + # tokens with an implicit operation i.e. "1 kg" + if op_priority[""] <= op_priority.get(prev_op, -1): + # previous operator is higher priority than implicit, so end + # previous binary op + return result, index - 1 + right, index = _build_eval_tree( + tokens, op_priority, index, depth + 1, "" + ) + result = EvalTreeNode(left=result, right=right) + else: + # get first token + result = EvalTreeNode(left=current_token) + + if tokens[index][0] == tokenlib.ENDMARKER: + if prev_op == "(": + raise DefinitionSyntaxError("unclosed parentheses in tokens") + if depth > 0 or prev_op: + # have to close recursion + assert result is not None + return result, index + else: + # recursion all closed, so just return the final result + assert result is not None + return result, -1 + + if index + 1 >= len(tokens): + # should hit ENDMARKER before this ever happens + raise DefinitionSyntaxError("unexpected end to tokens") + + index += 1 + + +def build_eval_tree( + tokens: Iterable[TokenInfo], + op_priority: dict[str, int] | None = None, +) -> EvalTreeNode: + """Build an evaluation tree from a set of tokens. + + Params: + Index, depth, and prev_op used recursively, so don't touch. + Tokens is an iterable of tokens from an expression to be evaluated. + + Transform the tokens from an expression into a recursive parse tree, following order + of operations. Operations can include binary ops (3 + 4), implicit ops (3 kg), or + unary ops (-1). + + General Strategy: + 1) Get left side of operator + 2) If no tokens left, return final result + 3) Get operator + 4) Use recursion to create tree starting at token on right side of operator (start at step #1) + 4.1) If recursive call encounters an operator with lower or equal priority to step #2, exit recursion + 5) Combine left side, operator, and right side into a new left side + 6) Go back to step #2 + + Raises + ------ + DefinitionSyntaxError + If there is a syntax error. + + """ + + if op_priority is None: + op_priority = _OP_PRIORITY + + if not isinstance(tokens, list): + # ensure tokens is list so we can access by index + tokens = list(tokens) + + result, _ = _build_eval_tree(tokens, op_priority, 0, 0) + + return result diff --git a/datasette/vendored/pint/py.typed b/datasette/vendored/pint/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/datasette/vendored/pint/registry.py b/datasette/vendored/pint/registry.py new file mode 100644 index 0000000000..ceb9b62d1a --- /dev/null +++ b/datasette/vendored/pint/registry.py @@ -0,0 +1,272 @@ +""" + pint.registry + ~~~~~~~~~~~~~ + + Defines the UnitRegistry, a class to contain units and their relations. + + This registry contains all pint capabilities, but you can build your + customized registry by picking only the features that you actually + need. + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from typing import Generic + +from . import facets, registry_helpers +from .compat import TypeAlias +from .util import logger, pi_theorem + +# To build the Quantity and Unit classes +# we follow the UnitRegistry bases +# but + + +class Quantity( + facets.SystemRegistry.Quantity, + facets.ContextRegistry.Quantity, + facets.DaskRegistry.Quantity, + facets.NumpyRegistry.Quantity, + facets.MeasurementRegistry.Quantity, + facets.NonMultiplicativeRegistry.Quantity, + facets.PlainRegistry.Quantity, +): + pass + + +class Unit( + facets.SystemRegistry.Unit, + facets.ContextRegistry.Unit, + facets.DaskRegistry.Unit, + facets.NumpyRegistry.Unit, + facets.MeasurementRegistry.Unit, + facets.NonMultiplicativeRegistry.Unit, + facets.PlainRegistry.Unit, +): + pass + + +class GenericUnitRegistry( + Generic[facets.QuantityT, facets.UnitT], + facets.GenericSystemRegistry[facets.QuantityT, facets.UnitT], + facets.GenericContextRegistry[facets.QuantityT, facets.UnitT], + facets.GenericDaskRegistry[facets.QuantityT, facets.UnitT], + facets.GenericNumpyRegistry[facets.QuantityT, facets.UnitT], + facets.GenericMeasurementRegistry[facets.QuantityT, facets.UnitT], + facets.GenericNonMultiplicativeRegistry[facets.QuantityT, facets.UnitT], + facets.GenericPlainRegistry[facets.QuantityT, facets.UnitT], +): + pass + + +class UnitRegistry(GenericUnitRegistry[Quantity, Unit]): + """The unit registry stores the definitions and relationships between units. + + Parameters + ---------- + filename : + path of the units definition file to load or line-iterable object. + Empty string to load the default definition file. (default) + None to leave the UnitRegistry empty. + force_ndarray : bool + convert any input, scalar or not to a numpy.ndarray. + (Default: False) + force_ndarray_like : bool + convert all inputs other than duck arrays to a numpy.ndarray. + (Default: False) + default_as_delta : + In the context of a multiplication of units, interpret + non-multiplicative units as their *delta* counterparts. + (Default: False) + autoconvert_offset_to_baseunit : + If True converts offset units in quantities are + converted to their plain units in multiplicative + context. If False no conversion happens. (Default: False) + on_redefinition : str + action to take in case a unit is redefined. + 'warn', 'raise', 'ignore' (Default: 'raise') + auto_reduce_dimensions : + If True, reduce dimensionality on appropriate operations. + (Default: False) + autoconvert_to_preferred : + If True, converts preferred units on appropriate operations. + (Default: False) + preprocessors : + list of callables which are iteratively ran on any input expression + or unit string or None for no preprocessor. + (Default=None) + fmt_locale : + locale identifier string, used in `format_babel` or None. + (Default=None) + case_sensitive : bool, optional + Control default case sensitivity of unit parsing. (Default: True) + cache_folder : str or pathlib.Path or None, optional + Specify the folder in which cache files are saved and loaded from. + If None, the cache is disabled. (default) + """ + + Quantity: TypeAlias = Quantity + Unit: TypeAlias = Unit + + def __init__( + self, + filename="", + force_ndarray: bool = False, + force_ndarray_like: bool = False, + default_as_delta: bool = True, + autoconvert_offset_to_baseunit: bool = False, + on_redefinition: str = "warn", + system=None, + auto_reduce_dimensions=False, + autoconvert_to_preferred=False, + preprocessors=None, + fmt_locale=None, + non_int_type=float, + case_sensitive: bool = True, + cache_folder=None, + ): + super().__init__( + filename=filename, + force_ndarray=force_ndarray, + force_ndarray_like=force_ndarray_like, + on_redefinition=on_redefinition, + default_as_delta=default_as_delta, + autoconvert_offset_to_baseunit=autoconvert_offset_to_baseunit, + system=system, + auto_reduce_dimensions=auto_reduce_dimensions, + autoconvert_to_preferred=autoconvert_to_preferred, + preprocessors=preprocessors, + fmt_locale=fmt_locale, + non_int_type=non_int_type, + case_sensitive=case_sensitive, + cache_folder=cache_folder, + ) + + def pi_theorem(self, quantities): + """Builds dimensionless quantities using the Buckingham π theorem + + Parameters + ---------- + quantities : dict + mapping between variable name and units + + Returns + ------- + list + a list of dimensionless quantities expressed as dicts + + """ + return pi_theorem(quantities, self) + + def setup_matplotlib(self, enable: bool = True) -> None: + """Set up handlers for matplotlib's unit support. + + Parameters + ---------- + enable : bool + whether support should be enabled or disabled (Default value = True) + + """ + # Delays importing matplotlib until it's actually requested + from .matplotlib import setup_matplotlib_handlers + + setup_matplotlib_handlers(self, enable) + + wraps = registry_helpers.wraps + + check = registry_helpers.check + + +class LazyRegistry(Generic[facets.QuantityT, facets.UnitT]): + def __init__(self, args=None, kwargs=None): + self.__dict__["params"] = args or (), kwargs or {} + + def __init(self): + args, kwargs = self.__dict__["params"] + kwargs["on_redefinition"] = "raise" + self.__class__ = UnitRegistry + self.__init__(*args, **kwargs) + self._after_init() + + def __getattr__(self, item): + if item == "_on_redefinition": + return "raise" + self.__init() + return getattr(self, item) + + def __setattr__(self, key, value): + if key == "__class__": + super().__setattr__(key, value) + else: + self.__init() + setattr(self, key, value) + + def __getitem__(self, item): + self.__init() + return self[item] + + def __call__(self, *args, **kwargs): + self.__init() + return self(*args, **kwargs) + + +class ApplicationRegistry: + """A wrapper class used to distribute changes to the application registry.""" + + __slots__ = ["_registry"] + + def __init__(self, registry): + self._registry = registry + + def get(self): + """Get the wrapped registry""" + return self._registry + + def set(self, new_registry): + """Set the new registry + + Parameters + ---------- + new_registry : ApplicationRegistry or LazyRegistry or UnitRegistry + The new registry. + + See Also + -------- + set_application_registry + """ + if isinstance(new_registry, type(self)): + new_registry = new_registry.get() + + if not isinstance(new_registry, (LazyRegistry, UnitRegistry)): + raise TypeError("Expected UnitRegistry; got %s" % type(new_registry)) + logger.debug( + "Changing app registry from %r to %r.", self._registry, new_registry + ) + self._registry = new_registry + + def __getattr__(self, name): + return getattr(self._registry, name) + + def __setattr__(self, name, value): + if name in self.__slots__: + super().__setattr__(name, value) + else: + setattr(self._registry, name, value) + + def __dir__(self): + return dir(self._registry) + + def __getitem__(self, item): + return self._registry[item] + + def __call__(self, *args, **kwargs): + return self._registry(*args, **kwargs) + + def __contains__(self, item): + return self._registry.__contains__(item) + + def __iter__(self): + return iter(self._registry) diff --git a/datasette/vendored/pint/registry_helpers.py b/datasette/vendored/pint/registry_helpers.py new file mode 100644 index 0000000000..f2961cc74c --- /dev/null +++ b/datasette/vendored/pint/registry_helpers.py @@ -0,0 +1,386 @@ +""" + pint.registry_helpers + ~~~~~~~~~~~~~~~~~~~~~ + + Miscellaneous methods of the registry written as separate functions. + + :copyright: 2016 by Pint Authors, see AUTHORS for more details.. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import functools +from collections.abc import Callable, Iterable +from inspect import Parameter, signature +from itertools import zip_longest +from typing import TYPE_CHECKING, Any, TypeVar + +from ._typing import F +from .errors import DimensionalityError +from .util import UnitsContainer, to_units_container + +if TYPE_CHECKING: + from ._typing import Quantity, Unit + from .registry import UnitRegistry + +T = TypeVar("T") + + +def _replace_units(original_units, values_by_name): + """Convert a unit compatible type to a UnitsContainer. + + Parameters + ---------- + original_units : + a UnitsContainer instance. + values_by_name : + a map between original names and the new values. + + Returns + ------- + + """ + q = 1 + for arg_name, exponent in original_units.items(): + q = q * values_by_name[arg_name] ** exponent + + return getattr(q, "_units", UnitsContainer({})) + + +def _to_units_container(a, registry=None): + """Convert a unit compatible type to a UnitsContainer, + checking if it is string field prefixed with an equal + (which is considered a reference) + + Parameters + ---------- + a : + + registry : + (Default value = None) + + Returns + ------- + UnitsContainer, bool + + + """ + if isinstance(a, str) and "=" in a: + return to_units_container(a.split("=", 1)[1]), True + return to_units_container(a, registry), False + + +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() + defs_args_ndx = set() + + # Arguments which depend on others + dependent_args_ndx = set() + + # Arguments which have units. + unit_args_ndx = set() + + # _to_units_container + args_as_uc = [_to_units_container(arg, registry) for arg in args] + + # Check for references in args, remove None values + for ndx, (arg, is_ref) in enumerate(args_as_uc): + if arg is None: + continue + elif is_ref: + if len(arg) == 1: + [(key, value)] = arg.items() + if value == 1 and key not in defs_args: + # This is the first time that + # a variable is used => it is a definition. + defs_args.add(key) + defs_args_ndx.add(ndx) + args_as_uc[ndx] = (key, True) + else: + # The variable was already found elsewhere, + # we consider it a dependent variable. + dependent_args_ndx.add(ndx) + else: + dependent_args_ndx.add(ndx) + else: + unit_args_ndx.add(ndx) + + # Check that all valid dependent variables + for ndx in dependent_args_ndx: + arg, is_ref = args_as_uc[ndx] + if not isinstance(arg, dict): + continue + if not set(arg.keys()) <= defs_args: + raise ValueError( + "Found a missing token while wrapping a function: " + "Not all variable referenced in %s are defined using !" % args[ndx] + ) + + def _converter(ureg, sig, values, kw, strict): + len_initial_values = len(values) + + # pack kwargs + for i, param_name in enumerate(sig.parameters): + if i >= len_initial_values: + values.append(kw[param_name]) + + values_by_name = {} + + # first pass: Grab named values + for ndx in defs_args_ndx: + value = values[ndx] + values_by_name[args_as_uc[ndx][0]] = value + values[ndx] = getattr(value, "_magnitude", value) + + # second pass: calculate derived values based on named values + for ndx in dependent_args_ndx: + value = values[ndx] + assert _replace_units(args_as_uc[ndx][0], values_by_name) is not None + values[ndx] = ureg._convert( + getattr(value, "_magnitude", value), + getattr(value, "_units", UnitsContainer({})), + _replace_units(args_as_uc[ndx][0], values_by_name), + ) + + # third pass: convert other arguments + for ndx in unit_args_ndx: + if isinstance(values[ndx], ureg.Quantity): + values[ndx] = ureg._convert( + values[ndx]._magnitude, values[ndx]._units, args_as_uc[ndx][0] + ) + else: + if strict: + if isinstance(values[ndx], str): + # if the value is a string, we try to parse it + tmp_value = ureg.parse_expression(values[ndx]) + values[ndx] = ureg._convert( + tmp_value._magnitude, tmp_value._units, args_as_uc[ndx][0] + ) + else: + raise ValueError( + "A wrapped function using strict=True requires " + "quantity or a string for all arguments with not None units. " + "(error found for {}, {})".format( + args_as_uc[ndx][0], values[ndx] + ) + ) + + # unpack kwargs + for i, param_name in enumerate(sig.parameters): + if i >= len_initial_values: + kw[param_name] = values[i] + + return values[:len_initial_values], kw, values_by_name + + return _converter + + +def _apply_defaults(sig, args, kwargs): + """Apply default keyword arguments. + + Named keywords may have been left blank. This function applies the default + values so that every argument is defined. + """ + + for i, param in enumerate(sig.parameters.values()): + if ( + i >= len(args) + and param.default != Parameter.empty + and param.name not in kwargs + ): + kwargs[param.name] = param.default + return list(args), kwargs + + +def wraps( + ureg: UnitRegistry, + ret: str | Unit | Iterable[str | Unit | None] | None, + args: str | Unit | Iterable[str | Unit | None] | None, + strict: bool = True, +) -> Callable[[Callable[..., Any]], Callable[..., Quantity]]: + """Wraps a function to become pint-aware. + + Use it when a function requires a numerical value but in some specific + units. The wrapper function will take a pint quantity, convert to the units + specified in `args` and then call the wrapped function with the resulting + magnitude. + + The value returned by the wrapped function will be converted to the units + specified in `ret`. + + Parameters + ---------- + ureg : pint.UnitRegistry + a UnitRegistry instance. + ret : str, pint.Unit, or iterable of str or pint.Unit + Units of each of the return values. Use `None` to skip argument conversion. + args : str, pint.Unit, or iterable of str or pint.Unit + Units of each of the input arguments. Use `None` to skip argument conversion. + strict : bool + Indicates that only quantities are accepted. (Default value = True) + + Returns + ------- + callable + the wrapper function. + + Raises + ------ + TypeError + if the number of given arguments does not match the number of function parameters. + if any of the provided arguments is not a unit a string or Quantity + + """ + + if not isinstance(args, (list, tuple)): + args = (args,) + + for arg in args: + if arg is not None and not isinstance(arg, (ureg.Unit, str)): + raise TypeError( + "wraps arguments must by of type str or Unit, not %s (%s)" + % (type(arg), arg) + ) + + converter = _parse_wrap_args(args) + + is_ret_container = isinstance(ret, (list, tuple)) + if is_ret_container: + for arg in ret: + if arg is not None and not isinstance(arg, (ureg.Unit, str)): + raise TypeError( + "wraps 'ret' argument must by of type str or Unit, not %s (%s)" + % (type(arg), arg) + ) + ret = ret.__class__([_to_units_container(arg, ureg) for arg in ret]) + else: + if ret is not None and not isinstance(ret, (ureg.Unit, str)): + raise TypeError( + "wraps 'ret' argument must by of type str or Unit, not %s (%s)" + % (type(ret), ret) + ) + ret = _to_units_container(ret, ureg) + + def decorator(func: Callable[..., Any]) -> Callable[..., Quantity]: + sig = signature(func) + count_params = len(sig.parameters) + if len(args) != count_params: + raise TypeError( + "%s takes %i parameters, but %i units were passed" + % (func.__name__, count_params, len(args)) + ) + + assigned = tuple( + attr for attr in functools.WRAPPER_ASSIGNMENTS if hasattr(func, attr) + ) + updated = tuple( + attr for attr in functools.WRAPPER_UPDATES if hasattr(func, attr) + ) + + @functools.wraps(func, assigned=assigned, updated=updated) + def wrapper(*values, **kw) -> Quantity: + values, kw = _apply_defaults(sig, values, kw) + + # In principle, the values are used as is + # When then extract the magnitudes when needed. + new_values, new_kw, values_by_name = converter( + ureg, sig, values, kw, strict + ) + + result = func(*new_values, **new_kw) + + if is_ret_container: + out_units = ( + _replace_units(r, values_by_name) if is_ref else r + for (r, is_ref) in ret + ) + return ret.__class__( + res if unit is None else ureg.Quantity(res, unit) + for unit, res in zip_longest(out_units, result) + ) + + if ret[0] is None: + return result + + return ureg.Quantity( + result, _replace_units(ret[0], values_by_name) if ret[1] else ret[0] + ) + + return wrapper + + return decorator + + +def check( + ureg: UnitRegistry, *args: str | UnitsContainer | Unit | None +) -> Callable[[F], F]: + """Decorator to for quantity type checking for function inputs. + + Use it to ensure that the decorated function input parameters match + the expected dimension of pint quantity. + + The wrapper function raises: + - `pint.DimensionalityError` if an argument doesn't match the required dimensions. + + ureg : UnitRegistry + a UnitRegistry instance. + args : str or UnitContainer or None + Dimensions of each of the input arguments. + Use `None` to skip argument conversion. + + Returns + ------- + callable + the wrapped function. + + Raises + ------ + TypeError + If the number of given dimensions does not match the number of function + parameters. + ValueError + If the any of the provided dimensions cannot be parsed as a dimension. + """ + dimensions = [ + ureg.get_dimensionality(dim) if dim is not None else None for dim in args + ] + + def decorator(func): + sig = signature(func) + count_params = len(sig.parameters) + if len(dimensions) != count_params: + raise TypeError( + "%s takes %i parameters, but %i dimensions were passed" + % (func.__name__, count_params, len(dimensions)) + ) + + assigned = tuple( + attr for attr in functools.WRAPPER_ASSIGNMENTS if hasattr(func, attr) + ) + updated = tuple( + attr for attr in functools.WRAPPER_UPDATES if hasattr(func, attr) + ) + + @functools.wraps(func, assigned=assigned, updated=updated) + def wrapper(*args, **kwargs): + list_args, kw = _apply_defaults(sig, args, kwargs) + + for i, param_name in enumerate(sig.parameters): + if i >= len(args): + list_args.append(kw[param_name]) + + for dim, value in zip(dimensions, list_args): + if dim is None: + continue + + if not ureg.Quantity(value).check(dim): + val_dim = ureg.get_dimensionality(value) + raise DimensionalityError(value, "a quantity of", val_dim, dim) + return func(*args, **kwargs) + + return wrapper + + return decorator diff --git a/datasette/vendored/pint/toktest.py b/datasette/vendored/pint/toktest.py new file mode 100644 index 0000000000..808305e26f --- /dev/null +++ b/datasette/vendored/pint/toktest.py @@ -0,0 +1,32 @@ +from __future__ import annotations + +import tokenize + +from datasette.vendored.pint.pint_eval import _plain_tokenizer, uncertainty_tokenizer + +tokenizer = _plain_tokenizer + +input_lines = [ + "( 8.0 + / - 4.0 ) e6 m", + "( 8.0 ± 4.0 ) e6 m", + "( 8.0 + / - 4.0 ) e-6 m", + "( nan + / - 0 ) e6 m", + "( nan ± 4.0 ) m", + "8.0 + / - 4.0 m", + "8.0 ± 4.0 m", + "8.0(4)m", + "8.0(.4)m", + "8.0(-4)m", # error! + "pint == wonderfulness ^ N + - + / - * ± m J s", +] + +for line in input_lines: + result = [] + g = list(uncertainty_tokenizer(line)) # tokenize the string + for toknum, tokval, _, _, _ in g: + result.append((toknum, tokval)) + + print("====") + print(f"input line: {line}") + print(result) + print(tokenize.untokenize(result)) diff --git a/datasette/vendored/pint/util.py b/datasette/vendored/pint/util.py new file mode 100644 index 0000000000..3f48b14a5c --- /dev/null +++ b/datasette/vendored/pint/util.py @@ -0,0 +1,1175 @@ +""" + pint.util + ~~~~~~~~~ + + Miscellaneous functions for pint. + + :copyright: 2016 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import logging +import math +import operator +import re +import tokenize +import types +from collections.abc import Callable, Generator, Hashable, Iterable, Iterator, Mapping +from fractions import Fraction +from functools import lru_cache, partial +from logging import NullHandler +from numbers import Number +from token import NAME, NUMBER +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + TypeVar, +) + +from . import pint_eval +from ._typing import Scalar +from .compat import NUMERIC_TYPES, Self +from .errors import DefinitionSyntaxError +from .pint_eval import build_eval_tree + +if TYPE_CHECKING: + from ._typing import QuantityOrUnitLike + from .registry import UnitRegistry + + +logger = logging.getLogger(__name__) +logger.addHandler(NullHandler()) + +T = TypeVar("T") +TH = TypeVar("TH", bound=Hashable) +TT = TypeVar("TT", bound=type) + +# TODO: Change when Python 3.10 becomes minimal version. +# ItMatrix: TypeAlias = Iterable[Iterable[PintScalar]] +# Matrix: TypeAlias = list[list[PintScalar]] +ItMatrix = Iterable[Iterable[Scalar]] +Matrix = list[list[Scalar]] + + +def _noop(x: T) -> T: + return x + + +def matrix_to_string( + matrix: ItMatrix, + row_headers: Iterable[str] | None = None, + col_headers: Iterable[str] | None = None, + fmtfun: Callable[ + [ + Scalar, + ], + str, + ] = "{:0.0f}".format, +) -> str: + """Return a string representation of a matrix. + + Parameters + ---------- + matrix + A matrix given as an iterable of an iterable of numbers. + row_headers + An iterable of strings to serve as row headers. + (default = None, meaning no row headers are printed.) + col_headers + An iterable of strings to serve as column headers. + (default = None, meaning no col headers are printed.) + fmtfun + A callable to convert a number into string. + (default = `"{:0.0f}".format`) + + Returns + ------- + str + String representation of the matrix. + """ + ret: list[str] = [] + if col_headers: + ret.append(("\t" if row_headers else "") + "\t".join(col_headers)) + if row_headers: + ret += [ + rh + "\t" + "\t".join(fmtfun(f) for f in row) + for rh, row in zip(row_headers, matrix) + ] + else: + ret += ["\t".join(fmtfun(f) for f in row) for row in matrix] + + return "\n".join(ret) + + +def transpose(matrix: ItMatrix) -> Matrix: + """Return the transposed version of a matrix. + + Parameters + ---------- + matrix + A matrix given as an iterable of an iterable of numbers. + + Returns + ------- + Matrix + The transposed version of the matrix. + """ + return [list(val) for val in zip(*matrix)] + + +def matrix_apply( + matrix: ItMatrix, + func: Callable[ + [ + Scalar, + ], + Scalar, + ], +) -> Matrix: + """Apply a function to individual elements within a matrix. + + Parameters + ---------- + matrix + A matrix given as an iterable of an iterable of numbers. + func + A callable that converts a number to another. + + Returns + ------- + A new matrix in which each element has been replaced by new one. + """ + return [[func(x) for x in row] for row in matrix] + + +def column_echelon_form( + matrix: ItMatrix, ntype: type = Fraction, transpose_result: bool = False +) -> tuple[Matrix, Matrix, list[int]]: + """Calculate the column echelon form using Gaussian elimination. + + Parameters + ---------- + matrix + A 2D matrix as nested list. + ntype + The numerical type to use in the calculation. + (default = Fraction) + transpose_result + Indicates if the returned matrix should be transposed. + (default = False) + + Returns + ------- + ech_matrix + Column echelon form. + id_matrix + Transformed identity matrix. + swapped + Swapped rows. + """ + + _transpose: Callable[ + [ + ItMatrix, + ], + Matrix, + ] = ( + transpose if transpose_result else _noop + ) + + ech_matrix = matrix_apply( + transpose(matrix), + lambda x: ntype.from_float(x) if isinstance(x, float) else ntype(x), # type: ignore + ) + + rows, cols = len(ech_matrix), len(ech_matrix[0]) + # M = [[ntype(x) for x in row] for row in M] + id_matrix: list[list[Scalar]] = [ # noqa: E741 + [ntype(1) if n == nc else ntype(0) for nc in range(rows)] for n in range(rows) + ] + + swapped: list[int] = [] + lead = 0 + for r in range(rows): + if lead >= cols: + return _transpose(ech_matrix), _transpose(id_matrix), swapped + s = r + while ech_matrix[s][lead] == 0: # type: ignore + s += 1 + if s != rows: + continue + s = r + lead += 1 + if cols == lead: + return _transpose(ech_matrix), _transpose(id_matrix), swapped + + ech_matrix[s], ech_matrix[r] = ech_matrix[r], ech_matrix[s] + id_matrix[s], id_matrix[r] = id_matrix[r], id_matrix[s] + + swapped.append(s) + lv = ech_matrix[r][lead] + ech_matrix[r] = [mrx / lv for mrx in ech_matrix[r]] + id_matrix[r] = [mrx / lv for mrx in id_matrix[r]] + + for s in range(rows): + if s == r: + continue + lv = ech_matrix[s][lead] + ech_matrix[s] = [ + iv - lv * rv for rv, iv in zip(ech_matrix[r], ech_matrix[s]) + ] + id_matrix[s] = [iv - lv * rv for rv, iv in zip(id_matrix[r], id_matrix[s])] + + lead += 1 + + return _transpose(ech_matrix), _transpose(id_matrix), swapped + + +def pi_theorem(quantities: dict[str, Any], registry: UnitRegistry | None = None): + """Builds dimensionless quantities using the Buckingham π theorem + + Parameters + ---------- + quantities : dict + mapping between variable name and units + registry : + (default value = None) + + Returns + ------- + type + a list of dimensionless quantities expressed as dicts + + """ + + # Preprocess input and build the dimensionality Matrix + quant = [] + dimensions = set() + + if registry is None: + getdim = _noop + non_int_type = float + else: + getdim = registry.get_dimensionality + non_int_type = registry.non_int_type + + for name, value in quantities.items(): + if isinstance(value, str): + value = ParserHelper.from_string(value, non_int_type=non_int_type) + if isinstance(value, dict): + dims = getdim(registry.UnitsContainer(value)) + elif not hasattr(value, "dimensionality"): + dims = getdim(value) + else: + dims = value.dimensionality + + if not registry and any(not key.startswith("[") for key in dims): + logger.warning( + "A non dimension was found and a registry was not provided. " + "Assuming that it is a dimension name: {}.".format(dims) + ) + + quant.append((name, dims)) + dimensions = dimensions.union(dims.keys()) + + dimensions = list(dimensions) + + # Calculate dimensionless quantities + matrix = [ + [dimensionality[dimension] for name, dimensionality in quant] + for dimension in dimensions + ] + + ech_matrix, id_matrix, pivot = column_echelon_form(matrix, transpose_result=False) + + # Collect results + # Make all numbers integers and minimize the number of negative exponents. + # Remove zeros + results = [] + for rowm, rowi in zip(ech_matrix, id_matrix): + if any(el != 0 for el in rowm): + continue + max_den = max(f.denominator for f in rowi) + neg = -1 if sum(f < 0 for f in rowi) > sum(f > 0 for f in rowi) else 1 + results.append( + { + q[0]: neg * f.numerator * max_den / f.denominator + for q, f in zip(quant, rowi) + if f.numerator != 0 + } + ) + return results + + +def solve_dependencies( + dependencies: dict[TH, set[TH]], +) -> Generator[set[TH], None, None]: + """Solve a dependency graph. + + Parameters + ---------- + dependencies : + dependency dictionary. For each key, the value is an iterable indicating its + dependencies. + + Yields + ------ + set + iterator of sets, each containing keys of independents tasks dependent only of + the previous tasks in the list. + + Raises + ------ + ValueError + if a cyclic dependency is found. + """ + while dependencies: + # values not in keys (items without dep) + t = {i for v in dependencies.values() for i in v} - dependencies.keys() + # and keys without value (items without dep) + t.update(k for k, v in dependencies.items() if not v) + # can be done right away + if not t: + raise ValueError( + "Cyclic dependencies exist among these items: {}".format( + ", ".join(repr(x) for x in dependencies.items()) + ) + ) + # and cleaned up + dependencies = {k: v - t for k, v in dependencies.items() if v} + yield t + + +def find_shortest_path( + graph: dict[TH, set[TH]], start: TH, end: TH, path: list[TH] | None = None +): + """Find shortest path between two nodes within a graph. + + Parameters + ---------- + graph + A graph given as a mapping of nodes + to a set of all connected nodes to it. + start + Starting node. + end + End node. + path + Path to prepend to the one found. + (default = None, empty path.) + + Returns + ------- + list[TH] + The shortest path between two nodes. + """ + path = (path or []) + [start] + if start == end: + return path + + # TODO: raise ValueError when start not in graph + if start not in graph: + return None + + shortest = None + for node in graph[start]: + if node not in path: + newpath = find_shortest_path(graph, node, end, path) + if newpath: + if not shortest or len(newpath) < len(shortest): + shortest = newpath + + return shortest + + +def find_connected_nodes( + graph: dict[TH, set[TH]], start: TH, visited: set[TH] | None = None +) -> set[TH] | None: + """Find all nodes connected to a start node within a graph. + + Parameters + ---------- + graph + A graph given as a mapping of nodes + to a set of all connected nodes to it. + start + Starting node. + visited + Mutable set to collect visited nodes. + (default = None, empty set) + + Returns + ------- + set[TH] + The shortest path between two nodes. + """ + + # TODO: raise ValueError when start not in graph + if start not in graph: + return None + + visited = visited or set() + visited.add(start) + + for node in graph[start]: + if node not in visited: + find_connected_nodes(graph, node, visited) + + return visited + + +class udict(dict[str, Scalar]): + """Custom dict implementing __missing__.""" + + def __missing__(self, key: str): + return 0 + + def copy(self: Self) -> Self: + return udict(self) + + +class UnitsContainer(Mapping[str, Scalar]): + """The UnitsContainer stores the product of units and their respective + exponent and implements the corresponding operations. + + UnitsContainer is a read-only mapping. All operations (even in place ones) + return new instances. + + Parameters + ---------- + non_int_type + Numerical type used for non integer values. + """ + + __slots__ = ("_d", "_hash", "_one", "_non_int_type") + + _d: udict + _hash: int | None + _one: Scalar + _non_int_type: type + + def __init__( + self, *args: Any, non_int_type: type | None = None, **kwargs: Any + ) -> None: + if args and isinstance(args[0], UnitsContainer): + default_non_int_type = args[0]._non_int_type + else: + default_non_int_type = float + + self._non_int_type = non_int_type or default_non_int_type + + if self._non_int_type is float: + self._one = 1 + else: + self._one = self._non_int_type("1") + + d = udict(*args, **kwargs) + self._d = d + for key, value in d.items(): + if not isinstance(key, str): + raise TypeError(f"key must be a str, not {type(key)}") + if not isinstance(value, Number): + raise TypeError(f"value must be a number, not {type(value)}") + if not isinstance(value, int) and not isinstance(value, self._non_int_type): + d[key] = self._non_int_type(value) + self._hash = None + + def copy(self: Self) -> Self: + """Create a copy of this UnitsContainer.""" + return self.__copy__() + + def add(self: Self, key: str, value: Number) -> Self: + """Create a new UnitsContainer adding value to + the value existing for a given key. + + Parameters + ---------- + key + unit to which the value will be added. + value + value to be added. + + Returns + ------- + UnitsContainer + A copy of this container. + """ + newval = self._d[key] + self._normalize_nonfloat_value(value) + new = self.copy() + if newval: + new._d[key] = newval + else: + new._d.pop(key) + new._hash = None + return new + + def remove(self: Self, keys: Iterable[str]) -> Self: + """Create a new UnitsContainer purged from given entries. + + Parameters + ---------- + keys + Iterable of keys (units) to remove. + + Returns + ------- + UnitsContainer + A copy of this container. + """ + new = self.copy() + for k in keys: + new._d.pop(k) + new._hash = None + return new + + def rename(self: Self, oldkey: str, newkey: str) -> Self: + """Create a new UnitsContainer in which an entry has been renamed. + + Parameters + ---------- + oldkey + Existing key (unit). + newkey + New key (unit). + + Returns + ------- + UnitsContainer + A copy of this container. + """ + new = self.copy() + new._d[newkey] = new._d.pop(oldkey) + new._hash = None + return new + + def unit_items(self) -> Iterable[tuple[str, Scalar]]: + return self._d.items() + + def __iter__(self) -> Iterator[str]: + return iter(self._d) + + def __len__(self) -> int: + return len(self._d) + + def __getitem__(self, key: str) -> Scalar: + return self._d[key] + + def __contains__(self, key: str) -> bool: + return key in self._d + + def __hash__(self) -> int: + if self._hash is None: + self._hash = hash(frozenset(self._d.items())) + return self._hash + + # Only needed by pickle protocol 0 and 1 (used by pytables) + def __getstate__(self) -> tuple[udict, Scalar, type]: + return self._d, self._one, self._non_int_type + + def __setstate__(self, state: tuple[udict, Scalar, type]): + self._d, self._one, self._non_int_type = state + self._hash = None + + def __eq__(self, other: Any) -> bool: + if isinstance(other, UnitsContainer): + # UnitsContainer.__hash__(self) is not the same as hash(self); see + # ParserHelper.__hash__ and __eq__. + # Different hashes guarantee that the actual contents are different, but + # identical hashes give no guarantee of equality. + # e.g. in CPython, hash(-1) == hash(-2) + if UnitsContainer.__hash__(self) != UnitsContainer.__hash__(other): + return False + other = other._d + + elif isinstance(other, str): + try: + other = ParserHelper.from_string(other, self._non_int_type) + except DefinitionSyntaxError: + return False + + other = other._d + + return dict.__eq__(self._d, other) + + def __str__(self) -> str: + return self.__format__("") + + def __repr__(self) -> str: + tmp = "{%s}" % ", ".join( + [f"'{key}': {value}" for key, value in sorted(self._d.items())] + ) + return f"" + + def __format__(self, spec: str) -> str: + # TODO: provisional + from .formatting import format_unit + + return format_unit(self, spec) + + def format_babel(self, spec: str, registry=None, **kwspec) -> str: + # TODO: provisional + from .formatting import format_unit + + return format_unit(self, spec, registry=registry, **kwspec) + + def __copy__(self): + # Skip expensive health checks performed by __init__ + out = object.__new__(self.__class__) + out._d = self._d.copy() + out._hash = self._hash + out._non_int_type = self._non_int_type + out._one = self._one + return out + + def __mul__(self, other: Any): + if not isinstance(other, self.__class__): + err = "Cannot multiply UnitsContainer by {}" + raise TypeError(err.format(type(other))) + + new = self.copy() + for key, value in other.items(): + new._d[key] += value + if new._d[key] == 0: + del new._d[key] + + new._hash = None + return new + + __rmul__ = __mul__ + + def __pow__(self, other: Any): + if not isinstance(other, NUMERIC_TYPES): + err = "Cannot power UnitsContainer by {}" + raise TypeError(err.format(type(other))) + + new = self.copy() + for key, value in new._d.items(): + new._d[key] *= other + new._hash = None + return new + + def __truediv__(self, other: Any): + if not isinstance(other, self.__class__): + err = "Cannot divide UnitsContainer by {}" + raise TypeError(err.format(type(other))) + + new = self.copy() + for key, value in other.items(): + new._d[key] -= self._normalize_nonfloat_value(value) + if new._d[key] == 0: + del new._d[key] + + new._hash = None + return new + + def __rtruediv__(self, other: Any): + if not isinstance(other, self.__class__) and other != 1: + err = "Cannot divide {} by UnitsContainer" + raise TypeError(err.format(type(other))) + + return self**-1 + + def _normalize_nonfloat_value(self, value: Scalar) -> Scalar: + if not isinstance(value, int) and not isinstance(value, self._non_int_type): + return self._non_int_type(value) # type: ignore[no-any-return] + return value + + +class ParserHelper(UnitsContainer): + """The ParserHelper stores in place the product of variables and + their respective exponent and implements the corresponding operations. + It also provides a scaling factor. + + For example: + `3 * m ** 2` becomes ParserHelper(3, m=2) + + Briefly is a UnitsContainer with a scaling factor. + + ParserHelper is a read-only mapping. All operations (even in place ones) + return new instances. + + WARNING : The hash value used does not take into account the scale + attribute so be careful if you use it as a dict key and then two unequal + object can have the same hash. + + Parameters + ---------- + scale + Scaling factor. + (default = 1) + **kwargs + Used to populate the dict of units and exponents. + """ + + __slots__ = ("scale",) + + scale: Scalar + + def __init__(self, scale: Scalar = 1, *args, **kwargs): + super().__init__(*args, **kwargs) + self.scale = scale + + @classmethod + def from_word(cls, input_word: str, non_int_type: type = float) -> ParserHelper: + """Creates a ParserHelper object with a single variable with exponent one. + + Equivalent to: ParserHelper(1, {input_word: 1}) + + Parameters + ---------- + input_word + + non_int_type + Numerical type used for non integer values. + """ + if non_int_type is float: + return cls(1, [(input_word, 1)], non_int_type=non_int_type) + else: + ONE = non_int_type("1") + return cls(ONE, [(input_word, ONE)], non_int_type=non_int_type) + + @classmethod + def eval_token(cls, token: tokenize.TokenInfo, non_int_type: type = float): + token_type = token.type + token_text = token.string + if token_type == NUMBER: + if non_int_type is float: + try: + return int(token_text) + except ValueError: + return float(token_text) + else: + return non_int_type(token_text) + elif token_type == NAME: + return ParserHelper.from_word(token_text, non_int_type=non_int_type) + else: + raise Exception("unknown token type") + + @classmethod + @lru_cache + def from_string(cls, input_string: str, non_int_type: type = float) -> ParserHelper: + """Parse linear expression mathematical units and return a quantity object. + + Parameters + ---------- + input_string + + non_int_type + Numerical type used for non integer values. + """ + if not input_string: + return cls(non_int_type=non_int_type) + + input_string = string_preprocessor(input_string) + if "[" in input_string: + input_string = input_string.replace("[", "__obra__").replace( + "]", "__cbra__" + ) + reps = True + else: + reps = False + + gen = pint_eval.tokenizer(input_string) + ret = build_eval_tree(gen).evaluate( + partial(cls.eval_token, non_int_type=non_int_type) + ) + + if isinstance(ret, Number): + return cls(ret, non_int_type=non_int_type) + + if reps: + ret = cls( + ret.scale, + { + key.replace("__obra__", "[").replace("__cbra__", "]"): value + for key, value in ret.items() + }, + non_int_type=non_int_type, + ) + + for k in list(ret): + if k.lower() == "nan": + del ret._d[k] + ret.scale = non_int_type(math.nan) + + return ret + + def __copy__(self): + new = super().__copy__() + new.scale = self.scale + return new + + def copy(self): + return self.__copy__() + + def __hash__(self): + if self.scale != 1: + mess = "Only scale 1 ParserHelper instance should be considered hashable" + raise ValueError(mess) + return super().__hash__() + + # Only needed by pickle protocol 0 and 1 (used by pytables) + def __getstate__(self): + return super().__getstate__() + (self.scale,) + + def __setstate__(self, state): + super().__setstate__(state[:-1]) + self.scale = state[-1] + + def __eq__(self, other: Any) -> bool: + if isinstance(other, ParserHelper): + return self.scale == other.scale and super().__eq__(other) + elif isinstance(other, str): + return self == ParserHelper.from_string(other, self._non_int_type) + elif isinstance(other, Number): + return self.scale == other and not len(self._d) + + return self.scale == 1 and super().__eq__(other) + + def operate(self, items, op=operator.iadd, cleanup: bool = True): + d = udict(self._d) + for key, value in items: + d[key] = op(d[key], value) + + if cleanup: + keys = [key for key, value in d.items() if value == 0] + for key in keys: + del d[key] + + return self.__class__(self.scale, d, non_int_type=self._non_int_type) + + def __str__(self): + tmp = "{%s}" % ", ".join( + [f"'{key}': {value}" for key, value in sorted(self._d.items())] + ) + return f"{self.scale} {tmp}" + + def __repr__(self): + tmp = "{%s}" % ", ".join( + [f"'{key}': {value}" for key, value in sorted(self._d.items())] + ) + return f"" + + def __mul__(self, other): + if isinstance(other, str): + new = self.add(other, self._one) + elif isinstance(other, Number): + new = self.copy() + new.scale *= other + elif isinstance(other, self.__class__): + new = self.operate(other.items()) + new.scale *= other.scale + else: + new = self.operate(other.items()) + return new + + __rmul__ = __mul__ + + def __pow__(self, other): + d = self._d.copy() + for key in self._d: + d[key] *= other + return self.__class__(self.scale**other, d, non_int_type=self._non_int_type) + + def __truediv__(self, other): + if isinstance(other, str): + new = self.add(other, -1) + elif isinstance(other, Number): + new = self.copy() + new.scale /= other + elif isinstance(other, self.__class__): + new = self.operate(other.items(), operator.sub) + new.scale /= other.scale + else: + new = self.operate(other.items(), operator.sub) + return new + + __floordiv__ = __truediv__ + + def __rtruediv__(self, other): + new = self.__pow__(-1) + if isinstance(other, str): + new = new.add(other, self._one) + elif isinstance(other, Number): + new.scale *= other + elif isinstance(other, self.__class__): + new = self.operate(other.items(), operator.add) + new.scale *= other.scale + else: + new = new.operate(other.items(), operator.add) + return new + + +#: List of regex substitution pairs. +_subs_re_list = [ + ("\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"), + (r"cubic ({})", r"\1**3"), + (r"square ({})", r"\1**2"), + (r"sq ({})", r"\1**2"), + ( + 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 +] + +#: Compiles the regex and replace {} by a regex that matches an identifier. +_subs_re = [ + (re.compile(a.format(r"[_a-zA-Z][_a-zA-Z0-9]*")), b) for a, b in _subs_re_list +] +_pretty_table = str.maketrans("⁰¹²³⁴⁵⁶⁷⁸⁹·⁻", "0123456789*-") +_pretty_exp_re = re.compile(r"(⁻?[⁰¹²³⁴⁵⁶⁷⁸⁹]+(?:\.[⁰¹²³⁴⁵⁶⁷⁸⁹]*)?)") + + +def string_preprocessor(input_string: str) -> str: + input_string = input_string.replace(",", "") + input_string = input_string.replace(" per ", "/") + + for a, b in _subs_re: + input_string = a.sub(b, input_string) + + input_string = _pretty_exp_re.sub(r"**(\1)", input_string) + # Replace pretty format characters + input_string = input_string.translate(_pretty_table) + + # Handle caret exponentiation + input_string = input_string.replace("^", "**") + return input_string + + +def _is_dim(name: str) -> bool: + return name[0] == "[" and name[-1] == "]" + + +class SharedRegistryObject: + """Base class for object keeping a reference to the registree. + + Such object are for now Quantity and Unit, in a number of places it is + that an object from this class has a '_units' attribute. + + Parameters + ---------- + + Returns + ------- + + """ + + _REGISTRY: ClassVar[UnitRegistry] + _units: UnitsContainer + + def __new__(cls, *args, **kwargs): + inst = object.__new__(cls) + if not hasattr(cls, "_REGISTRY"): + # Base class, not subclasses dynamically by + # UnitRegistry._init_dynamic_classes + from . import application_registry + + inst._REGISTRY = application_registry.get() + return inst + + def _check(self, other: Any) -> bool: + """Check if the other object use a registry and if so that it is the + same registry. + + Parameters + ---------- + other + + Returns + ------- + bool + + Raises + ------ + ValueError + if other don't use the same unit registry. + """ + if self._REGISTRY is getattr(other, "_REGISTRY", None): + return True + + elif isinstance(other, SharedRegistryObject): + mess = "Cannot operate with {} and {} of different registries." + raise ValueError( + mess.format(self.__class__.__name__, other.__class__.__name__) + ) + else: + return False + + +class PrettyIPython: + """Mixin to add pretty-printers for IPython""" + + default_format: str + + def _repr_html_(self) -> str: + if "~" in self._REGISTRY.formatter.default_format: + return f"{self:~H}" + return f"{self:H}" + + def _repr_latex_(self) -> str: + if "~" in self._REGISTRY.formatter.default_format: + return f"${self:~L}$" + return f"${self:L}$" + + def _repr_pretty_(self, p, cycle: bool): + # if cycle: + if "~" in self._REGISTRY.formatter.default_format: + p.text(f"{self:~P}") + else: + p.text(f"{self:P}") + # else: + # p.pretty(self.magnitude) + # p.text(" ") + # p.pretty(self.units) + + +def to_units_container( + unit_like: QuantityOrUnitLike, registry: UnitRegistry | None = None +) -> UnitsContainer: + """Convert a unit compatible type to a UnitsContainer. + + Parameters + ---------- + unit_like + Quantity or Unit to infer the plain units from. + registry + If provided, uses the registry's UnitsContainer and parse_unit_name. If None, + uses the registry attached to unit_like. + + Returns + ------- + UnitsContainer + """ + mro = type(unit_like).mro() + if UnitsContainer in mro: + return unit_like + elif SharedRegistryObject in mro: + return unit_like._units + elif str in mro: + if registry: + # TODO: document how to whether to lift preprocessing loop out to caller + for p in registry.preprocessors: + unit_like = p(unit_like) + return registry.parse_units_as_container(unit_like) + else: + return ParserHelper.from_string(unit_like) + elif dict in mro: + if registry: + return registry.UnitsContainer(unit_like) + else: + return UnitsContainer(unit_like) + + +def infer_base_unit( + unit_like: QuantityOrUnitLike, registry: UnitRegistry | None = None +) -> UnitsContainer: + """ + Given a Quantity or UnitLike, give the UnitsContainer for it's plain units. + + Parameters + ---------- + unit_like + Quantity or Unit to infer the plain units from. + registry + If provided, uses the registry's UnitsContainer and parse_unit_name. If None, + uses the registry attached to unit_like. + + Returns + ------- + UnitsContainer + + Raises + ------ + ValueError + The unit_like did not reference a registry, and no registry was provided. + + """ + d = udict() + + original_units = to_units_container(unit_like, registry) + + if registry is None and hasattr(unit_like, "_REGISTRY"): + registry = unit_like._REGISTRY + if registry is None: + raise ValueError("No registry provided.") + + for unit_name, power in original_units.items(): + candidates = registry.parse_unit_name(unit_name) + assert len(candidates) == 1 + _, base_unit, _ = candidates[0] + d[base_unit] += power + + # remove values that resulted in a power of 0 + nonzero_dict = {k: v for k, v in d.items() if v != 0} + + return registry.UnitsContainer(nonzero_dict) + + +def getattr_maybe_raise(obj: Any, item: str): + """Helper function invoked at start of all overridden ``__getattr__``. + + Raise AttributeError if the user tries to ask for a _ or __ attribute, + *unless* it is immediately followed by a number, to enable units + encompassing constants, such as ``L / _100km``. + + Parameters + ---------- + item + attribute to be found. + + Raises + ------ + AttributeError + """ + # Double-underscore attributes are tricky to detect because they are + # automatically prefixed with the class name - which may be a subclass of obj + if ( + item.endswith("__") + or len(item.lstrip("_")) == 0 + or (item.startswith("_") and not item.lstrip("_")[0].isdigit()) + ): + raise AttributeError(f"{obj!r} object has no attribute {item!r}") + + +def iterable(y: Any) -> bool: + """Check whether or not an object can be iterated over.""" + try: + iter(y) + except TypeError: + return False + return True + + +def sized(y: Any) -> bool: + """Check whether or not an object has a defined length.""" + try: + len(y) + except TypeError: + return False + return True + + +def create_class_with_registry( + registry: UnitRegistry, base_class: type[TT] +) -> type[TT]: + """Create new class inheriting from base_class and + filling _REGISTRY class attribute with an actual instanced registry. + """ + + class_body = { + "__module__": "pint", + "_REGISTRY": registry, + } + + return types.new_class( + base_class.__name__, + bases=(base_class,), + exec_body=lambda ns: ns.update(class_body), + ) diff --git a/datasette/vendored/pint/xtranslated.txt b/datasette/vendored/pint/xtranslated.txt new file mode 100644 index 0000000000..b2d16163d4 --- /dev/null +++ b/datasette/vendored/pint/xtranslated.txt @@ -0,0 +1,18 @@ + +# a few unit definitions added to use the translations by unicode cldr + +dietary_calorie = 1000 * calorie = Cal = Calorie +metric_cup = liter / 4 +square_meter = meter ** 2 = sq_m +square_kilometer = kilometer ** 2 = sq_km +mile_scandinavian = 10000 * meter +cubic_mile = 1 * mile ** 3 = cu_mile = cubic_miles +cubic_meter = 1 * meter ** 3 = cu_m +cubic_kilometer = 1 * kilometer ** 3 = cu_km + +[consumption] = [volume] / [length] +liter_per_kilometer = liter / kilometer +liter_per_100kilometers = liter / (100 * kilometers) + +[US_consumption] = [length] / [volume] +MPG = mile / gallon diff --git a/datasette/views/base.py b/datasette/views/base.py index 0ccf3c9c4b..d20421d385 100644 --- a/datasette/views/base.py +++ b/datasette/views/base.py @@ -8,7 +8,7 @@ from markupsafe import escape -import pint +from datasette.vendored import pint from datasette import __version__ from datasette.database import QueryInterrupted diff --git a/pytest.ini b/pytest.ini index 559e518c74..fa01b24921 100644 --- a/pytest.ini +++ b/pytest.ini @@ -9,3 +9,4 @@ filterwarnings= markers = serial: tests to avoid using with pytest-xdist asyncio_mode = strict +asyncio_default_fixture_loop_scope = function \ No newline at end of file diff --git a/setup.py b/setup.py index 2514f4ed71..970a6df745 100644 --- a/setup.py +++ b/setup.py @@ -38,7 +38,9 @@ def get_version(): "CI": "https://github.com/simonw/datasette/actions?query=workflow%3ATest", }, packages=find_packages(exclude=("tests",)), - package_data={"datasette": ["templates/*.html"]}, + package_data={ + "datasette": ["templates/*.html", "vendored/**/*.txt"], + }, include_package_data=True, python_requires=">=3.7", install_requires=[ @@ -48,7 +50,6 @@ def get_version(): "Jinja2>=2.10.3", "hupper>=1.9", "httpx>=0.20", - "pint>=0.9", "pluggy>=1.0", "uvicorn>=0.11", "aiofiles>=0.4", @@ -59,6 +60,11 @@ def get_version(): "itsdangerous>=1.1", "setuptools", "pip", + # Needed by our vendored Pint: + "platformdirs>=2.1.0", + "typing_extensions>=4.0.0", + "flexcache>=0.3", + "flexparser>=0.3", ], entry_points=""" [console_scripts] @@ -95,9 +101,10 @@ def get_version(): "Intended Audience :: End Users/Desktop", "Topic :: Database", "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.7", ], ) diff --git a/tests/plugins/my_plugin.py b/tests/plugins/my_plugin.py index c19243951a..b1b814705e 100644 --- a/tests/plugins/my_plugin.py +++ b/tests/plugins/my_plugin.py @@ -5,7 +5,7 @@ from datasette.utils import path_with_added_args from datasette.utils.asgi import asgi_send_json, Response import base64 -import pint +from datasette.vendored import pint import json ureg = pint.UnitRegistry()