diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0797decb5..dfebe5bdc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,18 +7,22 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.10", "3.11", "3.12"] + python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] numpy: [null, "numpy>=1.23,<2.0.0", "numpy>=2.0.0rc1"] uncertainties: [null, "uncertainties==3.1.6", "uncertainties>=3.1.6,<4.0.0"] extras: [null] include: - python-version: "3.10" # Minimal versions - numpy: "numpy" - extras: matplotlib==2.2.5 + numpy: "numpy>=1.23,<2.0.0" + extras: matplotlib==3.5.3 - python-version: "3.10" numpy: "numpy" uncertainties: "uncertainties" - extras: "sparse xarray netCDF4 dask[complete]==2023.4.0 graphviz babel==2.8 mip>=1.13" + extras: "sparse xarray netCDF4 dask[complete]==2024.5.1 graphviz babel==2.8 mip>=1.13" + - python-version: "3.10" + numpy: "numpy==1.26.1" + uncertainties: null + extras: "babel==2.15 matplotlib==3.9.0" runs-on: ubuntu-latest env: diff --git a/CHANGES b/CHANGES index f017a0643..c6753acf5 100644 --- a/CHANGES +++ b/CHANGES @@ -1,26 +1,77 @@ Pint Changelog ============== -0.24 (unreleased) ------------------ +0.25.0 (unreleased) +------------------- - Implement numpy broadcast_array (Related to issue #981) + + +0.24.4 (2024-11-07) +------------------- + +- add error for prefixed non multi units (#1998) +- build: typing_extensions version +- build: switch from appdirs to platformdirs +- fix GenericPlainRegistry getattr type (#2045) +- Replace references to the deprecated `UnitRegistry.default_format` (#2058) +- fix: upgrade to flexparser>=0.4, exceptions are no longer dataclasses. + (required for Python 3.13) + + +0.24.2 (2024-07-28) +------------------- + +- Fix the default behaviour for pint-convert (cli) for importing uncertainties package (PR #2032, Issue #2016) +- Added mu and mc as alternatives for SI micro prefix +- Added ℓ as alternative for liter +- Support permille units and `‰` symbol (PR #2033, Issue #1963) +- Switch from appdirs to platformdirs. +- Fixes issues related to GenericPlainRegistry.__getattr__ type (PR #2038, Issues #1946 and #1804) +- Removed deprecated references in documentation and tests (PR #2058, Issue #2057) + + +0.24.1 (2024-06-24) +----------------- + +- Fix custom formatter needing the registry object. (PR #2011) +- Support python 3.9 following difficulties installing with NumPy 2. (PR #2019) +- Fix default formatting of dimensionless unit issue. (PR #2012) +- Fix bug preventing custom formatters with modifiers working. (PR #2021) + + +0.24 (2024-06-07) +----------------- + +- Fix detection of invalid conversion between offset and delta units. (PR #1905) +- Added dBW, decibel Watts, which is used in RF high power applications - NumPy 2.0 support (PR #1985, #1971) - Implement numpy roll (Related to issue #981) +- Implement numpy correlate + (PR #1990) - Add `dim_sort` function to _formatter_helpers. - Add `dim_order` and `default_sort_func` properties to FullFormatter. (PR #1926, fixes Issue #1841) +- Minimum version requirement added for typing_extensions>=4.0.0. + (PR #1996) +- Documented packages using pint. + (PR #1960) - Fixed bug causing operations between arrays of quantity scalars and quantity holding array resulting in incorrect units. (PR #1677) - Fix LaTeX siuntix formatting when using non_int_type=decimal.Decimal. + (PR #1977) - Added refractive index units. (PR #1816) - Fix converting to offset units of higher dimension e.g. gauge pressure (PR #1949) - Fix unhandled TypeError when auto_reduce_dimensions=True and non_int_type=Decimal (PR #1853) +- Creating prefixed offset units now raises an error. + (PR #1998) +- Improved error message in `get_dimensionality()` when non existent units are passed. + (PR #1874, Issue #1716) 0.23 (2023-12-08) @@ -50,8 +101,7 @@ Pint Changelog (PR #1819) - Add numpy.linalg.norm implementation. (PR #1251) -- Improved error message in `get_dimensionality()` when non existent units are passed. - (PR #1874, Issue #1716) + 0.22 (2023-05-25) ----------------- diff --git a/README.rst b/README.rst index a839fcdd7..3c16a4541 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,6 @@ :alt: Latest Version .. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json - :target: https://github.com/python/black :target: https://github.com/astral-sh/ruff :alt: Ruff diff --git a/docs/_static/style.css b/docs/_static/style.css index b2bc297d6..a2ac3f7fd 100644 --- a/docs/_static/style.css +++ b/docs/_static/style.css @@ -38,8 +38,14 @@ pre, code { .sd-card .sd-card-header { border: none; - color: #150458 !important; + color: #150458; font-size: var(--pst-font-size-h5); font-weight: bold; padding: 2.5rem 0rem 0.5rem 0rem; } + +html[data-theme=dark] { + .sd-card .sd-card-header { + color: #FFF; + } +} diff --git a/docs/advanced/currencies.rst b/docs/advanced/currencies.rst index 26b66b531..addc94785 100644 --- a/docs/advanced/currencies.rst +++ b/docs/advanced/currencies.rst @@ -84,3 +84,16 @@ currency on its own dimension, and then implement transformations:: More sophisticated formulas, e.g. dealing with flat fees and thresholds, can be implemented with arbitrary python code by programmatically defining a context (see :ref:`contexts`). + +Currency Symbols +---------------- + +Many common currency symbols are not supported by the pint parser. A preprocessor can be used as a workaround: + +.. doctest:: + + >>> import pint + >>> ureg = pint.UnitRegistry(preprocessors = [lambda s: s.replace("€", "EUR")]) + >>> ureg.define("euro = [currency] = € = EUR") + >>> print(ureg.Quantity("1 €")) + 1 euro diff --git a/docs/advanced/performance.rst b/docs/advanced/performance.rst index d7b8a0cd5..998cac681 100644 --- a/docs/advanced/performance.rst +++ b/docs/advanced/performance.rst @@ -120,7 +120,7 @@ If you want to use the default cache folder provided by the OS, use **:auto:** >>> import pint >>> ureg = pint.UnitRegistry(cache_folder=":auto:") # doctest: +SKIP -Pint use an included version of appdirs_ to obtain the correct folder, +Pint use an external dependency of platformdirs_ to obtain the correct folder, for example in macOS is `/Users//Library/Caches/pint` In any case, you can check the location of the cache folder. @@ -146,5 +146,5 @@ In any case, you can check the location of the cache folder. .. _`brentq method`: http://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.brentq.html -.. _appdirs: https://pypi.org/project/appdirs/ +.. _platformdirs: https://pypi.org/project/platformdirs .. _flexcache: https://github.com/hgrecco/flexcache/ diff --git a/docs/dev/pint-convert.rst b/docs/dev/pint-convert.rst index dbb0804f4..4ba0ad888 100644 --- a/docs/dev/pint-convert.rst +++ b/docs/dev/pint-convert.rst @@ -77,36 +77,39 @@ With the `uncertainties` package, the experimental uncertainty in the physical constants is considered, and the result is given in compact notation, with the uncertainty in the last figures in parentheses: +The uncertainty can be enabled with `-U` (by default it is not enabled): + +.. code-block:: console + + $ pint-convert -p 20 -U Eh eV + 1 hartree = 27.211386245988(52) eV + .. code-block:: console - $ pint-convert Eh eV + $ pint-convert -U Eh eV 1 hartree = 27.21138624599(5) eV The precision is limited by both the maximum number of significant digits (`-p`) and the maximum number of uncertainty digits (`-u`, 2 by default):: - $ pint-convert -p 20 Eh eV + $ pint-convert -U -p 20 Eh eV 1 hartree = 27.211386245988(52) eV - $ pint-convert -p 20 -u 4 Eh eV + $ pint-convert -U -p 20 -u 4 Eh eV 1 hartree = 27.21138624598847(5207) eV -The uncertainty can be disabled with `-U`): - -.. code-block:: console - - $ pint-convert -p 20 -U Eh eV - 1 hartree = 27.211386245988471444 eV - Correlations between experimental constants are also known, and taken into -account. Use `-C` to disable it: +account if uncertainties `-U` is enabled. Use `-C` to disable it: .. code-block:: console $ pint-convert --sys atomic m_p + 1 proton_mass = 1836.15267344 m_e + + $ pint-convert -U --sys atomic m_p 1 proton_mass = 1836.15267344(11) m_e - $ pint-convert --sys atomic -C m_p + $ pint-convert -U --sys atomic -C m_p 1 proton_mass = 1836.15267344(79) m_e Again, note that results may differ slightly, usually in the last figure, from diff --git a/docs/ecosystem.rst b/docs/ecosystem.rst index 7610fd019..c83c52f49 100644 --- a/docs/ecosystem.rst +++ b/docs/ecosystem.rst @@ -7,5 +7,18 @@ Here is a list of known projects, packages and integrations using pint. Pint integrations: ------------------ +- `ucumvert `_ `UCUM `_ (Unified Code for Units of Measure) integration - `pint-pandas `_ Pandas integration - `pint-xarray `_ Xarray integration + + +Packages using pint: +------------------ + +- `fluids `_ Practical fluid dynamics calculations +- `ht `_ Practical heat transfer calculations +- `chemicals `_ Chemical property calculations and lookups +- `thermo `_ Thermodynamic equilibrium calculations +- `Taurus `_ Control system UI creation +- `InstrumentKit `_ Interacting with laboratory equipment over various buses. +- `NEMO `_ Electricity production cost model diff --git a/docs/getting/tutorial.rst b/docs/getting/tutorial.rst index d675860f2..a0836fe7e 100644 --- a/docs/getting/tutorial.rst +++ b/docs/getting/tutorial.rst @@ -415,7 +415,7 @@ Additionally, you can specify a default format specification: >>> accel = 1.3 * ureg.parse_units('meter/second**2') >>> 'The acceleration is {}'.format(accel) 'The acceleration is 1.3 meter / second ** 2' - >>> ureg.default_format = 'P' + >>> ureg.formatter.default_format = 'P' >>> 'The acceleration is {}'.format(accel) 'The acceleration is 1.3 meter/second²' @@ -446,7 +446,7 @@ and by doing that, string formatting is now localized: .. doctest:: - >>> ureg.default_format = 'P' + >>> ureg.formatter.default_format = 'P' >>> accel = 1.3 * ureg.parse_units('meter/second**2') >>> str(accel) '1,3 mètres par seconde²' diff --git a/docs/user/formatting.rst b/docs/user/formatting.rst index d45fc1e13..fbf2fae42 100644 --- a/docs/user/formatting.rst +++ b/docs/user/formatting.rst @@ -98,7 +98,8 @@ formats: '2.3e-06 kilogram ** -1 * meter ** 3 * second ** -2' where ``unit`` is a :py:class:`dict` subclass containing the unit names and -their exponents. +their exponents, ``registry`` is the current instance of :py:class:``UnitRegistry`` and +``options`` is not yet implemented. You can choose to replace the complete formatter. Briefly, the formatter if an object with the following methods: `format_magnitude`, `format_unit`, `format_quantity`, `format_uncertainty`, diff --git a/docs/user/log_units.rst b/docs/user/log_units.rst index 03e007914..096397350 100644 --- a/docs/user/log_units.rst +++ b/docs/user/log_units.rst @@ -111,16 +111,16 @@ will not work: .. doctest:: >>> -161.0 * ureg('dBm/Hz') == (-161.0 * ureg.dBm / ureg.Hz) - False + np.False_ But this will: .. doctest:: >>> ureg('-161.0 dBm/Hz') == (-161.0 * ureg.dBm / ureg.Hz) - True + np.True_ >>> Q_(-161.0, 'dBm') / ureg.Hz == (-161.0 * ureg.dBm / ureg.Hz) - True + np.True_ To begin using this feature while avoiding problems, define logarithmic units as single-unit quantities and convert them to their base units as quickly as diff --git a/docs/user/nonmult.rst b/docs/user/nonmult.rst index a649d2ad1..905dd0835 100644 --- a/docs/user/nonmult.rst +++ b/docs/user/nonmult.rst @@ -18,7 +18,7 @@ For example, to convert from celsius to fahrenheit: >>> from pint import UnitRegistry >>> ureg = UnitRegistry() - >>> ureg.default_format = '.3f' + >>> ureg.formatter.default_format = '.3f' >>> Q_ = ureg.Quantity >>> home = Q_(25.4, ureg.degC) >>> print(home.to('degF')) diff --git a/pint/compat.py b/pint/compat.py index 277662410..4b4cbab92 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -19,9 +19,13 @@ from typing import ( Any, NoReturn, - TypeAlias, # noqa ) +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: @@ -71,7 +75,8 @@ class BehaviorChangeWarning(UserWarning): try: from uncertainties import UFloat, ufloat - from uncertainties import unumpy as unp + + unp = None HAS_UNCERTAINTIES = True except ImportError: @@ -88,6 +93,8 @@ class BehaviorChangeWarning(UserWarning): 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) @@ -233,6 +240,8 @@ def _to_magnitude(value, force_ndarray=False, force_ndarray_like=False): "xarray.core.variable.Variable", "pandas.core.series.Series", "pandas.core.frame.DataFrame", + "pandas.Series", + "pandas.DataFrame", "xarray.core.dataarray.DataArray", ) diff --git a/pint/default_en.txt b/pint/default_en.txt index 83ea967f6..4250a48cb 100644 --- a/pint/default_en.txt +++ b/pint/default_en.txt @@ -72,7 +72,7 @@ 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- +micro- = 1e-6 = µ- = μ- = u- = mu- = mc- milli- = 1e-3 = m- centi- = 1e-2 = c- deci- = 1e-1 = d- @@ -150,6 +150,7 @@ byte = 8 * bit = B = octet # Ratios percent = 0.01 = % +permille = 0.001 = ‰ ppm = 1e-6 # Length @@ -221,7 +222,7 @@ hectare = 100 * are = ha # Volume [volume] = [length] ** 3 -liter = decimeter ** 3 = l = L = litre +liter = decimeter ** 3 = l = L = ℓ = litre cubic_centimeter = centimeter ** 3 = cc lambda = microliter = λ stere = meter ** 3 @@ -504,6 +505,7 @@ refractive_index_unit = [] = RIU # 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 diff --git a/pint/delegates/formatter/_compound_unit_helpers.py b/pint/delegates/formatter/_compound_unit_helpers.py index 89bda87a2..06a8ac2d3 100644 --- a/pint/delegates/formatter/_compound_unit_helpers.py +++ b/pint/delegates/formatter/_compound_unit_helpers.py @@ -20,12 +20,11 @@ TYPE_CHECKING, Any, Literal, - TypeAlias, TypedDict, TypeVar, ) -from ...compat import babel_parse +from ...compat import TypeAlias, babel_parse from ...util import UnitsContainer T = TypeVar("T") @@ -83,11 +82,19 @@ def localize_per( 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 "{}/{}" - return patterns.get(length, default or "{}/{}") + # babel 2.8 + if isinstance(patterns, str): + return patterns + + # babe; 2.15 + return patterns.get("compound", default or "{}/{}") @functools.lru_cache @@ -257,6 +264,12 @@ def prepare_compount_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( diff --git a/pint/delegates/formatter/_format_helpers.py b/pint/delegates/formatter/_format_helpers.py index 995159e65..8a2f37a59 100644 --- a/pint/delegates/formatter/_format_helpers.py +++ b/pint/delegates/formatter/_format_helpers.py @@ -131,6 +131,8 @@ def join_mu(joint_fstring: str, mstr: str, ustr: str) -> str: 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) diff --git a/pint/delegates/formatter/_to_register.py b/pint/delegates/formatter/_to_register.py index 0f8f46788..697973716 100644 --- a/pint/delegates/formatter/_to_register.py +++ b/pint/delegates/formatter/_to_register.py @@ -61,6 +61,8 @@ def wrapper(func: Callable[[PlainUnit, UnitRegistry], str]): raise ValueError(f"format {name!r} already exists") # or warn instead class NewFormatter(BaseFormatter): + spec = name + def format_magnitude( self, magnitude: Magnitude, diff --git a/pint/delegates/formatter/full.py b/pint/delegates/formatter/full.py index e6d0eee47..d5de43326 100644 --- a/pint/delegates/formatter/full.py +++ b/pint/delegates/formatter/full.py @@ -102,12 +102,20 @@ def get_formatter(self, spec: str): if k in spec: return v - try: - return REGISTERED_FORMATTERS[spec] - except KeyError: - pass + for k, v in REGISTERED_FORMATTERS.items(): + if k in spec: + orphan_fmt = REGISTERED_FORMATTERS[k] + break + else: + return self._formatters["D"] - 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] diff --git a/pint/delegates/txt_defparser/common.py b/pint/delegates/txt_defparser/common.py index ebdabc062..def901d88 100644 --- a/pint/delegates/txt_defparser/common.py +++ b/pint/delegates/txt_defparser/common.py @@ -12,7 +12,7 @@ from __future__ import annotations -from dataclasses import dataclass, field +from dataclasses import dataclass import flexparser as fp @@ -20,7 +20,6 @@ from ..base_defparser import ParserConfig -@dataclass(frozen=True) class DefinitionSyntaxError(errors.DefinitionSyntaxError, fp.ParsingError): """A syntax error was found in a definition. Combines: @@ -30,7 +29,11 @@ class DefinitionSyntaxError(errors.DefinitionSyntaxError, fp.ParsingError): and an extra location attribute in which the filename or reseource is stored. """ - location: str = field(init=False, default="") + msg: str + + def __init__(self, msg: str, location: str = ""): + self.msg = msg + self.location = location def __str__(self) -> str: msg = ( diff --git a/pint/errors.py b/pint/errors.py index 59d3b4569..d1882dbdd 100644 --- a/pint/errors.py +++ b/pint/errors.py @@ -11,7 +11,6 @@ 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" @@ -81,12 +80,10 @@ def def_err(self, msg: str): return DefinitionError(self.name, self.__class__, msg) -@dataclass(frozen=False) class PintError(Exception): """Base exception for all Pint errors.""" -@dataclass(frozen=False) class DefinitionError(ValueError, PintError): """Raised when a definition is not properly constructed.""" @@ -94,69 +91,76 @@ class DefinitionError(ValueError, PintError): definition_type: type msg: str + def __init__(self, name: str, definition_type: type, msg: str): + self.name = name + self.definition_type = definition_type + self.msg = msg + 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)) + return self.__class__, (self.name, self.definition_type, self.msg) -@dataclass(frozen=False) class DefinitionSyntaxError(ValueError, PintError): """Raised when a textual definition has a syntax error.""" msg: str + def __init__(self, msg: str): + self.msg = msg + def __str__(self): return self.msg def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + return self.__class__, (self.msg,) -@dataclass(frozen=False) class RedefinitionError(ValueError, PintError): """Raised when a unit or prefix is redefined.""" name: str definition_type: type + def __init__(self, name: str, definition_type: type): + self.name = name + self.definition_type = definition_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)) + return self.__class__, (self.name, self.definition_type) -@dataclass(frozen=False) class UndefinedUnitError(AttributeError, PintError): """Raised when the units are not defined in the unit registry.""" - unit_names: str | tuple[str, ...] + unit_names: tuple[str, ...] + + def __init__(self, unit_names: str | ty.Iterable[str]): + if isinstance(unit_names, str): + self.unit_names = (unit_names,) + else: + self.unit_names = tuple(unit_names) 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 - ): + if 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)) + return self.__class__, (self.unit_names,) -@dataclass(frozen=False) class PintTypeError(TypeError, PintError): - def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + pass -@dataclass(frozen=False) class DimensionalityError(PintTypeError): """Raised when trying to convert between incompatible units.""" @@ -166,6 +170,20 @@ class DimensionalityError(PintTypeError): dim2: str = "" extra_msg: str = "" + def __init__( + self, + units1: ty.Any, + units2: ty.Any, + dim1: str = "", + dim2: str = "", + extra_msg: str = "", + ) -> None: + self.units1 = units1 + self.units2 = units2 + self.dim1 = dim1 + self.dim2 = dim2 + self.extra_msg = extra_msg + def __str__(self): if self.dim1 or self.dim2: dim1 = f" ({self.dim1})" @@ -180,16 +198,25 @@ def __str__(self): ) def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + return self.__class__, ( + self.units1, + self.units2, + self.dim1, + self.dim2, + self.extra_msg, + ) -@dataclass(frozen=False) class OffsetUnitCalculusError(PintTypeError): """Raised on ambiguous operations with offset units.""" units1: ty.Any units2: ty.Optional[ty.Any] = None + def __init__(self, units1: ty.Any, units2: ty.Optional[ty.Any] = None) -> None: + self.units1 = units1 + self.units2 = units2 + def yield_units(self): yield self.units1 if self.units2: @@ -205,16 +232,19 @@ def __str__(self): ) def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + return self.__class__, (self.units1, self.units2) -@dataclass(frozen=False) class LogarithmicUnitCalculusError(PintTypeError): """Raised on inappropriate operations with logarithmic units.""" units1: ty.Any units2: ty.Optional[ty.Any] = None + def __init__(self, units1: ty.Any, units2: ty.Optional[ty.Any] = None) -> None: + self.units1 = units1 + self.units2 = units2 + def yield_units(self): yield self.units1 if self.units2: @@ -230,26 +260,28 @@ def __str__(self): ) def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + return self.__class__, (self.units1, self.units2) -@dataclass(frozen=False) class UnitStrippedWarning(UserWarning, PintError): msg: str + def __init__(self, msg: str): + self.msg = msg + def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + return self.__class__, (self.msg,) -@dataclass(frozen=False) class UnexpectedScaleInContainer(Exception): - def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + pass -@dataclass(frozen=False) class UndefinedBehavior(UserWarning, PintError): msg: str + def __init__(self, msg: str): + self.msg = msg + def __reduce__(self): - return self.__class__, tuple(getattr(self, f.name) for f in fields(self)) + return self.__class__, (self.msg,) diff --git a/pint/facets/nonmultiplicative/registry.py b/pint/facets/nonmultiplicative/registry.py index d476cc676..7f58d060c 100644 --- a/pint/facets/nonmultiplicative/registry.py +++ b/pint/facets/nonmultiplicative/registry.py @@ -119,7 +119,7 @@ def _is_multiplicative(self, unit_name: str) -> bool: Raises ------ UndefinedUnitError - If the unit is not in the registyr. + If the unit is not in the registry. """ if unit_name in self._units: return self._units[unit_name].is_multiplicative @@ -254,6 +254,7 @@ def _convert( 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) @@ -267,6 +268,8 @@ def _convert( # 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 @@ -274,6 +277,8 @@ def _convert( # 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) diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py index 554be9a7a..6f149433b 100644 --- a/pint/facets/numpy/numpy_func.py +++ b/pint/facets/numpy/numpy_func.py @@ -52,6 +52,10 @@ def _is_sequence_with_quantity_elements(obj): ------- 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) @@ -792,6 +796,15 @@ def _trapz(y, x=None, dx=1.0, **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: diff --git a/pint/facets/plain/registry.py b/pint/facets/plain/registry.py index 09fd220ee..be70a2ca8 100644 --- a/pint/facets/plain/registry.py +++ b/pint/facets/plain/registry.py @@ -49,7 +49,7 @@ # from ..._typing import Quantity, Unit -import appdirs +import platformdirs from ... import pint_eval from ..._typing import ( @@ -60,7 +60,12 @@ UnitLike, ) from ...compat import Self, TypeAlias, deprecated -from ...errors import DimensionalityError, RedefinitionError, UndefinedUnitError +from ...errors import ( + DimensionalityError, + OffsetUnitCalculusError, + RedefinitionError, + UndefinedUnitError, +) from ...pint_eval import build_eval_tree from ...util import ( ParserHelper, @@ -233,8 +238,7 @@ def __init__( self._init_dynamic_classes() if cache_folder == ":auto:": - cache_folder = appdirs.user_cache_dir(appname="pint", appauthor=False) - cache_folder = pathlib.Path(cache_folder) + cache_folder = platformdirs.user_cache_path(appname="pint", appauthor=False) from ... import delegates # TODO: change thiss @@ -255,6 +259,9 @@ def __init__( # 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 @@ -360,7 +367,7 @@ def __deepcopy__(self: Self, memo) -> type[Self]: new._init_dynamic_classes() return new - def __getattr__(self, item: str) -> QuantityT: + def __getattr__(self, item: str) -> UnitT: getattr_maybe_raise(self, item) # self.Unit will call parse_units @@ -664,6 +671,11 @@ def get_name(self, name_or_alias: str, case_sensitive: bool | None = None) -> st ) 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] diff --git a/pint/pint_convert.py b/pint/pint_convert.py old mode 100755 new mode 100644 index bf9097237..0934588b8 --- a/pint/pint_convert.py +++ b/pint/pint_convert.py @@ -42,10 +42,10 @@ ) parser.add_argument( "-U", - "--no-unc", + "--with-unc", dest="unc", - action="store_false", - help="ignore uncertainties in constants", + action="store_true", + help="consider uncertainties in constants", ) parser.add_argument( "-C", @@ -77,7 +77,12 @@ def _set(key: str, value): if args.unc: - import uncertainties + 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 @@ -103,9 +108,14 @@ def _set(key: str, value): [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 - (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 - ) + 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) @@ -160,6 +170,7 @@ def _set(key: str, value): 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: @@ -171,7 +182,8 @@ def convert(u_from, u_to=None, unc=None, factor=None): if factor: q *= ureg.Quantity(factor) nq *= ureg.Quantity(factor).to_base_units() - prec_unc = use_unc(nq.magnitude, fmt, args.prec_unc) + if args.unc: + prec_unc = use_unc(nq.magnitude, fmt, args.prec_unc) if prec_unc > 0: fmt = f".{prec_unc}uS" else: diff --git a/pint/testsuite/conftest.py b/pint/testsuite/conftest.py index 775480f0b..0a42f44af 100644 --- a/pint/testsuite/conftest.py +++ b/pint/testsuite/conftest.py @@ -14,7 +14,7 @@ femto- = 1e-15 = f- pico- = 1e-12 = p- nano- = 1e-9 = n- -micro- = 1e-6 = µ- = μ- = u- +micro- = 1e-6 = µ- = μ- = u- = mu- = mc- milli- = 1e-3 = m- centi- = 1e-2 = c- deci- = 1e-1 = d- diff --git a/pint/testsuite/helpers.py b/pint/testsuite/helpers.py index c9106b75a..d317e0755 100644 --- a/pint/testsuite/helpers.py +++ b/pint/testsuite/helpers.py @@ -128,9 +128,26 @@ def requires_numpy_at_least(version): ) -requires_babel = pytest.mark.skipif( - not HAS_BABEL, reason="Requires Babel with units support" -) +def requires_babel(tested_locales=[]): + if not HAS_BABEL: + return pytest.mark.skip("Requires Babel with units support") + + import locale + + default_locale = locale.getlocale(locale.LC_NUMERIC) + locales_unavailable = False + try: + for loc in tested_locales: + locale.setlocale(locale.LC_NUMERIC, loc) + except locale.Error: + locales_unavailable = True + locale.setlocale(locale.LC_NUMERIC, default_locale) + + return pytest.mark.skipif( + locales_unavailable, reason="Tested locales not available." + ) + + requires_not_babel = pytest.mark.skipif( HAS_BABEL, reason="Requires Babel not to be installed" ) diff --git a/pint/testsuite/test_babel.py b/pint/testsuite/test_babel.py index 2dd66d58d..c68c641e7 100644 --- a/pint/testsuite/test_babel.py +++ b/pint/testsuite/test_babel.py @@ -16,7 +16,7 @@ def test_no_babel(func_registry): distance.format_babel(locale="fr_FR", length="long") -@helpers.requires_babel() +@helpers.requires_babel(["fr_FR", "ro_RO"]) def test_format(func_registry): ureg = func_registry dirname = os.path.dirname(__file__) @@ -36,7 +36,7 @@ def test_format(func_registry): assert mks.format_babel(locale="fr_FR") == "métrique" -@helpers.requires_babel() +@helpers.requires_babel(["fr_FR", "ro_RO"]) def test_registry_locale(): ureg = UnitRegistry(fmt_locale="fr_FR") dirname = os.path.dirname(__file__) @@ -60,13 +60,13 @@ def test_registry_locale(): assert mks.format_babel(locale="fr_FR") == "métrique" -@helpers.requires_babel() +@helpers.requires_babel(["fr_FR"]) def test_unit_format_babel(): ureg = UnitRegistry(fmt_locale="fr_FR") volume = ureg.Unit("ml") assert volume.format_babel() == "millilitre" - ureg.default_format = "~" + ureg.formatter.default_format = "~" assert volume.format_babel() == "ml" dimensionless_unit = ureg.Unit("") @@ -85,7 +85,7 @@ def test_no_registry_locale(func_registry): distance.format_babel() -@helpers.requires_babel() +@helpers.requires_babel(["fr_FR"]) def test_str(func_registry): ureg = func_registry d = 24.1 * ureg.meter diff --git a/pint/testsuite/test_formatting.py b/pint/testsuite/test_formatting.py index e74c09c50..d8f10715b 100644 --- a/pint/testsuite/test_formatting.py +++ b/pint/testsuite/test_formatting.py @@ -59,6 +59,8 @@ def test_split_format(format, default, flag, expected): def test_register_unit_format(func_registry): @fmt.register_unit_format("custom") def format_custom(unit, registry, **options): + # Ensure the registry is correct.. + registry.Unit(unit) return "" quantity = 1.0 * func_registry.meter diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py index 2a0b7edf6..847f269f0 100644 --- a/pint/testsuite/test_issues.py +++ b/pint/testsuite/test_issues.py @@ -7,7 +7,12 @@ import pytest -from pint import Context, DimensionalityError, UnitRegistry, get_application_registry +from pint import ( + Context, + DimensionalityError, + UnitRegistry, + get_application_registry, +) from pint.compat import np from pint.delegates.formatter._compound_unit_helpers import sort_by_dimensionality from pint.facets.plain.unit import UnitsContainer @@ -403,6 +408,15 @@ def test_micro_creation_U03bc(self, module_registry): def test_micro_creation_U00b5(self, module_registry): module_registry.Quantity(2, "µm") + def test_micro_creation_mu(self, module_registry): + module_registry.Quantity(2, "mug") + + def test_micro_creation_mc(self, module_registry): + module_registry.Quantity(2, "mcg") + + def test_liter_creation_U2113(self, module_registry): + module_registry.Quantity(2, "ℓ") + @helpers.requires_numpy def test_issue171_real_imag(self, module_registry): qr = [1.0, 2.0, 3.0, 4.0] * module_registry.meter @@ -879,6 +893,24 @@ def test_issue1277(self, module_registry): assert c.to("percent").m == 50 # assert c.to("%").m == 50 # TODO: fails. + def test_issue1963(self, module_registry): + ureg = module_registry + assert ureg("‰") == ureg("permille") + assert ureg("‰") == ureg.permille + + a = ureg.Quantity("10 ‰") + b = ureg.Quantity("100 ppm") + c = ureg.Quantity("0.5") + + assert f"{a}" == "10 permille" + assert f"{a:~}" == "10 ‰" + + assert_equal(a, 0.01) + assert_equal(1e2 * b, a) + assert_equal(c, 50 * a) + + assert_equal((1 * ureg.milligram) / (1 * ureg.gram), ureg.permille) + @pytest.mark.xfail @helpers.requires_uncertainties() def test_issue_1300(self): @@ -908,7 +940,7 @@ def test_issue1674(self, module_registry): arr_of_q * q_arr, np.array([Q_(2, "m^2"), Q_(8, "m s")], dtype="object") ) - @helpers.requires_babel() + @helpers.requires_babel(["es_ES"]) def test_issue_1400(self, sess_registry): q1 = 3.1 * sess_registry.W q2 = 3.1 * sess_registry.W / sess_registry.cm @@ -917,6 +949,7 @@ def test_issue_1400(self, sess_registry): assert q2.format_babel("~", locale="es_ES") == "3,1 W/cm" assert q2.format_babel("", locale="es_ES") == "3,1 vatios por centímetro" + @helpers.requires_numpy() @helpers.requires_uncertainties() def test_issue1611(self, module_registry): from numpy.testing import assert_almost_equal @@ -1255,3 +1288,46 @@ def test_issue1949(registry_empty): def test_issue1772(given, expected): ureg = UnitRegistry(non_int_type=decimal.Decimal) assert f"{ureg(given):Lx}" == expected + + +def test_issue2017(): + ureg = UnitRegistry() + + from pint import formatting as fmt + + @fmt.register_unit_format("test") + def _test_format(unit, registry, **options): + print("format called") + proc = {u.replace("µ", "u"): e for u, e in unit.items()} + return fmt.formatter( + proc.items(), + as_ratio=True, + single_denominator=False, + product_fmt="*", + division_fmt="/", + power_fmt="{}{}", + parentheses_fmt="({})", + **options, + ) + + base_unit = ureg.microsecond + assert f"{base_unit:~test}" == "us" + assert f"{base_unit:test}" == "microsecond" + + +def test_issue2007(): + ureg = UnitRegistry() + q = ureg.Quantity(1, "") + assert f"{q:P}" == "1 dimensionless" + assert f"{q:C}" == "1 dimensionless" + assert f"{q:D}" == "1 dimensionless" + assert f"{q:H}" == "1 dimensionless" + + assert f"{q:L}" == "1\\ \\mathrm{dimensionless}" + # L returned '1\\ dimensionless' in pint 0.23 + + assert f"{q:Lx}" == "\\SI[]{1}{}" + assert f"{q:~P}" == "1" + assert f"{q:~C}" == "1" + assert f"{q:~D}" == "1" + assert f"{q:~H}" == "1" diff --git a/pint/testsuite/test_log_units.py b/pint/testsuite/test_log_units.py index c3b7b2c5a..5f1b0be49 100644 --- a/pint/testsuite/test_log_units.py +++ b/pint/testsuite/test_log_units.py @@ -65,6 +65,11 @@ def test_log_convert(self): helpers.assert_quantity_almost_equal( self.Q_(0.0, "dBm"), self.Q_(29.999999999999996, "dBu"), atol=1e-7 ) + # ## Test dB to dB units dBm - dBW + # 0 dBW = 1W = 1e3 mW = 30 dBm + helpers.assert_quantity_almost_equal( + self.Q_(0.0, "dBW"), self.Q_(29.999999999999996, "dBm"), atol=1e-7 + ) def test_mix_regular_log_units(self): # Test regular-logarithmic mixed definition, such as dB/km or dB/cm @@ -84,6 +89,8 @@ def test_mix_regular_log_units(self): log_unit_names = [ + "decibelwatt", + "dBW", "decibelmilliwatt", "dBm", "decibelmicrowatt", @@ -135,6 +142,7 @@ def test_quantity_by_multiplication(module_registry_auto_offset, unit_name, mag) @pytest.mark.parametrize( "unit1,unit2", [ + ("decibelwatt", "dBW"), ("decibelmilliwatt", "dBm"), ("decibelmicrowatt", "dBu"), ("decibel", "dB"), diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py index d7221fa40..06f7000e5 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -452,13 +452,14 @@ def test_trapz(self): ) @helpers.requires_array_function_protocol() + # NP2: Remove this when we only support np>=2.0 + # trapezoid added in numpy 2.0 + @helpers.requires_numpy_at_least("2.0") def test_trapezoid(self): - # NP2: Remove this when we only support np>=2.0 - if np.lib.NumpyVersion(np.__version__) >= "2.0.0b1": - helpers.assert_quantity_equal( - np.trapezoid([1.0, 2.0, 3.0, 4.0] * self.ureg.J, dx=1 * self.ureg.m), - 7.5 * self.ureg.J * self.ureg.m, - ) + helpers.assert_quantity_equal( + np.trapezoid([1.0, 2.0, 3.0, 4.0] * self.ureg.J, dx=1 * self.ureg.m), + 7.5 * self.ureg.J * self.ureg.m, + ) @helpers.requires_array_function_protocol() def test_dot(self): diff --git a/pint/testsuite/test_numpy_func.py b/pint/testsuite/test_numpy_func.py index 979b6ee25..9c69a238d 100644 --- a/pint/testsuite/test_numpy_func.py +++ b/pint/testsuite/test_numpy_func.py @@ -216,6 +216,14 @@ def test_trapz_no_autoconvert(self): with pytest.raises(OffsetUnitCalculusError): np.trapz(t, x=z) + def test_correlate(self): + a = self.Q_(np.array([1, 2, 3]), "m") + v = self.Q_(np.array([0, 1, 0.5]), "s") + res = np.correlate(a, v, "full") + ref = np.array([0.5, 2.0, 3.5, 3.0, 0.0]) + assert np.array_equal(res.magnitude, ref) + assert res.units == "meter * second" + def test_dot(self): with ExitStack() as stack: stack.callback( diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py index 8c6f15c49..26a5ee05d 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -2014,3 +2014,11 @@ def test_offset_autoconvert_gt_zero(self): assert q2 > 0 with pytest.raises(DimensionalityError): q1.__gt__(ureg.Quantity(0, "")) + + def test_types(self): + quantity = self.Q_(1.0, "m") + assert isinstance(quantity, self.Q_) + assert isinstance(quantity.units, self.ureg.Unit) + assert isinstance(quantity.m, float) + + assert isinstance(self.ureg.m, self.ureg.Unit) diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py index 5b5f69a0c..78d72e856 100644 --- a/pint/testsuite/test_unit.py +++ b/pint/testsuite/test_unit.py @@ -70,7 +70,7 @@ def test_latex_escaping(self, subtests): "Lx~": r"\si[]{\%}", }.items(): with subtests.test(spec): - ureg.default_format = spec + ureg.formatter.default_format = spec assert f"{x}" == result, f"Failed for {spec}, got {x} expected {result}" # no '#' here as it's a comment char when define()ing new units ureg.define(r"weirdunit = 1 = \~_^&%$_{}") @@ -83,7 +83,7 @@ def test_latex_escaping(self, subtests): # "Lx~": r"\si[]{\textbackslash \textasciitilde \_\textasciicircum \&\%\$\_\{\}}", }.items(): with subtests.test(spec): - ureg.default_format = spec + ureg.formatter.default_format = spec assert f"{x}" == result, f"Failed for {spec}, {result}" def test_unit_default_formatting(self, subtests): @@ -104,13 +104,13 @@ def test_unit_default_formatting(self, subtests): ("C~", "kg*m**2/s"), ): with subtests.test(spec): - ureg.default_format = spec + ureg.formatter.default_format = spec assert f"{x}" == result, f"Failed for {spec}, {result}" @pytest.mark.xfail(reason="Still not clear how default formatting will work.") def test_unit_formatting_defaults_warning(self): ureg = UnitRegistry() - ureg.default_format = "~P" + ureg.formatter.default_format = "~P" x = ureg.Unit("m / s ** 2") with pytest.warns(DeprecationWarning): @@ -136,7 +136,7 @@ def test_unit_formatting_snake_case(self, subtests): ("C~", "oil_bbl"), ): with subtests.test(spec): - ureg.default_format = spec + ureg.formatter.default_format = spec assert f"{x}" == result, f"Failed for {spec}, {result}" def test_unit_formatting_custom(self, monkeypatch): @@ -177,7 +177,7 @@ def pretty(cls, data): ) x._repr_pretty_(Pretty, False) assert "".join(alltext) == "kilogram·meter²/second" - ureg.default_format = "~" + ureg.formatter.default_format = "~" assert x._repr_html_() == "kg m2/s" assert ( x._repr_latex_() == r"$\frac{\mathrm{kg} \cdot \mathrm{m}^{2}}{\mathrm{s}}$" @@ -322,11 +322,11 @@ def test_default_format(self): q = ureg.meter s1 = f"{q}" s2 = f"{q:~}" - ureg.default_format = "~" + ureg.formatter.default_format = "~" s3 = f"{q}" assert s2 == s3 assert s1 != s3 - assert ureg.default_format == "~" + assert ureg.formatter.default_format == "~" def test_iterate(self): ureg = UnitRegistry() @@ -989,6 +989,8 @@ class TestConvertWithOffset(QuantityTestCase): (({"degC": 2}, {"kelvin": 2}), "error"), (({"degC": 1, "degF": 1}, {"kelvin": 2}), "error"), (({"degC": 1, "kelvin": 1}, {"kelvin": 2}), "error"), + (({"delta_degC": 1}, {"degF": 1}), "error"), + (({"delta_degC": 1}, {"degC": 1}), "error"), ] @pytest.mark.parametrize(("input_tuple", "expected"), convert_with_offset) @@ -1041,3 +1043,8 @@ def test_alias(self): # Define against unknown name with pytest.raises(KeyError): ureg.define("@alias notexist = something") + + def test_prefix_offset_units(self): + ureg = UnitRegistry() + with pytest.raises(errors.OffsetUnitCalculusError): + ureg.parse_units("kilodegree_Celsius") diff --git a/pyproject.toml b/pyproject.toml index a376bd6a4..9f29f8f92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,11 +22,12 @@ classifiers = [ "Programming Language :: Python", "Topic :: Scientific/Engineering", "Topic :: Software Development :: Libraries", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] -requires-python = ">=3.10" +requires-python = ">=3.9" dynamic = ["version", "dependencies"] [tool.setuptools.package-data] diff --git a/requirements.txt b/requirements.txt index 0bc99005a..b63f8da99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -appdirs>=1.4.4 -typing_extensions +platformdirs>=2.1.0 +typing_extensions>=4.0.0 flexcache>=0.3 -flexparser>=0.3 +flexparser>=0.4