From 6f5ff5a34f8f5fa8db1c7d87d8e8e80ac53a56ef Mon Sep 17 00:00:00 2001 From: Hernan Date: Sun, 1 May 2022 11:23:30 -0300 Subject: Typing and documentation changes --- pint/__init__.py | 4 +- pint/_typing.py | 5 +- pint/facets/context/__init__.py | 3 +- pint/facets/context/definitions.py | 2 +- pint/facets/context/objects.py | 2 +- pint/facets/context/registry.py | 2 +- pint/facets/formatting/__init__.py | 225 +------------------------- pint/facets/formatting/objects.py | 227 +++++++++++++++++++++++++++ pint/facets/formatting/registry.py | 17 ++ pint/facets/measurement/__init__.py | 219 ++------------------------ pint/facets/measurement/objects.py | 192 ++++++++++++++++++++++ pint/facets/measurement/registry.py | 42 +++++ pint/facets/nonmultiplicative/__init__.py | 8 +- pint/facets/nonmultiplicative/definitions.py | 8 + pint/facets/nonmultiplicative/objects.py | 8 + pint/facets/nonmultiplicative/registry.py | 8 + pint/facets/numpy/numpy_func.py | 6 +- pint/facets/numpy/quantity.py | 8 + pint/facets/numpy/unit.py | 8 + pint/facets/plain/definitions.py | 2 - pint/facets/plain/registry.py | 8 + pint/facets/plain/unit.py | 2 - pint/pint-convert | 2 + pint/util.py | 11 +- 24 files changed, 566 insertions(+), 453 deletions(-) create mode 100644 pint/facets/formatting/objects.py create mode 100644 pint/facets/formatting/registry.py create mode 100644 pint/facets/measurement/objects.py create mode 100644 pint/facets/measurement/registry.py diff --git a/pint/__init__.py b/pint/__init__.py index a39c281..a450323 100644 --- a/pint/__init__.py +++ b/pint/__init__.py @@ -11,6 +11,8 @@ :license: BSD, see LICENSE for more details. """ +from __future__ import annotations + from importlib.metadata import version from .errors import ( # noqa: F401 @@ -63,7 +65,7 @@ def _unpickle(cls, *args): object of type cls """ - from pint.facets.plain.unit import UnitsContainer + from pint.facets.plain import UnitsContainer for arg in args: # Prefixed units are defined within the registry diff --git a/pint/_typing.py b/pint/_typing.py index b8c5675..cfb803b 100644 --- a/pint/_typing.py +++ b/pint/_typing.py @@ -1,8 +1,9 @@ +from __future__ import annotations + from typing import TYPE_CHECKING, Any, Callable, Tuple, TypeVar, Union if TYPE_CHECKING: - from .facets.plain import Quantity, Unit - from .facets.plain.unit import UnitsContainer + from .facets.plain import Quantity, Unit, UnitsContainer UnitLike = Union[str, "UnitsContainer", "Unit"] diff --git a/pint/facets/context/__init__.py b/pint/facets/context/__init__.py index 7058e19..61685a2 100644 --- a/pint/facets/context/__init__.py +++ b/pint/facets/context/__init__.py @@ -2,7 +2,8 @@ pint.facets.context ~~~~~~~~~~~~~~~~~~~ - Adds pint the capability to contexts. + Adds pint the capability to contexts: predefined conversions + between incompatible dimensions. :copyright: 2022 by Pint Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. diff --git a/pint/facets/context/definitions.py b/pint/facets/context/definitions.py index f1ac7f7..6e07ba5 100644 --- a/pint/facets/context/definitions.py +++ b/pint/facets/context/definitions.py @@ -1,5 +1,5 @@ """ - pint.facets.systems.definitions + pint.facets.context.definitions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: 2022 by Pint Authors, see AUTHORS for more details. diff --git a/pint/facets/context/objects.py b/pint/facets/context/objects.py index cc408d3..0aca430 100644 --- a/pint/facets/context/objects.py +++ b/pint/facets/context/objects.py @@ -1,5 +1,5 @@ """ - pint.facets.systems.objects + pint.facets.context.objects ~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: 2022 by Pint Authors, see AUTHORS for more details. diff --git a/pint/facets/context/registry.py b/pint/facets/context/registry.py index a663326..27f19bb 100644 --- a/pint/facets/context/registry.py +++ b/pint/facets/context/registry.py @@ -1,5 +1,5 @@ """ - pint.facets.systems.registry + pint.facets.context.registry ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :copyright: 2022 by Pint Authors, see AUTHORS for more details. diff --git a/pint/facets/formatting/__init__.py b/pint/facets/formatting/__init__.py index 7d423a1..f9c8c82 100644 --- a/pint/facets/formatting/__init__.py +++ b/pint/facets/formatting/__init__.py @@ -10,226 +10,7 @@ from __future__ import annotations -import re -from typing import Any +from .objects import FormattingQuantity, FormattingUnit +from .registry import FormattingRegistry -from ...compat import babel_parse, ndarray, np -from ...formatting import ( - _pretty_fmt_exponent, - extract_custom_flags, - format_unit, - ndarray_to_latex, - remove_custom_flags, - siunitx_format_unit, - split_format, -) -from ...util import iterable -from ..plain import UnitsContainer - - -class FormattingQuantity: - - _exp_pattern = re.compile(r"([0-9]\.?[0-9]*)e(-?)\+?0*([0-9]+)") - - def __format__(self, spec: str) -> str: - if self._REGISTRY.fmt_locale is not None: - return self.format_babel(spec) - - mspec, uspec = split_format( - spec, self.default_format, self._REGISTRY.separate_format_defaults - ) - - # If Compact is selected, do it at the beginning - if "#" in spec: - # TODO: don't replace '#' - mspec = mspec.replace("#", "") - uspec = uspec.replace("#", "") - obj = self.to_compact() - else: - obj = self - - if "L" in uspec: - allf = plain_allf = r"{}\ {}" - elif "H" in uspec: - allf = plain_allf = "{} {}" - if iterable(obj.magnitude): - # Use HTML table instead of plain text template for array-likes - allf = ( - "" - "" - "" - "" - "
Magnitude{}
Units{}
" - ) - else: - allf = plain_allf = "{} {}" - - if "Lx" in uspec: - # the LaTeX siunitx code - # TODO: add support for extracting options - opts = "" - ustr = siunitx_format_unit(obj.units._units, obj._REGISTRY) - allf = r"\SI[%s]{{{}}}{{{}}}" % opts - else: - # Hand off to unit formatting - # TODO: only use `uspec` after completing the deprecation cycle - ustr = format(obj.units, mspec + uspec) - - # mspec = remove_custom_flags(spec) - if "H" in uspec: - # HTML formatting - if hasattr(obj.magnitude, "_repr_html_"): - # If magnitude has an HTML repr, nest it within Pint's - mstr = obj.magnitude._repr_html_() - else: - if isinstance(self.magnitude, ndarray): - # Use custom ndarray text formatting with monospace font - formatter = "{{:{}}}".format(mspec) - # Need to override for scalars, which are detected as iterable, - # and don't respond to printoptions. - if self.magnitude.ndim == 0: - allf = plain_allf = "{} {}" - mstr = formatter.format(obj.magnitude) - else: - with np.printoptions( - formatter={"float_kind": formatter.format} - ): - mstr = ( - "
"
-                                + format(obj.magnitude).replace("\n", "
") - + "
" - ) - elif not iterable(obj.magnitude): - # Use plain text for scalars - mstr = format(obj.magnitude, mspec) - else: - # Use monospace font for other array-likes - mstr = ( - "
"
-                        + format(obj.magnitude, mspec).replace("\n", "
") - + "
" - ) - elif isinstance(self.magnitude, ndarray): - if "L" in uspec: - # Use ndarray LaTeX special formatting - mstr = ndarray_to_latex(obj.magnitude, mspec) - else: - # Use custom ndarray text formatting--need to handle scalars differently - # since they don't respond to printoptions - formatter = "{{:{}}}".format(mspec) - if obj.magnitude.ndim == 0: - mstr = formatter.format(obj.magnitude) - else: - with np.printoptions(formatter={"float_kind": formatter.format}): - mstr = format(obj.magnitude).replace("\n", "") - else: - mstr = format(obj.magnitude, mspec).replace("\n", "") - - if "L" in uspec: - mstr = self._exp_pattern.sub(r"\1\\times 10^{\2\3}", mstr) - elif "H" in uspec or "P" in uspec: - m = self._exp_pattern.match(mstr) - _exp_formatter = ( - _pretty_fmt_exponent if "P" in uspec else lambda s: f"{s}" - ) - if m: - exp = int(m.group(2) + m.group(3)) - mstr = self._exp_pattern.sub(r"\1×10" + _exp_formatter(exp), mstr) - - if allf == plain_allf and ustr.startswith("1 /"): - # Write e.g. "3 / s" instead of "3 1 / s" - ustr = ustr[2:] - return allf.format(mstr, ustr).strip() - - def _repr_pretty_(self, p, cycle): - if cycle: - super()._repr_pretty_(p, cycle) - else: - p.pretty(self.magnitude) - p.text(" ") - p.pretty(self.units) - - def format_babel(self, spec: str = "", **kwspec: Any) -> str: - spec = spec or self.default_format - - # standard cases - if "#" in spec: - spec = spec.replace("#", "") - obj = self.to_compact() - else: - obj = self - kwspec = dict(kwspec) - if "length" in kwspec: - kwspec["babel_length"] = kwspec.pop("length") - - loc = kwspec.get("locale", self._REGISTRY.fmt_locale) - if loc is None: - raise ValueError("Provide a `locale` value to localize translation.") - - kwspec["locale"] = babel_parse(loc) - kwspec["babel_plural_form"] = kwspec["locale"].plural_form(obj.magnitude) - return "{} {}".format( - format(obj.magnitude, remove_custom_flags(spec)), - obj.units.format_babel(spec, **kwspec), - ).replace("\n", "") - - def __str__(self) -> str: - if self._REGISTRY.fmt_locale is not None: - return self.format_babel() - - return format(self) - - -class FormattingUnit: - def __str__(self): - return format(self) - - def __format__(self, spec) -> str: - _, uspec = split_format( - spec, self.default_format, self._REGISTRY.separate_format_defaults - ) - if "~" in uspec: - if not self._units: - return "" - units = UnitsContainer( - dict( - (self._REGISTRY._get_symbol(key), value) - for key, value in self._units.items() - ) - ) - uspec = uspec.replace("~", "") - else: - units = self._units - - return format_unit(units, uspec, registry=self._REGISTRY) - - def format_babel(self, spec="", locale=None, **kwspec: Any) -> str: - spec = spec or extract_custom_flags(self.default_format) - - if "~" in spec: - if self.dimensionless: - return "" - units = UnitsContainer( - dict( - (self._REGISTRY._get_symbol(key), value) - for key, value in self._units.items() - ) - ) - spec = spec.replace("~", "") - else: - units = self._units - - locale = self._REGISTRY.fmt_locale if locale is None else locale - - if locale is None: - raise ValueError("Provide a `locale` value to localize translation.") - else: - kwspec["locale"] = babel_parse(locale) - - return units.format_babel(spec, registry=self._REGISTRY, **kwspec) - - -class FormattingRegistry: - - _quantity_class = FormattingQuantity - _unit_class = FormattingUnit +__all__ = [FormattingQuantity, FormattingUnit, FormattingRegistry] diff --git a/pint/facets/formatting/objects.py b/pint/facets/formatting/objects.py new file mode 100644 index 0000000..fb701f1 --- /dev/null +++ b/pint/facets/formatting/objects.py @@ -0,0 +1,227 @@ +""" + pint.facets.formatting.objects + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import re +from typing import Any + +from ...compat import babel_parse, ndarray, np +from ...formatting import ( + _pretty_fmt_exponent, + extract_custom_flags, + format_unit, + ndarray_to_latex, + remove_custom_flags, + siunitx_format_unit, + split_format, +) +from ...util import iterable +from ..plain import UnitsContainer + + +class FormattingQuantity: + + _exp_pattern = re.compile(r"([0-9]\.?[0-9]*)e(-?)\+?0*([0-9]+)") + + def __format__(self, spec: str) -> str: + if self._REGISTRY.fmt_locale is not None: + return self.format_babel(spec) + + mspec, uspec = split_format( + spec, self.default_format, self._REGISTRY.separate_format_defaults + ) + + # If Compact is selected, do it at the beginning + if "#" in spec: + # TODO: don't replace '#' + mspec = mspec.replace("#", "") + uspec = uspec.replace("#", "") + obj = self.to_compact() + else: + obj = self + + if "L" in uspec: + allf = plain_allf = r"{}\ {}" + elif "H" in uspec: + allf = plain_allf = "{} {}" + if iterable(obj.magnitude): + # Use HTML table instead of plain text template for array-likes + allf = ( + "" + "" + "" + "" + "
Magnitude{}
Units{}
" + ) + else: + allf = plain_allf = "{} {}" + + if "Lx" in uspec: + # the LaTeX siunitx code + # TODO: add support for extracting options + opts = "" + ustr = siunitx_format_unit(obj.units._units, obj._REGISTRY) + allf = r"\SI[%s]{{{}}}{{{}}}" % opts + else: + # Hand off to unit formatting + # TODO: only use `uspec` after completing the deprecation cycle + ustr = format(obj.units, mspec + uspec) + + # mspec = remove_custom_flags(spec) + if "H" in uspec: + # HTML formatting + if hasattr(obj.magnitude, "_repr_html_"): + # If magnitude has an HTML repr, nest it within Pint's + mstr = obj.magnitude._repr_html_() + else: + if isinstance(self.magnitude, ndarray): + # Use custom ndarray text formatting with monospace font + formatter = "{{:{}}}".format(mspec) + # Need to override for scalars, which are detected as iterable, + # and don't respond to printoptions. + if self.magnitude.ndim == 0: + allf = plain_allf = "{} {}" + mstr = formatter.format(obj.magnitude) + else: + with np.printoptions( + formatter={"float_kind": formatter.format} + ): + mstr = ( + "
"
+                                + format(obj.magnitude).replace("\n", "
") + + "
" + ) + elif not iterable(obj.magnitude): + # Use plain text for scalars + mstr = format(obj.magnitude, mspec) + else: + # Use monospace font for other array-likes + mstr = ( + "
"
+                        + format(obj.magnitude, mspec).replace("\n", "
") + + "
" + ) + elif isinstance(self.magnitude, ndarray): + if "L" in uspec: + # Use ndarray LaTeX special formatting + mstr = ndarray_to_latex(obj.magnitude, mspec) + else: + # Use custom ndarray text formatting--need to handle scalars differently + # since they don't respond to printoptions + formatter = "{{:{}}}".format(mspec) + if obj.magnitude.ndim == 0: + mstr = formatter.format(obj.magnitude) + else: + with np.printoptions(formatter={"float_kind": formatter.format}): + mstr = format(obj.magnitude).replace("\n", "") + else: + mstr = format(obj.magnitude, mspec).replace("\n", "") + + if "L" in uspec: + mstr = self._exp_pattern.sub(r"\1\\times 10^{\2\3}", mstr) + elif "H" in uspec or "P" in uspec: + m = self._exp_pattern.match(mstr) + _exp_formatter = ( + _pretty_fmt_exponent if "P" in uspec else lambda s: f"{s}" + ) + if m: + exp = int(m.group(2) + m.group(3)) + mstr = self._exp_pattern.sub(r"\1×10" + _exp_formatter(exp), mstr) + + if allf == plain_allf and ustr.startswith("1 /"): + # Write e.g. "3 / s" instead of "3 1 / s" + ustr = ustr[2:] + return allf.format(mstr, ustr).strip() + + def _repr_pretty_(self, p, cycle): + if cycle: + super()._repr_pretty_(p, cycle) + else: + p.pretty(self.magnitude) + p.text(" ") + p.pretty(self.units) + + def format_babel(self, spec: str = "", **kwspec: Any) -> str: + spec = spec or self.default_format + + # standard cases + if "#" in spec: + spec = spec.replace("#", "") + obj = self.to_compact() + else: + obj = self + kwspec = dict(kwspec) + if "length" in kwspec: + kwspec["babel_length"] = kwspec.pop("length") + + loc = kwspec.get("locale", self._REGISTRY.fmt_locale) + if loc is None: + raise ValueError("Provide a `locale` value to localize translation.") + + kwspec["locale"] = babel_parse(loc) + kwspec["babel_plural_form"] = kwspec["locale"].plural_form(obj.magnitude) + return "{} {}".format( + format(obj.magnitude, remove_custom_flags(spec)), + obj.units.format_babel(spec, **kwspec), + ).replace("\n", "") + + def __str__(self) -> str: + if self._REGISTRY.fmt_locale is not None: + return self.format_babel() + + return format(self) + + +class FormattingUnit: + def __str__(self): + return format(self) + + def __format__(self, spec) -> str: + _, uspec = split_format( + spec, self.default_format, self._REGISTRY.separate_format_defaults + ) + if "~" in uspec: + if not self._units: + return "" + units = UnitsContainer( + dict( + (self._REGISTRY._get_symbol(key), value) + for key, value in self._units.items() + ) + ) + uspec = uspec.replace("~", "") + else: + units = self._units + + return format_unit(units, uspec, registry=self._REGISTRY) + + def format_babel(self, spec="", locale=None, **kwspec: Any) -> str: + spec = spec or extract_custom_flags(self.default_format) + + if "~" in spec: + if self.dimensionless: + return "" + units = UnitsContainer( + dict( + (self._REGISTRY._get_symbol(key), value) + for key, value in self._units.items() + ) + ) + spec = spec.replace("~", "") + else: + units = self._units + + locale = self._REGISTRY.fmt_locale if locale is None else locale + + if locale is None: + raise ValueError("Provide a `locale` value to localize translation.") + else: + kwspec["locale"] = babel_parse(locale) + + return units.format_babel(spec, registry=self._REGISTRY, **kwspec) diff --git a/pint/facets/formatting/registry.py b/pint/facets/formatting/registry.py new file mode 100644 index 0000000..f3bd9c7 --- /dev/null +++ b/pint/facets/formatting/registry.py @@ -0,0 +1,17 @@ +""" + pint.facets.formatting.registry + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +from .objects import FormattingQuantity, FormattingUnit + + +class FormattingRegistry: + + _quantity_class = FormattingQuantity + _unit_class = FormattingUnit diff --git a/pint/facets/measurement/__init__.py b/pint/facets/measurement/__init__.py index f43598b..83454dc 100644 --- a/pint/facets/measurement/__init__.py +++ b/pint/facets/measurement/__init__.py @@ -1,213 +1,16 @@ -from __future__ import annotations - -import copy -import re - -from ...compat import ufloat -from ...formatting import _FORMATS, extract_custom_flags, siunitx_format_unit -from ...util import build_dependent_class, create_class_with_registry -from ..plain import PlainQuantity - -MISSING = object() - - -class MeasurementQuantity: - - # Measurement support - def plus_minus(self, error, relative=False): - if isinstance(error, self.__class__): - if relative: - raise ValueError("{} is not a valid relative error.".format(error)) - error = error.to(self._units).magnitude - else: - if relative: - error = error * abs(self.magnitude) - - return self._REGISTRY.Measurement(copy.copy(self.magnitude), error, self._units) - - -class Measurement(PlainQuantity): - """Implements a class to describe a quantity with uncertainty. - - Parameters - ---------- - value : pint.Quantity or any numeric type - The expected value of the measurement - error : pint.Quantity or any numeric type - The error or uncertainty of the measurement - - Returns - ------- - - """ - - def __new__(cls, value, error, units=MISSING): - if units is MISSING: - try: - value, units = value.magnitude, value.units - except AttributeError: - # if called with two arguments and the first looks like a ufloat - # then assume the second argument is the units, keep value intact - if hasattr(value, "nominal_value"): - units = error - error = MISSING # used for check below - else: - units = "" - try: - error = error.to(units).magnitude - except AttributeError: - pass - - if error is MISSING: - mag = value - elif error < 0: - raise ValueError("The magnitude of the error cannot be negative") - else: - mag = ufloat(value, error) - - inst = super().__new__(cls, mag, units) - return inst - - @property - def value(self): - return self._REGISTRY.Quantity(self.magnitude.nominal_value, self.units) - - @property - def error(self): - return self._REGISTRY.Quantity(self.magnitude.std_dev, self.units) - - @property - def rel(self): - return abs(self.magnitude.std_dev / self.magnitude.nominal_value) - - def __reduce__(self): - # See notes in Quantity.__reduce__ - from pint import _unpickle_measurement - - return _unpickle_measurement, (Measurement, self.magnitude, self._units) +""" + pint.facets.measurement + ~~~~~~~~~~~~~~~~~~~~~~~ - def __repr__(self): - return "".format( - self.magnitude.nominal_value, self.magnitude.std_dev, self.units - ) + Adds pint the capability to handle measurements (quantities with uncertainties). - def __str__(self): - return "{}".format(self) + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" - def __format__(self, spec): - - spec = spec or self.default_format - - # special cases - if "Lx" in spec: # the LaTeX siunitx code - # the uncertainties module supports formatting - # numbers in value(unc) notation (i.e. 1.23(45) instead of 1.23 +/- 0.45), - # using type code 'S', which siunitx actually accepts as input. - # However, the implementation is incompatible with siunitx. - # Uncertainties will do 9.1(1.1), which is invalid, should be 9.1(11). - # TODO: add support for extracting options - # - # Get rid of this code, we'll deal with it here - spec = spec.replace("Lx", "") - # The most compatible format from uncertainties is the default format, - # but even this requires fixups. - # For one, SIUnitx does not except some formats that unc does, like 'P', - # and 'S' is broken as stated, so... - spec = spec.replace("S", "").replace("P", "") - # get SIunitx options - # TODO: allow user to set this value, somehow - opts = _FORMATS["Lx"]["siopts"] - if opts != "": - opts = r"[" + opts + r"]" - # SI requires space between "+-" (or "\pm") and the nominal value - # and uncertainty, and doesn't accept "+/-", so this setting - # selects the desired replacement. - pm_fmt = _FORMATS["Lx"]["pm_fmt"] - mstr = format(self.magnitude, spec).replace(r"+/-", pm_fmt) - # Also, SIunitx doesn't accept parentheses, which uncs uses with - # scientific notation ('e' or 'E' and sometimes 'g' or 'G'). - mstr = mstr.replace("(", "").replace(")", " ") - ustr = siunitx_format_unit(self.units._units, self._REGISTRY) - return r"\SI%s{%s}{%s}" % (opts, mstr, ustr) - - # standard cases - if "L" in spec: - newpm = pm = r" \pm " - pars = _FORMATS["L"]["parentheses_fmt"] - elif "P" in spec: - newpm = pm = "±" - pars = _FORMATS["P"]["parentheses_fmt"] - else: - newpm = pm = "+/-" - pars = _FORMATS[""]["parentheses_fmt"] - - if "C" in spec: - sp = "" - newspec = spec.replace("C", "") - pars = _FORMATS["C"]["parentheses_fmt"] - else: - sp = " " - newspec = spec - - if "H" in spec: - newpm = "±" - newspec = spec.replace("H", "") - pars = _FORMATS["H"]["parentheses_fmt"] - - mag = format(self.magnitude, newspec).replace(pm, sp + newpm + sp) - if "(" in mag: - # Exponential format has its own parentheses - pars = "{}" - - if "L" in newspec and "S" in newspec: - mag = mag.replace("(", r"\left(").replace(")", r"\right)") - - if "L" in newspec: - space = r"\ " - else: - space = " " - - uspec = extract_custom_flags(spec) - ustr = format(self.units, uspec) - if not ("uS" in newspec or "ue" in newspec or "u%" in newspec): - mag = pars.format(mag) - - if "H" in spec: - # Fix exponential format - mag = re.sub(r"\)e\+0?(\d+)", r")×10\1", mag) - mag = re.sub(r"\)e-0?(\d+)", r")×10-\1", mag) - - return mag + space + ustr - - -# TODO: Remove in the near future -# This is kept for easy backward compatibility during refactoring. -_Measurement = Measurement - - -class MeasurementRegistry: - - _quantity_class = MeasurementQuantity - _measurement_class = Measurement - - def __init_subclass__(cls, **kwargs): - super().__init_subclass__() - - cls.Measurement = build_dependent_class( - cls, "Measurement", "_measurement_class" - ) - - def _init_dynamic_classes(self) -> None: - """Generate subclasses on the fly and attach them to self""" - super()._init_dynamic_classes() - - if ufloat is not None: - self.Measurement = create_class_with_registry(self, self.Measurement) - else: +from __future__ import annotations - def no_uncertainties(*args, **kwargs): - raise RuntimeError( - "Pint requires the 'uncertainties' package to create a Measurement object." - ) +from .objects import Measurement, MeasurementQuantity +from .registry import MeasurementRegistry - self.Measurement = no_uncertainties +__all__ = [Measurement, MeasurementQuantity, MeasurementRegistry] diff --git a/pint/facets/measurement/objects.py b/pint/facets/measurement/objects.py new file mode 100644 index 0000000..7817bdf --- /dev/null +++ b/pint/facets/measurement/objects.py @@ -0,0 +1,192 @@ +""" + pint.facets.measurement.objects + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + +from __future__ import annotations + +import copy +import re + +from ...compat import ufloat +from ...formatting import _FORMATS, extract_custom_flags, siunitx_format_unit +from ..plain import PlainQuantity + +MISSING = object() + + +class MeasurementQuantity: + + # Measurement support + def plus_minus(self, error, relative=False): + if isinstance(error, self.__class__): + if relative: + raise ValueError("{} is not a valid relative error.".format(error)) + error = error.to(self._units).magnitude + else: + if relative: + error = error * abs(self.magnitude) + + return self._REGISTRY.Measurement(copy.copy(self.magnitude), error, self._units) + + +class Measurement(PlainQuantity): + """Implements a class to describe a quantity with uncertainty. + + Parameters + ---------- + value : pint.Quantity or any numeric type + The expected value of the measurement + error : pint.Quantity or any numeric type + The error or uncertainty of the measurement + + Returns + ------- + + """ + + def __new__(cls, value, error, units=MISSING): + if units is MISSING: + try: + value, units = value.magnitude, value.units + except AttributeError: + # if called with two arguments and the first looks like a ufloat + # then assume the second argument is the units, keep value intact + if hasattr(value, "nominal_value"): + units = error + error = MISSING # used for check below + else: + units = "" + try: + error = error.to(units).magnitude + except AttributeError: + pass + + if error is MISSING: + mag = value + elif error < 0: + raise ValueError("The magnitude of the error cannot be negative") + else: + mag = ufloat(value, error) + + inst = super().__new__(cls, mag, units) + return inst + + @property + def value(self): + return self._REGISTRY.Quantity(self.magnitude.nominal_value, self.units) + + @property + def error(self): + return self._REGISTRY.Quantity(self.magnitude.std_dev, self.units) + + @property + def rel(self): + return abs(self.magnitude.std_dev / self.magnitude.nominal_value) + + def __reduce__(self): + # See notes in Quantity.__reduce__ + from pint import _unpickle_measurement + + return _unpickle_measurement, (Measurement, self.magnitude, self._units) + + def __repr__(self): + return "".format( + self.magnitude.nominal_value, self.magnitude.std_dev, self.units + ) + + def __str__(self): + return "{}".format(self) + + def __format__(self, spec): + + spec = spec or self.default_format + + # special cases + if "Lx" in spec: # the LaTeX siunitx code + # the uncertainties module supports formatting + # numbers in value(unc) notation (i.e. 1.23(45) instead of 1.23 +/- 0.45), + # using type code 'S', which siunitx actually accepts as input. + # However, the implementation is incompatible with siunitx. + # Uncertainties will do 9.1(1.1), which is invalid, should be 9.1(11). + # TODO: add support for extracting options + # + # Get rid of this code, we'll deal with it here + spec = spec.replace("Lx", "") + # The most compatible format from uncertainties is the default format, + # but even this requires fixups. + # For one, SIUnitx does not except some formats that unc does, like 'P', + # and 'S' is broken as stated, so... + spec = spec.replace("S", "").replace("P", "") + # get SIunitx options + # TODO: allow user to set this value, somehow + opts = _FORMATS["Lx"]["siopts"] + if opts != "": + opts = r"[" + opts + r"]" + # SI requires space between "+-" (or "\pm") and the nominal value + # and uncertainty, and doesn't accept "+/-", so this setting + # selects the desired replacement. + pm_fmt = _FORMATS["Lx"]["pm_fmt"] + mstr = format(self.magnitude, spec).replace(r"+/-", pm_fmt) + # Also, SIunitx doesn't accept parentheses, which uncs uses with + # scientific notation ('e' or 'E' and sometimes 'g' or 'G'). + mstr = mstr.replace("(", "").replace(")", " ") + ustr = siunitx_format_unit(self.units._units, self._REGISTRY) + return r"\SI%s{%s}{%s}" % (opts, mstr, ustr) + + # standard cases + if "L" in spec: + newpm = pm = r" \pm " + pars = _FORMATS["L"]["parentheses_fmt"] + elif "P" in spec: + newpm = pm = "±" + pars = _FORMATS["P"]["parentheses_fmt"] + else: + newpm = pm = "+/-" + pars = _FORMATS[""]["parentheses_fmt"] + + if "C" in spec: + sp = "" + newspec = spec.replace("C", "") + pars = _FORMATS["C"]["parentheses_fmt"] + else: + sp = " " + newspec = spec + + if "H" in spec: + newpm = "±" + newspec = spec.replace("H", "") + pars = _FORMATS["H"]["parentheses_fmt"] + + mag = format(self.magnitude, newspec).replace(pm, sp + newpm + sp) + if "(" in mag: + # Exponential format has its own parentheses + pars = "{}" + + if "L" in newspec and "S" in newspec: + mag = mag.replace("(", r"\left(").replace(")", r"\right)") + + if "L" in newspec: + space = r"\ " + else: + space = " " + + uspec = extract_custom_flags(spec) + ustr = format(self.units, uspec) + if not ("uS" in newspec or "ue" in newspec or "u%" in newspec): + mag = pars.format(mag) + + if "H" in spec: + # Fix exponential format + mag = re.sub(r"\)e\+0?(\d+)", r")×10\1", mag) + mag = re.sub(r"\)e-0?(\d+)", r")×10-\1", mag) + + return mag + space + ustr + + +# TODO: Remove in the near future +# This is kept for easy backward compatibility during refactoring. +_Measurement = Measurement diff --git a/pint/facets/measurement/registry.py b/pint/facets/measurement/registry.py new file mode 100644 index 0000000..77bde3d --- /dev/null +++ b/pint/facets/measurement/registry.py @@ -0,0 +1,42 @@ +""" + pint.facets.measurement.objects + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + + +from __future__ import annotations + +from ...compat import ufloat +from ...util import build_dependent_class, create_class_with_registry +from .objects import Measurement, MeasurementQuantity + + +class MeasurementRegistry: + + _quantity_class = MeasurementQuantity + _measurement_class = Measurement + + def __init_subclass__(cls, **kwargs): + super().__init_subclass__() + + cls.Measurement = build_dependent_class( + cls, "Measurement", "_measurement_class" + ) + + def _init_dynamic_classes(self) -> None: + """Generate subclasses on the fly and attach them to self""" + super()._init_dynamic_classes() + + if ufloat is not None: + self.Measurement = create_class_with_registry(self, self.Measurement) + else: + + def no_uncertainties(*args, **kwargs): + raise RuntimeError( + "Pint requires the 'uncertainties' package to create a Measurement object." + ) + + self.Measurement = no_uncertainties diff --git a/pint/facets/nonmultiplicative/__init__.py b/pint/facets/nonmultiplicative/__init__.py index 3aca18c..56b8710 100644 --- a/pint/facets/nonmultiplicative/__init__.py +++ b/pint/facets/nonmultiplicative/__init__.py @@ -1,8 +1,10 @@ """ - pint.facets.group - ~~~~~~~~~~~~~~~~~ + pint.facets.nonmultiplicative + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Adds pint the capability to group units. + Adds pint the capability to handle nonmultiplicative units: + - offset + - logarithmic :copyright: 2022 by Pint Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. diff --git a/pint/facets/nonmultiplicative/definitions.py b/pint/facets/nonmultiplicative/definitions.py index 370501f..024fedb 100644 --- a/pint/facets/nonmultiplicative/definitions.py +++ b/pint/facets/nonmultiplicative/definitions.py @@ -1,3 +1,11 @@ +""" + pint.facets.nonmultiplicative.definitions + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + from __future__ import annotations from dataclasses import dataclass diff --git a/pint/facets/nonmultiplicative/objects.py b/pint/facets/nonmultiplicative/objects.py index d11d0f6..1708e32 100644 --- a/pint/facets/nonmultiplicative/objects.py +++ b/pint/facets/nonmultiplicative/objects.py @@ -1,3 +1,11 @@ +""" + pint.facets.nonmultiplicative.objects + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + from __future__ import annotations from typing import List diff --git a/pint/facets/nonmultiplicative/registry.py b/pint/facets/nonmultiplicative/registry.py index 0cf2e14..0f2e30c 100644 --- a/pint/facets/nonmultiplicative/registry.py +++ b/pint/facets/nonmultiplicative/registry.py @@ -1,3 +1,11 @@ +""" + pint.facets.nonmultiplicative.registry + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + from __future__ import annotations from typing import Any, Optional, Union diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py index fc71f9b..4662761 100644 --- a/pint/facets/numpy/numpy_func.py +++ b/pint/facets/numpy/numpy_func.py @@ -1,8 +1,8 @@ """ - pint.numpy_func - ~~~~~~~~~~~~~~~ + pint.facets.numpy.numpy_func + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - :copyright: 2019 by Pint Authors, see AUTHORS for more details. + :copyright: 2022 by Pint Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ diff --git a/pint/facets/numpy/quantity.py b/pint/facets/numpy/quantity.py index b064f2d..2436100 100644 --- a/pint/facets/numpy/quantity.py +++ b/pint/facets/numpy/quantity.py @@ -1,3 +1,11 @@ +""" + pint.facets.numpy.quantity + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + from __future__ import annotations import functools diff --git a/pint/facets/numpy/unit.py b/pint/facets/numpy/unit.py index c4d81a8..fc94853 100644 --- a/pint/facets/numpy/unit.py +++ b/pint/facets/numpy/unit.py @@ -1,3 +1,11 @@ +""" + pint.facets.numpy.unit + ~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + from __future__ import annotations from ...compat import is_upcast_type diff --git a/pint/facets/plain/definitions.py b/pint/facets/plain/definitions.py index f17b0d5..c1d1d9a 100644 --- a/pint/facets/plain/definitions.py +++ b/pint/facets/plain/definitions.py @@ -2,8 +2,6 @@ pint.facets.plain.definitions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Base unit converting capabilites. - :copyright: 2022 by Pint Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ diff --git a/pint/facets/plain/registry.py b/pint/facets/plain/registry.py index 0f7c569..e235a60 100644 --- a/pint/facets/plain/registry.py +++ b/pint/facets/plain/registry.py @@ -1,3 +1,11 @@ +""" + pint.facets.plain.registry + ~~~~~~~~~~~~~~~~~~~~~~~~~~ + + :copyright: 2022 by Pint Authors, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" + from __future__ import annotations import copy diff --git a/pint/facets/plain/unit.py b/pint/facets/plain/unit.py index c8af857..5fb050b 100644 --- a/pint/facets/plain/unit.py +++ b/pint/facets/plain/unit.py @@ -2,8 +2,6 @@ pint.facets.plain.unit ~~~~~~~~~~~~~~~~~~~~~ - Functions and classes related to unit definitions and conversions. - :copyright: 2016 by Pint Authors, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ diff --git a/pint/pint-convert b/pint/pint-convert index 2d8eae0..600016b 100755 --- a/pint/pint-convert +++ b/pint/pint-convert @@ -8,6 +8,8 @@ :license: BSD, see LICENSE for more details. """ +from __future__ import annotations + import argparse import re diff --git a/pint/util.py b/pint/util.py index 99c03f9..2b95765 100644 --- a/pint/util.py +++ b/pint/util.py @@ -29,10 +29,9 @@ from .formatting import format_unit from .pint_eval import build_eval_tree if TYPE_CHECKING: - from pint.facets.plain.quantity import Quantity + from pint import Quantity, UnitRegistry from ._typing import UnitLike - from .facets.plain import PlainRegistry logger = logging.getLogger(__name__) logger.addHandler(NullHandler()) @@ -810,7 +809,7 @@ class SharedRegistryObject: """ - _REGISTRY: ClassVar[PlainRegistry] + _REGISTRY: ClassVar[UnitRegistry] _units: UnitsContainer def __new__(cls, *args, **kwargs): @@ -876,7 +875,7 @@ class PrettyIPython: def to_units_container( - unit_like: Union[UnitLike, Quantity], registry: Optional[PlainRegistry] = None + unit_like: Union[UnitLike, Quantity], registry: Optional[UnitRegistry] = None ) -> UnitsContainer: """Convert a unit compatible type to a UnitsContainer. @@ -909,7 +908,7 @@ def to_units_container( def infer_base_unit( - unit_like: Union[UnitLike, Quantity], registry: Optional[PlainRegistry] = None + unit_like: Union[UnitLike, Quantity], registry: Optional[UnitRegistry] = None ) -> UnitsContainer: """ Given a Quantity or UnitLike, give the UnitsContainer for it's plain units. @@ -919,7 +918,7 @@ def infer_base_unit( unit_like : Union[UnitLike, Quantity] Quantity or Unit to infer the plain units from. - registry: Optional[PlainRegistry] + registry: Optional[UnitRegistry] If provided, uses the registry's UnitsContainer and parse_unit_name. If None, uses the registry attached to unit_like. -- cgit v1.2.1