diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d445a2970..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"] - numpy: [null, "numpy>=1.23,<2.0.0"] + 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/.gitignore b/.gitignore index ae702bac3..69fd3338d 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,8 @@ MANIFEST .mypy_cache pip-wheel-metadata pint/testsuite/dask-worker-space +venv +.envrc # WebDAV file system cache files .DAV/ diff --git a/CHANGES b/CHANGES index 362dcb3b8..4732334e0 100644 --- a/CHANGES +++ b/CHANGES @@ -1,15 +1,83 @@ Pint Changelog ============== -0.24 (unreleased) +0.25.0 (unreleased) +------------------- + +- Add docs to the functions in ``pint.testing`` (PR #2070) +- Fix round function returning float instead of int (#2081) +- Fix return type of `PlainQuantity.to` (#2088) +- Update constants to CODATA 2022 recommended values. (#2049) +- Fixed issue with `.to_compact` and Magnitudes with uncertainties / Quantities with units (PR #2069, issue #2044) +- `Quantity` now converts `datetime.timedelta` objects to seconds or specified units when + initializing a `Quantity` with a `datetime.timedelta` value. + (PR #1978) + +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) -- `Quantity` now converts `datetime.timedelta` objects to seconds or specified units when - initializing a `Quantity` with a `datetime.timedelta` value. - (PR #1978) +- 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) @@ -40,6 +108,7 @@ Pint Changelog - Add numpy.linalg.norm implementation. (PR #1251) + 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/measurement.rst b/docs/advanced/measurement.rst index a49c8212b..0958d8db8 100644 --- a/docs/advanced/measurement.rst +++ b/docs/advanced/measurement.rst @@ -69,4 +69,4 @@ the `Propagation of uncertainty`_ rules. .. _`Propagation of uncertainty`: http://en.wikipedia.org/wiki/Propagation_of_uncertainty -.. _`Uncertainties package`: https://uncertainties-python-package.readthedocs.io/en/latest/ +.. _`Uncertainties package`: https://uncertainties.readthedocs.io/en/latest/ 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/advanced/pitheorem.rst b/docs/advanced/pitheorem.rst index cd3716528..06409d8b5 100644 --- a/docs/advanced/pitheorem.rst +++ b/docs/advanced/pitheorem.rst @@ -33,8 +33,10 @@ Which can be pretty printed using the `Pint` formatter: >>> from pint import formatter >>> result = pi_theorem({'V': '[length]/[time]', 'T': '[time]', 'L': '[length]'}) - >>> print(formatter(result[0].items())) - T * V / L + >>> numerator = [item for item in result[0].items() if item[1]>0] + >>> denominator = [item for item in result[0].items() if item[1]<0] + >>> print(formatter(numerator, denominator)) + V * T / L You can also apply the Buckingham π theorem associated to a Registry. In this case, you can use derived dimensions such as speed: diff --git a/docs/api/facets.rst b/docs/api/facets.rst index f4b6a54e8..d835f5cea 100644 --- a/docs/api/facets.rst +++ b/docs/api/facets.rst @@ -16,7 +16,7 @@ The default UnitRegistry inherits from all of them. :members: :exclude-members: Quantity, Unit, Measurement, Group, Context, System -.. automodule:: pint.facets.formatting +.. automodule:: pint.delegates.formatter :members: :exclude-members: Quantity, Unit, Measurement, Group, Context, System diff --git a/docs/conf.py b/docs/conf.py index d856e1075..b27bff94a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,6 +45,7 @@ "sphinx_design", ] + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] 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..95a73bd45 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/overview.rst b/docs/getting/overview.rst index 61dfc14f4..f97ad2999 100644 --- a/docs/getting/overview.rst +++ b/docs/getting/overview.rst @@ -105,6 +105,7 @@ License ------- .. literalinclude:: ../../LICENSE + :language: none .. _`comprehensive list of physical units, prefixes and constants`: https://github.com/hgrecco/pint/blob/master/pint/default_en.txt .. _`uncertainties package`: https://pythonhosted.org/uncertainties/ diff --git a/docs/getting/tutorial.rst b/docs/getting/tutorial.rst index bb3505b51..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²' @@ -428,7 +428,7 @@ If Babel_ is installed you can translate unit names to any language .. doctest:: >>> ureg.formatter.format_quantity(accel, locale='fr_FR') - '1,3 mètres/secondes²' + '1,3 mètres par seconde²' You can also specify the format locale at the registry level either at creation: @@ -446,14 +446,14 @@ 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/secondes²' + '1,3 mètres par seconde²' >>> "%s" % accel - '1,3 mètres/secondes²' + '1,3 mètres par seconde²' >>> "{}".format(accel) - '1,3 mètres/secondes²' + '1,3 mètres par seconde²' If you want to customize string formatting, take a look at :ref:`formatting`. diff --git a/docs/user/angular_frequency.rst b/docs/user/angular_frequency.rst index 58e126a9c..61bdf1614 100644 --- a/docs/user/angular_frequency.rst +++ b/docs/user/angular_frequency.rst @@ -2,7 +2,7 @@ Angles and Angular Frequency -================= +============================= Angles ------ diff --git a/docs/user/defining-quantities.rst b/docs/user/defining-quantities.rst index e40b08cf9..a7405151a 100644 --- a/docs/user/defining-quantities.rst +++ b/docs/user/defining-quantities.rst @@ -134,7 +134,7 @@ For example, the units of .. doctest:: >>> Q_('3 l / 100 km') - + may be unexpected at first but, are a consequence of applying this rule. Use brackets to get the expected result: diff --git a/docs/user/formatting.rst b/docs/user/formatting.rst index f17939a86..fbf2fae42 100644 --- a/docs/user/formatting.rst +++ b/docs/user/formatting.rst @@ -95,10 +95,11 @@ formats: ... def format_unit_simple(unit, registry, **options): ... return " * ".join(f"{u} ** {p}" for u, p in unit.items()) >>> f"{q:Z}" - '2.3e-06 meter ** 3 * second ** -2 * kilogram ** -1' + '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`, @@ -111,10 +112,11 @@ following methods: `format_magnitude`, `format_unit`, `format_quantity`, `format ... ... default_format = "" ... - ... def format_unit(self, unit, uspec: str = "", **babel_kwds) -> str: + ... def format_unit(self, unit, uspec, sort_func, **babel_kwds) -> str: ... return "ups!" ... >>> ureg.formatter = MyFormatter() + >>> ureg.formatter._registry = ureg >>> str(q) '2.3e-06 ups!' 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 783fa236c..68de0ca67 100644 --- a/pint/compat.py +++ b/pint/compat.py @@ -24,6 +24,11 @@ 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: @@ -73,7 +78,8 @@ class BehaviorChangeWarning(UserWarning): try: from uncertainties import UFloat, ufloat - from uncertainties import unumpy as unp + + unp = None HAS_UNCERTAINTIES = True except ImportError: @@ -91,6 +97,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) @@ -239,6 +247,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/constants_en.txt b/pint/constants_en.txt index 9babc8fa2..2f6fcfb50 100644 --- a/pint/constants_en.txt +++ b/pint/constants_en.txt @@ -46,22 +46,21 @@ wien_wavelength_displacement_law_constant = ℎ * c / (k * wien_x) wien_frequency_displacement_law_constant = wien_u * k / ℎ #### MEASURED CONSTANTS #### -# Recommended CODATA-2018 values +# Recommended CODATA-2022 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) +rydberg_constant = 1.0973731568157e7 * m^-1 = R_∞ = R_inf # (12) +electron_g_factor = -2.00231930436092 = g_e # (36) +atomic_mass_constant = 1.66053906892e-27 kg = m_u # (52) +electron_mass = 9.1093837139e-31 kg = m_e = atomic_unit_of_mass = a_u_mass # (28) +proton_mass = 1.67262192595e-27 kg = m_p # (52) +neutron_mass = 1.67492750056e-27 kg = m_n # (85) +x_unit_Cu = 1.00207697e-13 m = Xu_Cu # (28) +x_unit_Mo = 1.00209952e-13 m = Xu_Mo # (53) +angstrom_star = 1.00001495e-10 = Å_star # (90) #### DERIVED CONSTANTS #### diff --git a/pint/default_en.txt b/pint/default_en.txt index 5fc7f8265..bbac09bed 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 @@ -161,9 +162,6 @@ 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 @@ -221,7 +219,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 @@ -494,12 +492,17 @@ buckingham = debye * angstrom 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 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/formatter/latex.py b/pint/delegates/formatter/latex.py index 476997b84..468a65fa4 100644 --- a/pint/delegates/formatter/latex.py +++ b/pint/delegates/formatter/latex.py @@ -124,8 +124,8 @@ def siunitx_format_unit( ) -> str: """Returns LaTeX code for the unit that can be put into an siunitx command.""" - def _tothe(power: int | float) -> str: - if isinstance(power, int) or (isinstance(power, float) and power.is_integer()): + def _tothe(power) -> str: + if power == int(power): if power == 1: return "" elif power == 2: 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 4985ba51b..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 @@ -192,7 +192,7 @@ 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 or (not slct_unit.is_multiplicative): + if slct_unit.is_logarithmic: # Extract reference unit slct_ref = slct_unit.reference @@ -204,6 +204,11 @@ def _add_ref_of_log_or_offset_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 @@ -249,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) @@ -262,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 @@ -269,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 29724837f..b79700f9f 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) @@ -284,6 +288,17 @@ def implement_func(func_type, func_str, input_units=None, output_unit=None): @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 @@ -413,6 +428,7 @@ def implementation(*args, **kwargs): "take", "trace", "transpose", + "roll", "ceil", "floor", "hypot", @@ -740,8 +756,11 @@ def _base_unit_if_needed(a): 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: @@ -749,17 +768,26 @@ def _trapz(y, x=None, dx=1.0, **kwargs): x = _base_unit_if_needed(x) units *= x.units x = x._magnitude - ret = np.trapz(y._magnitude, x, **kwargs) + ret = trapezoid(y._magnitude, x, **kwargs) else: if hasattr(dx, "units"): dx = _base_unit_if_needed(dx) units *= dx.units dx = dx._magnitude - ret = np.trapz(y._magnitude, dx=dx, **kwargs) + 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: @@ -850,6 +878,7 @@ def implementation(*args, **kwargs): ("median", "a", True), ("nanmedian", "a", True), ("transpose", "a", True), + ("roll", "a", True), ("copy", "a", True), ("average", "a", True), ("nanmean", "a", True), diff --git a/pint/facets/plain/qto.py b/pint/facets/plain/qto.py index 9de541584..9d8b7f611 100644 --- a/pint/facets/plain/qto.py +++ b/pint/facets/plain/qto.py @@ -110,12 +110,12 @@ def to_compact( ) return quantity - if ( - quantity.unitless - or quantity.magnitude == 0 - or math.isnan(quantity.magnitude) - or math.isinf(quantity.magnitude) - ): + qm = ( + quantity.magnitude + if not hasattr(quantity.magnitude, "nominal_value") + else quantity.magnitude.nominal_value + ) + if quantity.unitless or qm == 0 or math.isnan(qm) or math.isinf(qm): return quantity SI_prefixes: dict[int, str] = {} @@ -184,7 +184,7 @@ def to_preferred( >>> (1*ureg.acre).to_preferred([ureg.meters]) >>> (1*(ureg.force_pound*ureg.m)).to_preferred([ureg.W]) - + """ units = _get_preferred(quantity, preferred_units) @@ -204,7 +204,7 @@ def ito_preferred( >>> (1*ureg.acre).to_preferred([ureg.meters]) >>> (1*(ureg.force_pound*ureg.m)).to_preferred([ureg.W]) - + """ units = _get_preferred(quantity, preferred_units) diff --git a/pint/facets/plain/quantity.py b/pint/facets/plain/quantity.py index eabc59662..b3ed4074d 100644 --- a/pint/facets/plain/quantity.py +++ b/pint/facets/plain/quantity.py @@ -1,9 +1,9 @@ """ - pint.facets.plain.quantity - ~~~~~~~~~~~~~~~~~~~~~~~~~ +pint.facets.plain.quantity +~~~~~~~~~~~~~~~~~~~~~~~~~ - :copyright: 2022 by Pint Authors, see AUTHORS for more details. - :license: BSD, see LICENSE for more details. +:copyright: 2022 by Pint Authors, see AUTHORS for more details. +:license: BSD, see LICENSE for more details. """ from __future__ import annotations @@ -26,6 +26,7 @@ from ..._typing import Magnitude, QuantityOrUnitLike, Scalar, UnitLike from ...compat import ( HAS_NUMPY, + Self, _to_magnitude, convert_timedelta, deprecated, @@ -143,7 +144,7 @@ class PlainQuantity(Generic[MagnitudeT], PrettyIPython, SharedRegistryObject): def ndim(self) -> int: if isinstance(self.magnitude, numbers.Number): return 0 - if str(self.magnitude) == "": + if str(type(self.magnitude)) == "NAType": return 0 return self.magnitude.ndim @@ -526,7 +527,7 @@ def ito( def to( self, other: QuantityOrUnitLike | None = None, *contexts, **ctx_kwargs - ) -> PlainQuantity: + ) -> Self: """Return PlainQuantity rescaled to different units. Parameters @@ -1299,8 +1300,8 @@ def __rpow__(self, other) -> PlainQuantity[MagnitudeT]: 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 __round__(self, ndigits: int | None = None) -> PlainQuantity[int]: + return self.__class__(round(self._magnitude, ndigits), self._units) def __pos__(self) -> PlainQuantity[MagnitudeT]: return self.__class__(operator.pos(self._magnitude), self._units) diff --git a/pint/facets/plain/registry.py b/pint/facets/plain/registry.py index 277a6f7a2..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] @@ -736,7 +748,12 @@ def _get_dimensionality_recurse( for key in ref: exp2 = exp * ref[key] if _is_dim(key): - reg = self._dimensions[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: diff --git a/pint/pint_convert.py b/pint/pint_convert.py old mode 100755 new mode 100644 index bf9097237..dd830718c --- 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 @@ -86,26 +91,52 @@ def _set(key: str, value): # 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) + # x_Cu: Copper x unit + # x_Mo: Molybdenum x unit + # A_s: Angstrom star + R_i = (ureg._units["R_inf"].converter.scale, 0.0000000000012e7) + g_e = (ureg._units["g_e"].converter.scale, 0.00000000000036) + m_u = (ureg._units["m_u"].converter.scale, 0.00000000052e-27) + m_e = (ureg._units["m_e"].converter.scale, 0.0000000028e-31) + m_p = (ureg._units["m_p"].converter.scale, 0.00000000052e-27) + m_n = (ureg._units["m_n"].converter.scale, 0.00000000085e-27) + x_Cu = (ureg._units["x_unit_Cu"].converter.scale, 0.00000028e-13) + x_Mo = (ureg._units["x_unit_Mo"].converter.scale, 0.00000053e-13) + A_s = (ureg._units["angstrom_star"].converter.scale, 0.00000090e-10) if args.corr: + # fmt: off # Correlation matrix between measured constants (to be completed below) - # R_i g_e m_u m_e m_p m_n + # R_i g_e m_u m_e m_p m_n x_Cu x_Mo A_s 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 - (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 - ) + [ 1.00000, -0.00122, 0.00438, 0.00225, 0.00455, 0.00277, 0.00000, 0.00000, 0.00000], # R_i + [-0.00122, 1.00000, 0.97398, 0.97555, 0.97404, 0.59702, 0.00000, 0.00000, 0.00000], # g_e + [ 0.00438, 0.97398, 1.00000, 0.99839, 0.99965, 0.61279, 0.00000, 0.00000, 0.00000], # m_u + [ 0.00225, 0.97555, 0.99839, 1.00000, 0.99845, 0.61199, 0.00000, 0.00000, 0.00000], # m_e + [ 0.00455, 0.97404, 0.99965, 0.99845, 1.00000, 0.61281, 0.00000, 0.00000, 0.00000], # m_p + [ 0.00277, 0.59702, 0.61279, 0.61199, 0.61281, 1.00000,-0.00098,-0.00108,-0.00063], # m_n + [ 0.00000, 0.00000, 0.00000, 0.00000, 0.00000,-0.00098, 1.00000, 0.00067, 0.00039], # x_Cu + [ 0.00000, 0.00000, 0.00000, 0.00000, 0.00000,-0.00108, 0.00067, 1.00000, 0.00100], # x_Mo + [ 0.00000, 0.00000, 0.00000, 0.00000, 0.00000,-0.00063, 0.00039, 0.00100, 1.00000], # A_s + ] + # fmt: on + try: + ( + R_i, + g_e, + m_u, + m_e, + m_p, + m_n, + x_Cu, + x_Mo, + A_s, + ) = uncertainties.correlated_values_norm( + [R_i, g_e, m_u, m_e, m_p, m_n, x_Cu, x_Mo, A_s], 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) @@ -113,6 +144,9 @@ def _set(key: str, value): m_e = uncertainties.ufloat(*m_e) m_p = uncertainties.ufloat(*m_p) m_n = uncertainties.ufloat(*m_n) + x_Cu = uncertainties.ufloat(*x_Cu) + x_Mo = uncertainties.ufloat(*x_Mo) + A_s = uncertainties.ufloat(*A_s) _set("R_inf", R_i) _set("g_e", g_e) @@ -120,6 +154,9 @@ def _set(key: str, value): _set("m_e", m_e) _set("m_p", m_p) _set("m_n", m_n) + _set("x_unit_Cu", x_Cu) + _set("x_unit_Mo", x_Mo) + _set("angstrom_star", A_s) # Measured constants with zero correlation _set( @@ -129,37 +166,12 @@ def _set(key: str, value): ), ) - _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: @@ -171,7 +183,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/registry.py b/pint/registry.py index 210ea9112..ceb9b62d1 100644 --- a/pint/registry.py +++ b/pint/registry.py @@ -69,31 +69,38 @@ class UnitRegistry(GenericUnitRegistry[Quantity, Unit]): ---------- filename : path of the units definition file to load or line-iterable object. - Empty to load the default definition file. + 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. + context. If False no conversion happens. (Default: False) on_redefinition : str action to take in case a unit is redefined. - 'warn', 'raise', 'ignore' + '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 unit string or None for no preprocessor. + (Default=None) fmt_locale : - locale identifier string, used in `format_babel`. Default to None + 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 diff --git a/pint/testing.py b/pint/testing.py index 21a1f55dd..c5508d8d2 100644 --- a/pint/testing.py +++ b/pint/testing.py @@ -1,3 +1,13 @@ +""" + pint.testing + ~~~~~~~~~~~~ + + Functions for testing whether pint quantities are equal. + + :copyright: 2016 by Pint Authors, see AUTHORS for more details.. + :license: BSD, see LICENSE for more details. +""" + from __future__ import annotations import math @@ -35,6 +45,25 @@ def _get_comparable_magnitudes(first, second, msg): def assert_equal(first, second, msg: str | None = None) -> None: + """ + Assert that two quantities are equal + + Parameters + ---------- + first + First quantity to compare + + second + Second quantity to compare + + msg + If supplied, message to show if the two quantities aren't equal. + + Raises + ------ + AssertionError + The two quantities are not equal. + """ if msg is None: msg = f"Comparing {first!r} and {second!r}. " @@ -60,6 +89,33 @@ def assert_equal(first, second, msg: str | None = None) -> None: def assert_allclose( first, second, rtol: float = 1e-07, atol: float = 0, msg: str | None = None ) -> None: + """ + Assert that two quantities are all close + + Unlike numpy, this uses a symmetric check of closeness. + + Parameters + ---------- + first + First quantity to compare + + second + Second quantity to compare + + rtol + Relative tolerance to use when checking for closeness. + + atol + Absolute tolerance to use when checking for closeness. + + msg + If supplied, message to show if the two quantities aren't equal. + + Raises + ------ + AssertionError + The two quantities are not close to within the supplied tolerance. + """ if msg is None: try: msg = f"Comparing {first!r} and {second!r}. " diff --git a/pint/testsuite/benchmarks/test_10_registry.py b/pint/testsuite/benchmarks/test_10_registry.py index 09264fa44..3a1d42da5 100644 --- a/pint/testsuite/benchmarks/test_10_registry.py +++ b/pint/testsuite/benchmarks/test_10_registry.py @@ -164,6 +164,9 @@ def test_load_definitions_stage_1(benchmark, cache_folder, use_cache_folder): benchmark(pint.UnitRegistry, None, cache_folder=use_cache_folder) +@pytest.mark.skip( + "Test failing ValueError: Group USCSLengthInternational already present in registry" +) @pytest.mark.parametrize("use_cache_folder", (None, True)) def test_load_definitions_stage_2(benchmark, cache_folder, use_cache_folder): """empty registry creation + parsing default files + definition object loading""" 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_errors.py b/pint/testsuite/test_errors.py index 370ccfc9d..e0c4ec3f4 100644 --- a/pint/testsuite/test_errors.py +++ b/pint/testsuite/test_errors.py @@ -144,3 +144,13 @@ def test_pickle_definition_syntax_error(self, subtests): with pytest.raises(PintError): raise ex + + def test_dimensionality_error_message(self): + ureg = UnitRegistry(system="SI") + with pytest.raises(ValueError) as error: + ureg.get_dimensionality("[bilbo]") + + assert ( + str(error.value) + == "[bilbo] is not defined as dimension in the pint UnitRegistry" + ) 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 dc63ececd..8501661d0 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 @@ -395,7 +400,7 @@ def test_angstrom_creation(self, module_registry): module_registry.Quantity(2, "Å") def test_alternative_angstrom_definition(self, module_registry): - module_registry.Quantity(2, "\u212B") + module_registry.Quantity(2, "\u212b") def test_micro_creation_U03bc(self, module_registry): module_registry.Quantity(2, "μm") @@ -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): @@ -888,7 +920,27 @@ def test_issue_1300(self): m = module_registry.Measurement(1, 0.1, "meter") assert m.default_format == "~P" - @helpers.requires_babel() + @helpers.requires_numpy() + def test_issue1674(self, module_registry): + Q_ = module_registry.Quantity + arr_of_q = np.array([Q_(2, "m"), Q_(4, "m")], dtype="object") + q_arr = Q_(np.array([1, 2]), "m") + + helpers.assert_quantity_equal( + arr_of_q * q_arr, np.array([Q_(2, "m^2"), Q_(8, "m^2")], dtype="object") + ) + helpers.assert_quantity_equal( + arr_of_q / q_arr, np.array([Q_(2, ""), Q_(2, "")], dtype="object") + ) + + arr_of_q = np.array([Q_(2, "m"), Q_(4, "s")], dtype="object") + q_arr = Q_(np.array([1, 2]), "m") + + helpers.assert_quantity_equal( + arr_of_q * q_arr, np.array([Q_(2, "m^2"), Q_(8, "m s")], dtype="object") + ) + + @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 @@ -897,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 @@ -1147,7 +1200,7 @@ def test_issue1725(registry_empty): assert registry_empty.get_compatible_units("dollar") == set() -def test_issues_1505(): +def test_issue1505(): ur = UnitRegistry(non_int_type=decimal.Decimal) assert isinstance(ur.Quantity("1m/s").magnitude, decimal.Decimal) @@ -1159,6 +1212,13 @@ def test_issues_1505(): ) # unexpected fail (magnitude should be a decimal) +def test_issue_1845(): + ur = UnitRegistry(auto_reduce_dimensions=True, non_int_type=decimal.Decimal) + # before issue 1845 these inputs would have resulted in a TypeError + assert ur("km / h * m").units == ur.Quantity("meter ** 2 / hour") + assert ur("kW / min * W").units == ur.Quantity("watts ** 2 / minute") + + @pytest.mark.parametrize( "units,spec,expected", [ @@ -1201,3 +1261,91 @@ def test_issues_1841_xfail(): # this prints "2*pi hour * radian", not "2*pi radian * hour" unless sort_dims is True # print(q) + + +def test_issue1949(registry_empty): + ureg = UnitRegistry() + ureg.define( + "in_Hg_gauge = 3386389 * gram / metre / second ** 2; offset:101325000 = inHg_g = in_Hg_g = inHg_gauge" + ) + q = ureg.Quantity("1 atm").to("inHg_gauge") + assert q.units == ureg.in_Hg_gauge + assert_equal(q.magnitude, 0.0) + + +@pytest.mark.parametrize( + "given,expected", + [ + ( + "8.989e9 newton * meter^2 / coulomb^2", + r"\SI[]{8.989E+9}{\meter\squared\newton\per\coulomb\squared}", + ), + ("5 * meter / second", r"\SI[]{5}{\meter\per\second}"), + ("2.2 * meter^4", r"\SI[]{2.2}{\meter\tothe{4}}"), + ("2.2 * meter^-4", r"\SI[]{2.2}{\per\meter\tothe{4}}"), + ], +) +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" + + +@helpers.requires_uncertainties() +@helpers.requires_numpy() +def test_issue2044(): + from numpy.testing import assert_almost_equal + from uncertainties import ufloat + + ureg = UnitRegistry() + # First make sure this doesn't fail completely (A Measurement) + q = ureg.Quantity(10_000, "m").plus_minus(0.01).to_compact() + assert_almost_equal(q.m.n, 10.0) + assert q.u == "kilometer" + + # Similarly, for a Ufloat with units + q = (ufloat(10_000, 0.01) * ureg.m).to_compact() + assert_almost_equal(q.m.n, 10.0) + assert q.u == "kilometer" 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 69c8128c0..3075be7ac 100644 --- a/pint/testsuite/test_numpy.py +++ b/pint/testsuite/test_numpy.py @@ -288,6 +288,11 @@ def test_broadcast_arrays(self): result = np.broadcast_arrays(x, y, subok=True) helpers.assert_quantity_equal(result, expected) + def test_roll(self): + helpers.assert_quantity_equal( + np.roll(self.q, 1), [[4, 1], [2, 3]] * self.ureg.m + ) + class TestNumpyMathematicalFunctions(TestNumpyMethods): # https://www.numpy.org/devdocs/reference/routines.math.html @@ -433,6 +438,7 @@ def test_cross(self): np.cross(a, b), [[-15, -2, 39]] * self.ureg.kPa * self.ureg.m**2 ) + # NP2: Remove this when we only support np>=2.0 @helpers.requires_array_function_protocol() def test_trapz(self): helpers.assert_quantity_equal( @@ -440,6 +446,16 @@ def test_trapz(self): 7.5 * self.ureg.J * self.ureg.m, ) + @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): + 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): helpers.assert_quantity_equal( @@ -753,9 +769,12 @@ def test_minimum(self): np.minimum(self.q, self.Q_([0, 5], "m")), self.Q_([[0, 2], [0, 4]], "m") ) + # NP2: Can remove Q_(arr).ptp test when we only support numpy>=2 def test_ptp(self): - assert self.q.ptp() == 3 * self.ureg.m + if not np.lib.NumpyVersion(np.__version__) >= "2.0.0b1": + assert self.q.ptp() == 3 * self.ureg.m + # NP2: Keep this test for numpy>=2, it's only arr.ptp() that is deprecated @helpers.requires_array_function_protocol() def test_ptp_numpy_func(self): helpers.assert_quantity_equal(np.ptp(self.q, axis=0), [2, 2] * self.ureg.m) 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 3f9af463b..afe2fd5d6 100644 --- a/pint/testsuite/test_quantity.py +++ b/pint/testsuite/test_quantity.py @@ -60,6 +60,11 @@ def test_quantity_creation(self, caplog): assert 4.2 * self.ureg.meter == self.Q_(4.2, 2 * self.ureg.meter) assert len(caplog.records) == 1 + def test_round(self): + x = self.Q_(1.1, "kg") + assert isinstance(round(x).magnitude, int) + assert isinstance(round(x, 0).magnitude, float) + def test_quantity_with_quantity(self): x = self.Q_(4.2, "m") assert self.Q_(x, "m").magnitude == 4.2 @@ -2074,3 +2079,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/pint/util.py b/pint/util.py index 0c40c5187..c7a7ec10c 100644 --- a/pint/util.py +++ b/pint/util.py @@ -495,7 +495,7 @@ def add(self: Self, key: str, value: Number) -> Self: UnitsContainer A copy of this container. """ - newval = self._d[key] + value + newval = self._d[key] + self._normalize_nonfloat_value(value) new = self.copy() if newval: new._d[key] = newval @@ -656,7 +656,7 @@ def __truediv__(self, other: Any): new = self.copy() for key, value in other.items(): - new._d[key] -= value + new._d[key] -= self._normalize_nonfloat_value(value) if new._d[key] == 0: del new._d[key] @@ -670,6 +670,11 @@ def __rtruediv__(self, other: Any): 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 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..8a931d122 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -appdirs>=1.4.4 -typing_extensions +platformdirs>=2.1.0 +typing_extensions>=4.5.0 flexcache>=0.3 -flexparser>=0.3 +flexparser>=0.4