summaryrefslogtreecommitdiff
path: root/pint
diff options
context:
space:
mode:
authorRyan May <rmay@ucar.edu>2022-11-28 14:48:09 -0700
committerGitHub <noreply@github.com>2022-11-28 14:48:09 -0700
commit6ec28e21c4a2961a0a3421516f8874d27f941c0f (patch)
treeddd7bd899ec39bd40667b358ab348d0a1b983b9a /pint
parentc659d9ed8dda8b4223f157addc7ee6435566cb94 (diff)
parent1ee193fa8ee699495a679aa58fe99a8ffcface81 (diff)
downloadpint-6ec28e21c4a2961a0a3421516f8874d27f941c0f.tar.gz
Merge branch 'master' into fix-1584
Diffstat (limited to 'pint')
-rw-r--r--pint/__init__.py2
-rw-r--r--pint/_vendor/flexparser.py1455
-rw-r--r--pint/compat.py4
-rw-r--r--pint/default_en.txt17
-rw-r--r--pint/definitions.py149
-rw-r--r--pint/delegates/__init__.py14
-rw-r--r--pint/delegates/base_defparser.py107
-rw-r--r--pint/delegates/txt_defparser/__init__.py14
-rw-r--r--pint/delegates/txt_defparser/block.py45
-rw-r--r--pint/delegates/txt_defparser/common.py58
-rw-r--r--pint/delegates/txt_defparser/context.py196
-rw-r--r--pint/delegates/txt_defparser/defaults.py77
-rw-r--r--pint/delegates/txt_defparser/defparser.py120
-rw-r--r--pint/delegates/txt_defparser/group.py106
-rw-r--r--pint/delegates/txt_defparser/plain.py283
-rw-r--r--pint/delegates/txt_defparser/system.py110
-rw-r--r--pint/errors.py193
-rw-r--r--pint/facets/__init__.py7
-rw-r--r--pint/facets/context/definitions.py267
-rw-r--r--pint/facets/context/objects.py37
-rw-r--r--pint/facets/context/registry.py20
-rw-r--r--pint/facets/dask/__init__.py4
-rw-r--r--pint/facets/formatting/objects.py3
-rw-r--r--pint/facets/group/definitions.py104
-rw-r--r--pint/facets/group/objects.py26
-rw-r--r--pint/facets/group/registry.py44
-rw-r--r--pint/facets/measurement/registry.py4
-rw-r--r--pint/facets/nonmultiplicative/registry.py65
-rw-r--r--pint/facets/numpy/numpy_func.py14
-rw-r--r--pint/facets/plain/__init__.py3
-rw-r--r--pint/facets/plain/definitions.py378
-rw-r--r--pint/facets/plain/quantity.py6
-rw-r--r--pint/facets/plain/registry.py322
-rw-r--r--pint/facets/system/definitions.py122
-rw-r--r--pint/facets/system/objects.py13
-rw-r--r--pint/facets/system/registry.py28
-rw-r--r--pint/formatting.py5
-rw-r--r--pint/parser.py374
-rw-r--r--pint/testsuite/test_contexts.py11
-rw-r--r--pint/testsuite/test_definitions.py5
-rw-r--r--pint/testsuite/test_diskcache.py10
-rw-r--r--pint/testsuite/test_errors.py40
-rw-r--r--pint/testsuite/test_formatting.py15
-rw-r--r--pint/testsuite/test_issues.py32
-rw-r--r--pint/testsuite/test_non_int.py24
-rw-r--r--pint/testsuite/test_numpy.py18
-rw-r--r--pint/testsuite/test_quantity.py2
-rw-r--r--pint/testsuite/test_umath.py68
-rw-r--r--pint/testsuite/test_unit.py25
-rw-r--r--pint/util.py91
50 files changed, 3598 insertions, 1539 deletions
diff --git a/pint/__init__.py b/pint/__init__.py
index b30409a..ee80048 100644
--- a/pint/__init__.py
+++ b/pint/__init__.py
@@ -65,7 +65,7 @@ def _unpickle(cls, *args):
object of type cls
"""
- from pint.facets.plain import UnitsContainer
+ from pint.util import UnitsContainer
for arg in args:
# Prefixed units are defined within the registry
diff --git a/pint/_vendor/flexparser.py b/pint/_vendor/flexparser.py
new file mode 100644
index 0000000..8945b6e
--- /dev/null
+++ b/pint/_vendor/flexparser.py
@@ -0,0 +1,1455 @@
+"""
+ flexparser.flexparser
+ ~~~~~~~~~~~~~~~~~~~~~
+
+ Classes and functions to create parsers.
+
+ The idea is quite simple. You write a class for every type of content
+ (called here ``ParsedStatement``) you need to parse. Each class should
+ have a ``from_string`` constructor. We used extensively the ``typing``
+ module to make the output structure easy to use and less error prone.
+
+ For more information, take a look at https://github.com/hgrecco/flexparser
+
+ :copyright: 2022 by flexparser Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+import collections
+import dataclasses
+import enum
+import functools
+import hashlib
+import hmac
+import inspect
+import logging
+import pathlib
+import re
+import sys
+import typing as ty
+from collections.abc import Iterator
+from dataclasses import dataclass
+from functools import cached_property
+from importlib import resources
+from typing import Optional, Tuple, Type
+
+_LOGGER = logging.getLogger("flexparser")
+
+_SENTINEL = object()
+
+
+################
+# Exceptions
+################
+
+
+@dataclass(frozen=True)
+class Statement:
+ """Base class for parsed elements within a source file."""
+
+ start_line: int = dataclasses.field(init=False, default=None)
+ start_col: int = dataclasses.field(init=False, default=None)
+
+ end_line: int = dataclasses.field(init=False, default=None)
+ end_col: int = dataclasses.field(init=False, default=None)
+
+ raw: str = dataclasses.field(init=False, default=None)
+
+ @classmethod
+ def from_statement(cls, statement: Statement):
+ out = cls()
+ out.set_position(*statement.get_position())
+ out.set_raw(statement.raw)
+ return out
+
+ @classmethod
+ def from_statement_iterator_element(cls, values: ty.Tuple[int, int, int, int, str]):
+ out = cls()
+ out.set_position(*values[:-1])
+ out.set_raw(values[-1])
+ return out
+
+ @property
+ def format_position(self):
+ if self.start_line is None:
+ return "N/A"
+ return "%d,%d-%d,%d" % self.get_position()
+
+ @property
+ def raw_strip(self):
+ return self.raw.strip()
+
+ def get_position(self):
+ return self.start_line, self.start_col, self.end_line, self.end_col
+
+ def set_position(self, start_line, start_col, end_line, end_col):
+ object.__setattr__(self, "start_line", start_line)
+ object.__setattr__(self, "start_col", start_col)
+ object.__setattr__(self, "end_line", end_line)
+ object.__setattr__(self, "end_col", end_col)
+ return self
+
+ def set_raw(self, raw):
+ object.__setattr__(self, "raw", raw)
+ return self
+
+ def set_simple_position(self, line, col, width):
+ return self.set_position(line, col, line, col + width)
+
+
+@dataclass(frozen=True)
+class ParsingError(Statement, Exception):
+ """Base class for all parsing exceptions in this package."""
+
+ def __str__(self):
+ return Statement.__str__(self)
+
+
+@dataclass(frozen=True)
+class UnknownStatement(ParsingError):
+ """A string statement could not bee parsed."""
+
+ def __str__(self):
+ return f"Could not parse '{self.raw}' ({self.format_position})"
+
+
+@dataclass(frozen=True)
+class UnhandledParsingError(ParsingError):
+ """Base class for all parsing exceptions in this package."""
+
+ ex: Exception
+
+ def __str__(self):
+ return f"Unhandled exception while parsing '{self.raw}' ({self.format_position}): {self.ex}"
+
+
+@dataclass(frozen=True)
+class UnexpectedEOF(ParsingError):
+ """End of file was found within an open block."""
+
+
+#############################
+# Useful methods and classes
+#############################
+
+
+@dataclass(frozen=True)
+class Hash:
+ algorithm_name: str
+ hexdigest: str
+
+ def __eq__(self, other: Hash):
+ return (
+ isinstance(other, Hash)
+ and self.algorithm_name != ""
+ and self.algorithm_name == other.algorithm_name
+ and hmac.compare_digest(self.hexdigest, other.hexdigest)
+ )
+
+ @classmethod
+ def from_bytes(cls, algorithm, b: bytes):
+ hasher = algorithm(b)
+ return cls(hasher.name, hasher.hexdigest())
+
+ @classmethod
+ def from_file_pointer(cls, algorithm, fp: ty.BinaryIO):
+ return cls.from_bytes(algorithm, fp.read())
+
+ @classmethod
+ def nullhash(cls):
+ return cls("", "")
+
+
+def _yield_types(
+ obj, valid_subclasses=(object,), recurse_origin=(tuple, list, ty.Union)
+):
+ """Recursively transverse type annotation if the
+ origin is any of the types in `recurse_origin`
+ and yield those type which are subclasses of `valid_subclasses`.
+
+ """
+ if ty.get_origin(obj) in recurse_origin:
+ for el in ty.get_args(obj):
+ yield from _yield_types(el, valid_subclasses, recurse_origin)
+ else:
+ if inspect.isclass(obj) and issubclass(obj, valid_subclasses):
+ yield obj
+
+
+class classproperty: # noqa N801
+ """Decorator for a class property
+
+ In Python 3.9+ can be replaced by
+
+ @classmethod
+ @property
+ def myprop(self):
+ return 42
+
+ """
+
+ def __init__(self, fget):
+ self.fget = fget
+
+ def __get__(self, owner_self, owner_cls):
+ return self.fget(owner_cls)
+
+
+def is_relative_to(self, *other):
+ """Return True if the path is relative to another path or False.
+
+ In Python 3.9+ can be replaced by
+
+ path.is_relative_to(other)
+ """
+ try:
+ self.relative_to(*other)
+ return True
+ except ValueError:
+ return False
+
+
+class DelimiterInclude(enum.IntEnum):
+ """Specifies how to deal with delimiters while parsing."""
+
+ #: Split at delimiter, not including in any string
+ SPLIT = enum.auto()
+
+ #: Split after, keeping the delimiter with previous string.
+ SPLIT_AFTER = enum.auto()
+
+ #: Split before, keeping the delimiter with next string.
+ SPLIT_BEFORE = enum.auto()
+
+ #: Do not split at delimiter.
+ DO_NOT_SPLIT = enum.auto()
+
+
+class DelimiterAction(enum.IntEnum):
+ """Specifies how to deal with delimiters while parsing."""
+
+ #: Continue parsing normally.
+ CONTINUE = enum.auto()
+
+ #: Capture everything til end of line as a whole.
+ CAPTURE_NEXT_TIL_EOL = enum.auto()
+
+ #: Stop parsing line and move to next.
+ STOP_PARSING_LINE = enum.auto()
+
+ #: Stop parsing content.
+ STOP_PARSING = enum.auto()
+
+
+DO_NOT_SPLIT_EOL = {
+ "\r\n": (DelimiterInclude.DO_NOT_SPLIT, DelimiterAction.CONTINUE),
+ "\n": (DelimiterInclude.DO_NOT_SPLIT, DelimiterAction.CONTINUE),
+ "\r": (DelimiterInclude.DO_NOT_SPLIT, DelimiterAction.CONTINUE),
+}
+
+SPLIT_EOL = {
+ "\r\n": (DelimiterInclude.SPLIT, DelimiterAction.CONTINUE),
+ "\n": (DelimiterInclude.SPLIT, DelimiterAction.CONTINUE),
+ "\r": (DelimiterInclude.SPLIT, DelimiterAction.CONTINUE),
+}
+
+_EOLs_set = set(DO_NOT_SPLIT_EOL.keys())
+
+
+@functools.lru_cache
+def _build_delimiter_pattern(delimiters: ty.Tuple[str, ...]) -> re.Pattern:
+ """Compile a tuple of delimiters into a regex expression with a capture group
+ around the delimiter.
+ """
+ return re.compile("|".join(f"({re.escape(el)})" for el in delimiters))
+
+
+############
+# Iterators
+############
+
+DelimiterDictT = ty.Dict[str, ty.Tuple[DelimiterInclude, DelimiterAction]]
+
+
+class Spliter:
+ """Content iterator splitting according to given delimiters.
+
+ The pattern can be changed dynamically sending a new pattern to the generator,
+ see DelimiterInclude and DelimiterAction for more information.
+
+ The current scanning position can be changed at any time.
+
+ Parameters
+ ----------
+ content : str
+ delimiters : ty.Dict[str, ty.Tuple[DelimiterInclude, DelimiterAction]]
+
+ Yields
+ ------
+ start_line : int
+ line number of the start of the content (zero-based numbering).
+ start_col : int
+ column number of the start of the content (zero-based numbering).
+ end_line : int
+ line number of the end of the content (zero-based numbering).
+ end_col : int
+ column number of the end of the content (zero-based numbering).
+ part : str
+ part of the text between delimiters.
+ """
+
+ _pattern: ty.Optional[re.Pattern]
+ _delimiters: DelimiterDictT
+
+ __stop_searching_in_line = False
+
+ __pending = ""
+ __first_line_col = None
+
+ __lines = ()
+ __lineno = 0
+ __colno = 0
+
+ def __init__(self, content: str, delimiters: DelimiterDictT):
+ self.set_delimiters(delimiters)
+ self.__lines = content.splitlines(keepends=True)
+
+ def set_position(self, lineno: int, colno: int):
+ self.__lineno, self.__colno = lineno, colno
+
+ def set_delimiters(self, delimiters: DelimiterDictT):
+ for k, v in delimiters.items():
+ if v == (DelimiterInclude.DO_NOT_SPLIT, DelimiterAction.STOP_PARSING):
+ raise ValueError(
+ f"The delimiter action for {k} is not a valid combination ({v})"
+ )
+ # Build a pattern but removing eols
+ _pat_dlm = tuple(set(delimiters.keys()) - _EOLs_set)
+ if _pat_dlm:
+ self._pattern = _build_delimiter_pattern(_pat_dlm)
+ else:
+ self._pattern = None
+ # We add the end of line as delimiters if not present.
+ self._delimiters = {**DO_NOT_SPLIT_EOL, **delimiters}
+
+ def __iter__(self):
+ return self
+
+ def __next__(self):
+ if self.__lineno >= len(self.__lines):
+ raise StopIteration
+
+ while True:
+ if self.__stop_searching_in_line:
+ # There must be part of a line pending to parse
+ # due to stop
+ line = self.__lines[self.__lineno]
+ mo = None
+ self.__stop_searching_in_line = False
+ else:
+ # We get the current line and the find the first delimiter.
+ line = self.__lines[self.__lineno]
+ if self._pattern is None:
+ mo = None
+ else:
+ mo = self._pattern.search(line, self.__colno)
+
+ if mo is None:
+ # No delimiter was found,
+ # which should happen at end of the content or end of line
+ for k in DO_NOT_SPLIT_EOL.keys():
+ if line.endswith(k):
+ dlm = line[-len(k) :]
+ end_col, next_col = len(line) - len(k), 0
+ break
+ else:
+ # No EOL found, this is end of content
+ dlm = None
+ end_col, next_col = len(line), 0
+
+ next_line = self.__lineno + 1
+
+ else:
+ next_line = self.__lineno
+ end_col, next_col = mo.span()
+ dlm = mo.group()
+
+ part = line[self.__colno : end_col]
+
+ include, action = self._delimiters.get(
+ dlm, (DelimiterInclude.SPLIT, DelimiterAction.STOP_PARSING)
+ )
+
+ if include == DelimiterInclude.SPLIT:
+ next_pending = ""
+ elif include == DelimiterInclude.SPLIT_AFTER:
+ end_col += len(dlm)
+ part = part + dlm
+ next_pending = ""
+ elif include == DelimiterInclude.SPLIT_BEFORE:
+ next_pending = dlm
+ elif include == DelimiterInclude.DO_NOT_SPLIT:
+ self.__pending += line[self.__colno : end_col] + dlm
+ next_pending = ""
+ else:
+ raise ValueError(f"Unknown action {include}.")
+
+ if action == DelimiterAction.STOP_PARSING:
+ # this will raise a StopIteration in the next call.
+ next_line = len(self.__lines)
+ elif action == DelimiterAction.STOP_PARSING_LINE:
+ next_line = self.__lineno + 1
+ next_col = 0
+
+ start_line = self.__lineno
+ start_col = self.__colno
+ end_line = self.__lineno
+
+ self.__lineno = next_line
+ self.__colno = next_col
+
+ if action == DelimiterAction.CAPTURE_NEXT_TIL_EOL:
+ self.__stop_searching_in_line = True
+
+ if include == DelimiterInclude.DO_NOT_SPLIT:
+ self.__first_line_col = start_line, start_col
+ else:
+ if self.__first_line_col is None:
+ out = (
+ start_line,
+ start_col - len(self.__pending),
+ end_line,
+ end_col,
+ self.__pending + part,
+ )
+ else:
+ out = (
+ *self.__first_line_col,
+ end_line,
+ end_col,
+ self.__pending + part,
+ )
+ self.__first_line_col = None
+ self.__pending = next_pending
+ return out
+
+
+class StatementIterator:
+ """Content peekable iterator splitting according to given delimiters.
+
+ The pattern can be changed dynamically sending a new pattern to the generator,
+ see DelimiterInclude and DelimiterAction for more information.
+
+ Parameters
+ ----------
+ content : str
+ delimiters : dict[str, ty.Tuple[DelimiterInclude, DelimiterAction]]
+
+ Yields
+ ------
+ Statement
+ """
+
+ _cache: ty.Deque[Statement]
+
+ def __init__(
+ self, content: str, delimiters: DelimiterDictT, strip_spaces: bool = True
+ ):
+ self._cache = collections.deque()
+ self._spliter = Spliter(content, delimiters)
+ self._strip_spaces = strip_spaces
+
+ def __iter__(self):
+ return self
+
+ def set_delimiters(self, delimiters: DelimiterDictT):
+ self._spliter.set_delimiters(delimiters)
+ if self._cache:
+ value = self.peek()
+ # Elements are 1 based indexing, while splitter is 0 based.
+ self._spliter.set_position(value.start_line - 1, value.start_col)
+ self._cache.clear()
+
+ def _get_next_strip(self) -> Statement:
+ part = ""
+ while not part:
+ start_line, start_col, end_line, end_col, part = next(self._spliter)
+ lo = len(part)
+ part = part.lstrip()
+ start_col += lo - len(part)
+
+ lo = len(part)
+ part = part.rstrip()
+ end_col -= lo - len(part)
+
+ return Statement.from_statement_iterator_element(
+ (start_line + 1, start_col, end_line + 1, end_col, part)
+ )
+
+ def _get_next(self) -> Statement:
+ if self._strip_spaces:
+ return self._get_next_strip()
+
+ part = ""
+ while not part:
+ start_line, start_col, end_line, end_col, part = next(self._spliter)
+
+ return Statement.from_statement_iterator_element(
+ (start_line + 1, start_col, end_line + 1, end_col, part)
+ )
+
+ def peek(self, default=_SENTINEL) -> Statement:
+ """Return the item that will be next returned from ``next()``.
+
+ Return ``default`` if there are no items left. If ``default`` is not
+ provided, raise ``StopIteration``.
+
+ """
+ if not self._cache:
+ try:
+ self._cache.append(self._get_next())
+ except StopIteration:
+ if default is _SENTINEL:
+ raise
+ return default
+ return self._cache[0]
+
+ def __next__(self) -> Statement:
+ if self._cache:
+ return self._cache.popleft()
+ else:
+ return self._get_next()
+
+
+###########
+# Parsing
+###########
+
+# Configuration type
+CT = ty.TypeVar("CT")
+PST = ty.TypeVar("PST", bound="ParsedStatement")
+LineColStr = Tuple[int, int, str]
+FromString = ty.Union[None, PST, ParsingError]
+Consume = ty.Union[PST, ParsingError]
+NullableConsume = ty.Union[None, PST, ParsingError]
+
+Single = ty.Union[PST, ParsingError]
+Multi = ty.Tuple[ty.Union[PST, ParsingError], ...]
+
+
+@dataclass(frozen=True)
+class ParsedStatement(ty.Generic[CT], Statement):
+ """A single parsed statement.
+
+ In order to write your own, you need to subclass it as a
+ frozen dataclass and implement the parsing logic by overriding
+ `from_string` classmethod.
+
+ Takes two arguments: the string to parse and an object given
+ by the parser which can be used to store configuration information.
+
+ It should return an instance of this class if parsing
+ was successful or None otherwise
+ """
+
+ @classmethod
+ def from_string(cls: Type[PST], s: str) -> FromString[PST]:
+ """Parse a string into a ParsedStatement.
+
+ Return files and their meaning:
+ 1. None: the string cannot be parsed with this class.
+ 2. A subclass of ParsedStatement: the string was parsed successfully
+ 3. A subclass of ParsingError the string could be parsed with this class but there is
+ an error.
+ """
+ raise NotImplementedError(
+ "ParsedStatement subclasses must implement "
+ "'from_string' or 'from_string_and_config'"
+ )
+
+ @classmethod
+ def from_string_and_config(cls: Type[PST], s: str, config: CT) -> FromString[PST]:
+ """Parse a string into a ParsedStatement.
+
+ Return files and their meaning:
+ 1. None: the string cannot be parsed with this class.
+ 2. A subclass of ParsedStatement: the string was parsed successfully
+ 3. A subclass of ParsingError the string could be parsed with this class but there is
+ an error.
+ """
+ return cls.from_string(s)
+
+ @classmethod
+ def from_statement_and_config(
+ cls: Type[PST], statement: Statement, config: CT
+ ) -> FromString[PST]:
+ try:
+ out = cls.from_string_and_config(statement.raw, config)
+ except Exception as ex:
+ out = UnhandledParsingError(ex)
+
+ if out is None:
+ return None
+
+ out.set_position(*statement.get_position())
+ out.set_raw(statement.raw)
+ return out
+
+ @classmethod
+ def consume(
+ cls: Type[PST], statement_iterator: StatementIterator, config: CT
+ ) -> NullableConsume[PST]:
+ """Peek into the iterator and try to parse.
+
+ Return files and their meaning:
+ 1. None: the string cannot be parsed with this class, the iterator is kept an the current place.
+ 2. a subclass of ParsedStatement: the string was parsed successfully, advance the iterator.
+ 3. a subclass of ParsingError: the string could be parsed with this class but there is
+ an error, advance the iterator.
+ """
+ statement = statement_iterator.peek()
+ parsed_statement = cls.from_statement_and_config(statement, config)
+ if parsed_statement is None:
+ return None
+ next(statement_iterator)
+ return parsed_statement
+
+
+OPST = ty.TypeVar("OPST", bound="ParsedStatement")
+IPST = ty.TypeVar("IPST", bound="ParsedStatement")
+CPST = ty.TypeVar("CPST", bound="ParsedStatement")
+BT = ty.TypeVar("BT", bound="Block")
+RBT = ty.TypeVar("RBT", bound="RootBlock")
+
+
+@dataclass(frozen=True)
+class Block(ty.Generic[OPST, IPST, CPST, CT]):
+ """A sequence of statements with an opening, body and closing."""
+
+ opening: Consume[OPST]
+ body: Tuple[Consume[IPST], ...]
+ closing: Consume[CPST]
+
+ delimiters = {}
+
+ @property
+ def start_line(self):
+ return self.opening.start_line
+
+ @property
+ def start_col(self):
+ return self.opening.start_col
+
+ @property
+ def end_line(self):
+ return self.closing.end_line
+
+ @property
+ def end_col(self):
+ return self.closing.end_col
+
+ def get_position(self):
+ return self.start_line, self.start_col, self.end_line, self.end_col
+
+ @property
+ def format_position(self):
+ if self.start_line is None:
+ return "N/A"
+ return "%d,%d-%d,%d" % self.get_position()
+
+ @classmethod
+ def subclass_with(cls, *, opening=None, body=None, closing=None):
+ @dataclass(frozen=True)
+ class CustomBlock(Block):
+ pass
+
+ if opening:
+ CustomBlock.__annotations__["opening"] = Single[ty.Union[opening]]
+ if body:
+ CustomBlock.__annotations__["body"] = Multi[ty.Union[body]]
+ if closing:
+ CustomBlock.__annotations__["closing"] = Single[ty.Union[closing]]
+
+ return CustomBlock
+
+ def __iter__(self) -> Iterator[Statement]:
+ yield self.opening
+ for el in self.body:
+ if isinstance(el, Block):
+ yield from el
+ else:
+ yield el
+ yield self.closing
+
+ def iter_blocks(self) -> Iterator[ty.Union[Block, Statement]]:
+ yield self.opening
+ yield from self.body
+ yield self.closing
+
+ ###################################################
+ # Convenience methods to iterate parsed statements
+ ###################################################
+
+ _ElementT = ty.TypeVar("_ElementT", bound=Statement)
+
+ def filter_by(self, *klass: Type[_ElementT]) -> Iterator[_ElementT]:
+ """Yield elements of a given class or classes."""
+ yield from (el for el in self if isinstance(el, klass)) # noqa Bug in pycharm.
+
+ @cached_property
+ def errors(self) -> ty.Tuple[ParsingError, ...]:
+ """Tuple of errors found."""
+ return tuple(self.filter_by(ParsingError))
+
+ @property
+ def has_errors(self) -> bool:
+ """True if errors were found during parsing."""
+ return bool(self.errors)
+
+ ####################
+ # Statement classes
+ ####################
+
+ @classproperty
+ def opening_classes(cls) -> Iterator[Type[OPST]]:
+ """Classes representing any of the parsed statement that can open this block."""
+ opening = ty.get_type_hints(cls)["opening"]
+ yield from _yield_types(opening, ParsedStatement)
+
+ @classproperty
+ def body_classes(cls) -> Iterator[Type[IPST]]:
+ """Classes representing any of the parsed statement that can be in the body."""
+ body = ty.get_type_hints(cls)["body"]
+ yield from _yield_types(body, (ParsedStatement, Block))
+
+ @classproperty
+ def closing_classes(cls) -> Iterator[Type[CPST]]:
+ """Classes representing any of the parsed statement that can close this block."""
+ closing = ty.get_type_hints(cls)["closing"]
+ yield from _yield_types(closing, ParsedStatement)
+
+ ##########
+ # Consume
+ ##########
+
+ @classmethod
+ def consume_opening(
+ cls: Type[BT], statement_iterator: StatementIterator, config: CT
+ ) -> NullableConsume[OPST]:
+ """Peek into the iterator and try to parse with any of the opening classes.
+
+ See `ParsedStatement.consume` for more details.
+ """
+ for c in cls.opening_classes:
+ el = c.consume(statement_iterator, config)
+ if el is not None:
+ return el
+ return None
+
+ @classmethod
+ def consume_body(
+ cls, statement_iterator: StatementIterator, config: CT
+ ) -> Consume[IPST]:
+ """Peek into the iterator and try to parse with any of the body classes.
+
+ If the statement cannot be parsed, a UnknownStatement is returned.
+ """
+ for c in cls.body_classes:
+ el = c.consume(statement_iterator, config)
+ if el is not None:
+ return el
+ el = next(statement_iterator)
+ return UnknownStatement.from_statement(el)
+
+ @classmethod
+ def consume_closing(
+ cls: Type[BT], statement_iterator: StatementIterator, config: CT
+ ) -> NullableConsume[CPST]:
+ """Peek into the iterator and try to parse with any of the opening classes.
+
+ See `ParsedStatement.consume` for more details.
+ """
+ for c in cls.closing_classes:
+ el = c.consume(statement_iterator, config)
+ if el is not None:
+ return el
+ return None
+
+ @classmethod
+ def consume_body_closing(
+ cls: Type[BT], opening: OPST, statement_iterator: StatementIterator, config: CT
+ ) -> BT:
+ body = []
+ closing = None
+ last_line = opening.end_line
+ while closing is None:
+ try:
+ closing = cls.consume_closing(statement_iterator, config)
+ if closing is not None:
+ continue
+ el = cls.consume_body(statement_iterator, config)
+ body.append(el)
+ last_line = el.end_line
+ except StopIteration:
+ closing = cls.on_stop_iteration(config)
+ closing.set_position(last_line + 1, 0, last_line + 1, 0)
+
+ return cls(opening, tuple(body), closing)
+
+ @classmethod
+ def consume(
+ cls: Type[BT], statement_iterator: StatementIterator, config: CT
+ ) -> Optional[BT]:
+ """Try consume the block.
+
+ Possible outcomes:
+ 1. The opening was not matched, return None.
+ 2. A subclass of Block, where body and closing migh contain errors.
+ """
+ opening = cls.consume_opening(statement_iterator, config)
+ if opening is None:
+ return None
+
+ return cls.consume_body_closing(opening, statement_iterator, config)
+
+ @classmethod
+ def on_stop_iteration(cls, config):
+ return UnexpectedEOF()
+
+
+@dataclass(frozen=True)
+class BOS(ParsedStatement[CT]):
+ """Beginning of source."""
+
+ # Hasher algorithm name and hexdigest
+ content_hash: Hash
+
+ @classmethod
+ def from_string_and_config(cls: Type[PST], s: str, config: CT) -> FromString[PST]:
+ raise RuntimeError("BOS cannot be constructed from_string_and_config")
+
+ @property
+ def location(self) -> SourceLocationT:
+ return "<undefined>"
+
+
+@dataclass(frozen=True)
+class BOF(BOS):
+ """Beginning of file."""
+
+ path: pathlib.Path
+
+ # Modification time of the file.
+ mtime: float
+
+ @property
+ def location(self) -> SourceLocationT:
+ return self.path
+
+
+@dataclass(frozen=True)
+class BOR(BOS):
+ """Beginning of resource."""
+
+ package: str
+ resource_name: str
+
+ @property
+ def location(self) -> SourceLocationT:
+ return self.package, self.resource_name
+
+
+@dataclass(frozen=True)
+class EOS(ParsedStatement[CT]):
+ """End of sequence."""
+
+ @classmethod
+ def from_string_and_config(cls: Type[PST], s: str, config: CT) -> FromString[PST]:
+ return cls()
+
+
+class RootBlock(ty.Generic[IPST, CT], Block[BOS, IPST, EOS, CT]):
+ """A sequence of statement flanked by the beginning and ending of stream."""
+
+ opening: Single[BOS]
+ closing: Single[EOS]
+
+ @classmethod
+ def subclass_with(cls, *, body=None):
+ @dataclass(frozen=True)
+ class CustomRootBlock(RootBlock):
+ pass
+
+ if body:
+ CustomRootBlock.__annotations__["body"] = Multi[ty.Union[body]]
+
+ return CustomRootBlock
+
+ @classmethod
+ def consume_opening(
+ cls: Type[RBT], statement_iterator: StatementIterator, config: CT
+ ) -> NullableConsume[BOS]:
+ raise RuntimeError(
+ "Implementation error, 'RootBlock.consume_opening' should never be called"
+ )
+
+ @classmethod
+ def consume(
+ cls: Type[RBT], statement_iterator: StatementIterator, config: CT
+ ) -> RBT:
+ block = super().consume(statement_iterator, config)
+ if block is None:
+ raise RuntimeError(
+ "Implementation error, 'RootBlock.consume' should never return None"
+ )
+ return block
+
+ @classmethod
+ def consume_closing(
+ cls: Type[RBT], statement_iterator: StatementIterator, config: CT
+ ) -> NullableConsume[EOS]:
+ return None
+
+ @classmethod
+ def on_stop_iteration(cls, config):
+ return EOS()
+
+
+#################
+# Source parsing
+#################
+
+ResourceT = ty.Tuple[str, str] # package name, resource name
+StrictLocationT = ty.Union[pathlib.Path, ResourceT]
+SourceLocationT = ty.Union[str, StrictLocationT]
+
+
+@dataclass(frozen=True)
+class ParsedSource(ty.Generic[RBT, CT]):
+
+ parsed_source: RBT
+
+ # Parser configuration.
+ config: CT
+
+ @property
+ def location(self) -> StrictLocationT:
+ return self.parsed_source.opening.location
+
+ @cached_property
+ def has_errors(self) -> bool:
+ return self.parsed_source.has_errors
+
+ def errors(self):
+ yield from self.parsed_source.errors
+
+
+@dataclass(frozen=True)
+class CannotParseResourceAsFile(Exception):
+ """The requested python package resource cannot be located as a file
+ in the file system.
+ """
+
+ package: str
+ resource_name: str
+
+
+class Parser(ty.Generic[RBT, CT]):
+ """Parser class."""
+
+ #: class to iterate through statements in a source unit.
+ _statement_iterator_class: Type[StatementIterator] = StatementIterator
+
+ #: Delimiters.
+ _delimiters: DelimiterDictT = SPLIT_EOL
+
+ _strip_spaces: bool = True
+
+ #: root block class containing statements and blocks can be parsed.
+ _root_block_class: Type[RBT]
+
+ #: source file text encoding.
+ _encoding = "utf-8"
+
+ #: configuration passed to from_string functions.
+ _config: CT
+
+ #: try to open resources as files.
+ _prefer_resource_as_file: bool
+
+ #: parser algorithm to us. Must be a callable member of hashlib
+ _hasher = hashlib.blake2b
+
+ def __init__(self, config: CT, prefer_resource_as_file=True):
+ self._config = config
+ self._prefer_resource_as_file = prefer_resource_as_file
+
+ def parse(self, source_location: SourceLocationT) -> ParsedSource[RBT, CT]:
+ """Parse a file into a ParsedSourceFile or ParsedResource.
+
+ Parameters
+ ----------
+ source_location:
+ if str or pathlib.Path is interpreted as a file.
+ if (str, str) is interpreted as (package, resource) using the resource python api.
+ """
+ if isinstance(source_location, tuple) and len(source_location) == 2:
+ if self._prefer_resource_as_file:
+ try:
+ return self.parse_resource_from_file(*source_location)
+ except CannotParseResourceAsFile:
+ pass
+ return self.parse_resource(*source_location)
+
+ if isinstance(source_location, str):
+ return self.parse_file(pathlib.Path(source_location))
+
+ if isinstance(source_location, pathlib.Path):
+ return self.parse_file(source_location)
+
+ raise TypeError(
+ f"Unknown type {type(source_location)}, "
+ "use str or pathlib.Path for files or "
+ "(package: str, resource_name: str) tuple "
+ "for a resource."
+ )
+
+ def parse_bytes(self, b: bytes, bos: BOS = None) -> ParsedSource[RBT, CT]:
+ if bos is None:
+ bos = BOS(Hash.from_bytes(self._hasher, b)).set_simple_position(0, 0, 0)
+
+ sic = self._statement_iterator_class(
+ b.decode(self._encoding), self._delimiters, self._strip_spaces
+ )
+
+ parsed = self._root_block_class.consume_body_closing(bos, sic, self._config)
+
+ return ParsedSource(
+ parsed,
+ self._config,
+ )
+
+ def parse_file(self, path: pathlib.Path) -> ParsedSource[RBT, CT]:
+ """Parse a file into a ParsedSourceFile.
+
+ Parameters
+ ----------
+ path
+ path of the file.
+ """
+ with path.open(mode="rb") as fi:
+ content = fi.read()
+
+ bos = BOF(
+ Hash.from_bytes(self._hasher, content), path, path.stat().st_mtime
+ ).set_simple_position(0, 0, 0)
+ return self.parse_bytes(content, bos)
+
+ def parse_resource_from_file(
+ self, package: str, resource_name: str
+ ) -> ParsedSource[RBT, CT]:
+ """Parse a resource into a ParsedSourceFile, opening as a file.
+
+ Parameters
+ ----------
+ package
+ package name where the resource is located.
+ resource_name
+ name of the resource
+ """
+ if sys.version_info < (3, 9):
+ # Remove when Python 3.8 is dropped
+ with resources.path(package, resource_name) as p:
+ path = p.resolve()
+ else:
+ with resources.as_file(
+ resources.files(package).joinpath(resource_name)
+ ) as p:
+ path = p.resolve()
+
+ if path.exists():
+ return self.parse_file(path)
+
+ raise CannotParseResourceAsFile(package, resource_name)
+
+ def parse_resource(self, package: str, resource_name: str) -> ParsedSource[RBT, CT]:
+ """Parse a resource into a ParsedResource.
+
+ Parameters
+ ----------
+ package
+ package name where the resource is located.
+ resource_name
+ name of the resource
+ """
+ if sys.version_info < (3, 9):
+ # Remove when Python 3.8 is dropped
+ with resources.open_binary(package, resource_name) as fi:
+ content = fi.read()
+ else:
+ with resources.files(package).joinpath(resource_name).open("rb") as fi:
+ content = fi.read()
+
+ bos = BOR(
+ Hash.from_bytes(self._hasher, content), package, resource_name
+ ).set_simple_position(0, 0, 0)
+
+ return self.parse_bytes(content, bos)
+
+
+##########
+# Project
+##########
+
+
+class IncludeStatement(ParsedStatement):
+ """ "Include statements allow to merge files."""
+
+ @property
+ def target(self) -> str:
+ raise NotImplementedError(
+ "IncludeStatement subclasses must implement target property."
+ )
+
+
+class ParsedProject(
+ ty.Dict[
+ ty.Optional[ty.Tuple[StrictLocationT, str]],
+ ParsedSource,
+ ]
+):
+ """Collection of files, independent or connected via IncludeStatement.
+
+ Keys are either an absolute pathname or a tuple package name, resource name.
+
+ None is the name of the root.
+
+ """
+
+ @cached_property
+ def has_errors(self) -> bool:
+ return any(el.has_errors for el in self.values())
+
+ def errors(self):
+ for el in self.values():
+ yield from el.errors()
+
+ def _iter_statements(self, items, seen, include_only_once):
+ """Iter all definitions in the order they appear,
+ going into the included files.
+ """
+ for source_location, parsed in items:
+ seen.add(source_location)
+ for parsed_statement in parsed.parsed_source:
+ if isinstance(parsed_statement, IncludeStatement):
+ location = parsed.location, parsed_statement.target
+ if location in seen and include_only_once:
+ raise ValueError(f"{location} was already included.")
+ yield from self._iter_statements(
+ ((location, self[location]),), seen, include_only_once
+ )
+ else:
+ yield parsed_statement
+
+ def iter_statements(self, include_only_once=True):
+ """Iter all definitions in the order they appear,
+ going into the included files.
+
+ Parameters
+ ----------
+ include_only_once
+ if true, each file cannot be included more than once.
+ """
+ yield from self._iter_statements([(None, self[None])], set(), include_only_once)
+
+ def _iter_blocks(self, items, seen, include_only_once):
+ """Iter all definitions in the order they appear,
+ going into the included files.
+ """
+ for source_location, parsed in items:
+ seen.add(source_location)
+ for parsed_statement in parsed.parsed_source.iter_blocks():
+ if isinstance(parsed_statement, IncludeStatement):
+ location = parsed.location, parsed_statement.target
+ if location in seen and include_only_once:
+ raise ValueError(f"{location} was already included.")
+ yield from self._iter_blocks(
+ ((location, self[location]),), seen, include_only_once
+ )
+ else:
+ yield parsed_statement
+
+ def iter_blocks(self, include_only_once=True):
+ """Iter all definitions in the order they appear,
+ going into the included files.
+
+ Parameters
+ ----------
+ include_only_once
+ if true, each file cannot be included more than once.
+ """
+ yield from self._iter_blocks([(None, self[None])], set(), include_only_once)
+
+
+def default_locator(source_location: StrictLocationT, target: str) -> StrictLocationT:
+ """Return a new location from current_location and target."""
+
+ if isinstance(source_location, pathlib.Path):
+ current_location = pathlib.Path(source_location).resolve()
+
+ if current_location.is_file():
+ current_path = current_location.parent
+ else:
+ current_path = current_location
+
+ target_path = pathlib.Path(target)
+ if target_path.is_absolute():
+ raise ValueError(
+ f"Cannot refer to absolute paths in import statements ({source_location}, {target})."
+ )
+
+ tmp = (current_path / target_path).resolve()
+ if not is_relative_to(tmp, current_path):
+ raise ValueError(
+ f"Cannot refer to locations above the current location ({source_location}, {target})"
+ )
+
+ return tmp.absolute()
+
+ elif isinstance(source_location, tuple) and len(source_location) == 2:
+ return source_location[0], target
+
+ raise TypeError(
+ f"Cannot handle type {type(source_location)}, "
+ "use str or pathlib.Path for files or "
+ "(package: str, resource_name: str) tuple "
+ "for a resource."
+ )
+
+
+DefinitionT = ty.Union[ty.Type[Block], ty.Type[ParsedStatement]]
+
+SpecT = ty.Union[
+ ty.Type[Parser],
+ DefinitionT,
+ ty.Iterable[DefinitionT],
+ ty.Type[RootBlock],
+]
+
+
+def build_parser_class(spec: SpecT, *, strip_spaces: bool = True, delimiters=None):
+ """Build a custom parser class.
+
+ Parameters
+ ----------
+ spec
+ specification of the content to parse. Can be one of the following things:
+ - Parser class.
+ - Block or ParsedStatement derived class.
+ - Iterable of Block or ParsedStatement derived class.
+ - RootBlock derived class.
+ strip_spaces : bool
+ if True, spaces will be stripped for each statement before calling
+ ``from_string_and_config``.
+ delimiters : dict
+ Specify how the source file is split into statements (See below).
+
+ Delimiters dictionary
+ ---------------------
+ The delimiters are specified with the keys of the delimiters dict.
+ The dict files can be used to further customize the iterator. Each
+ consist of a tuple of two elements:
+ 1. A value of the DelimiterMode to indicate what to do with the
+ delimiter string: skip it, attach keep it with previous or next string
+ 2. A boolean indicating if parsing should stop after fiSBT
+ encountering this delimiter.
+ """
+
+ if delimiters is None:
+ delimiters = SPLIT_EOL
+
+ if isinstance(spec, type) and issubclass(spec, Parser):
+ CustomParser = spec
+ else:
+ if isinstance(spec, (tuple, list)):
+
+ for el in spec:
+ if not issubclass(el, (Block, ParsedStatement)):
+ raise TypeError(
+ "Elements in root_block_class must be of type Block or ParsedStatement, "
+ f"not {el}"
+ )
+
+ @dataclass(frozen=True)
+ class CustomRootBlock(RootBlock):
+ pass
+
+ CustomRootBlock.__annotations__["body"] = Multi[ty.Union[spec]]
+
+ elif isinstance(spec, type) and issubclass(spec, RootBlock):
+
+ CustomRootBlock = spec
+
+ elif isinstance(spec, type) and issubclass(spec, (Block, ParsedStatement)):
+
+ @dataclass(frozen=True)
+ class CustomRootBlock(RootBlock):
+ pass
+
+ CustomRootBlock.__annotations__["body"] = Multi[spec]
+
+ else:
+ raise TypeError(
+ "`spec` must be of type RootBlock or tuple of type Block or ParsedStatement, "
+ f"not {type(spec)}"
+ )
+
+ class CustomParser(Parser):
+
+ _delimiters = delimiters
+ _root_block_class = CustomRootBlock
+ _strip_spaces = strip_spaces
+
+ return CustomParser
+
+
+def parse(
+ entry_point: SourceLocationT,
+ spec: SpecT,
+ config=None,
+ *,
+ strip_spaces: bool = True,
+ delimiters=None,
+ locator: ty.Callable[[StrictLocationT, str], StrictLocationT] = default_locator,
+ prefer_resource_as_file: bool = True,
+ **extra_parser_kwargs,
+) -> ParsedProject:
+ """Parse sources into a ParsedProject dictionary.
+
+ Parameters
+ ----------
+ entry_point
+ file or resource, given as (package_name, resource_name).
+ spec
+ specification of the content to parse. Can be one of the following things:
+ - Parser class.
+ - Block or ParsedStatement derived class.
+ - Iterable of Block or ParsedStatement derived class.
+ - RootBlock derived class.
+ config
+ a configuration object that will be passed to `from_string_and_config`
+ classmethod.
+ strip_spaces : bool
+ if True, spaces will be stripped for each statement before calling
+ ``from_string_and_config``.
+ delimiters : dict
+ Specify how the source file is split into statements (See below).
+ locator : Callable
+ function that takes the current location and a target of an IncludeStatement
+ and returns a new location.
+ prefer_resource_as_file : bool
+ if True, resources will try to be located in the filesystem if
+ available.
+ extra_parser_kwargs
+ extra keyword arguments to be given to the parser.
+
+ Delimiters dictionary
+ ---------------------
+ The delimiters are specified with the keys of the delimiters dict.
+ The dict files can be used to further customize the iterator. Each
+ consist of a tuple of two elements:
+ 1. A value of the DelimiterMode to indicate what to do with the
+ delimiter string: skip it, attach keep it with previous or next string
+ 2. A boolean indicating if parsing should stop after fiSBT
+ encountering this delimiter.
+ """
+
+ CustomParser = build_parser_class(
+ spec, strip_spaces=strip_spaces, delimiters=delimiters
+ )
+ parser = CustomParser(
+ config, prefer_resource_as_file=prefer_resource_as_file, **extra_parser_kwargs
+ )
+
+ pp = ParsedProject()
+
+ # : ty.List[Optional[ty.Union[LocatorT, str]], ...]
+ pending: ty.List[ty.Tuple[StrictLocationT, str]] = []
+ if isinstance(entry_point, (str, pathlib.Path)):
+ entry_point = pathlib.Path(entry_point)
+ if not entry_point.is_absolute():
+ entry_point = pathlib.Path.cwd() / entry_point
+
+ elif not (isinstance(entry_point, tuple) and len(entry_point) == 2):
+ raise TypeError(
+ f"Cannot handle type {type(entry_point)}, "
+ "use str or pathlib.Path for files or "
+ "(package: str, resource_name: str) tuple "
+ "for a resource."
+ )
+
+ pp[None] = parsed = parser.parse(entry_point)
+ pending.extend(
+ (parsed.location, el.target)
+ for el in parsed.parsed_source.filter_by(IncludeStatement)
+ )
+
+ while pending:
+ source_location, target = pending.pop(0)
+ pp[(source_location, target)] = parsed = parser.parse(
+ locator(source_location, target)
+ )
+ pending.extend(
+ (parsed.location, el.target)
+ for el in parsed.parsed_source.filter_by(IncludeStatement)
+ )
+
+ return pp
+
+
+def parse_bytes(
+ content: bytes,
+ spec: SpecT,
+ config=None,
+ *,
+ strip_spaces: bool = True,
+ delimiters=None,
+ **extra_parser_kwargs,
+) -> ParsedProject:
+ """Parse sources into a ParsedProject dictionary.
+
+ Parameters
+ ----------
+ content
+ bytes.
+ spec
+ specification of the content to parse. Can be one of the following things:
+ - Parser class.
+ - Block or ParsedStatement derived class.
+ - Iterable of Block or ParsedStatement derived class.
+ - RootBlock derived class.
+ config
+ a configuration object that will be passed to `from_string_and_config`
+ classmethod.
+ strip_spaces : bool
+ if True, spaces will be stripped for each statement before calling
+ ``from_string_and_config``.
+ delimiters : dict
+ Specify how the source file is split into statements (See below).
+ """
+
+ CustomParser = build_parser_class(
+ spec, strip_spaces=strip_spaces, delimiters=delimiters
+ )
+ parser = CustomParser(config, prefer_resource_as_file=False, **extra_parser_kwargs)
+
+ pp = ParsedProject()
+
+ pp[None] = parsed = parser.parse_bytes(content)
+
+ if any(parsed.parsed_source.filter_by(IncludeStatement)):
+ raise ValueError("parse_bytes does not support using an IncludeStatement")
+
+ return pp
diff --git a/pint/compat.py b/pint/compat.py
index f5b03e3..4e0fba8 100644
--- a/pint/compat.py
+++ b/pint/compat.py
@@ -155,9 +155,9 @@ except ImportError:
# Pandas (Series)
try:
- from pandas import Series
+ from pandas import DataFrame, Series
- upcast_types.append(Series)
+ upcast_types += [DataFrame, Series]
except ImportError:
pass
diff --git a/pint/default_en.txt b/pint/default_en.txt
index a3d3a2a..95e2987 100644
--- a/pint/default_en.txt
+++ b/pint/default_en.txt
@@ -62,6 +62,8 @@
#### PREFIXES ####
# decimal prefixes
+quecto- = 1e-30 = q-
+ronto- = 1e-27 = r-
yocto- = 1e-24 = y-
zepto- = 1e-21 = z-
atto- = 1e-18 = a-
@@ -84,6 +86,8 @@ peta- = 1e15 = P-
exa- = 1e18 = E-
zetta- = 1e21 = Z-
yotta- = 1e24 = Y-
+ronna- = 1e27 = R-
+quetta- = 1e30 = Q-
# binary_prefixes
kibi- = 2**10 = Ki-
@@ -230,7 +234,8 @@ counts_per_second = count / second = cps
reciprocal_centimeter = 1 / cm = cm_1 = kayser
# Velocity
-[velocity] = [length] / [time] = [speed]
+[velocity] = [length] / [time]
+[speed] = [velocity]
knot = nautical_mile / hour = kt = knot_international = international_knot
mile_per_hour = mile / hour = mph = MPH
kilometer_per_hour = kilometer / hour = kph = KPH
@@ -443,17 +448,17 @@ farad = coulomb / volt = F
abfarad = 1e9 * farad = abF
conventional_farad_90 = R_K90 / R_K * farad = F_90
+# Magnetic flux
+[magnetic_flux] = [electric_potential] * [time]
+weber = volt * second = Wb
+unit_pole = µ_0 * biot * centimeter
+
# Inductance
[inductance] = [magnetic_flux] / [current]
henry = weber / ampere = H
abhenry = 1e-9 * henry = abH
conventional_henry_90 = R_K / R_K90 * henry = H_90
-# Magnetic flux
-[magnetic_flux] = [electric_potential] * [time]
-weber = volt * second = Wb
-unit_pole = µ_0 * biot * centimeter
-
# Magnetic field
[magnetic_field] = [magnetic_flux] / [area]
tesla = weber / meter ** 2 = T
diff --git a/pint/definitions.py b/pint/definitions.py
index 2459b4c..789d9e3 100644
--- a/pint/definitions.py
+++ b/pint/definitions.py
@@ -2,146 +2,27 @@
pint.definitions
~~~~~~~~~~~~~~~~
- Functions and classes related to unit definitions.
+ Kept for backwards compatibility
- :copyright: 2016 by Pint Authors, see AUTHORS for more details.
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
"""
-from __future__ import annotations
+from . import errors
+from ._vendor import flexparser as fp
+from .delegates import ParserConfig, txt_defparser
-from dataclasses import dataclass
-from typing import Callable, Optional, Tuple, Union
-from .converters import Converter
-
-
-@dataclass(frozen=True)
-class PreprocessedDefinition:
- """Splits a definition into the constitutive parts.
-
- A definition is given as a string with equalities in a single line::
-
- ---------------> rhs
- a = b = c = d = e
- | | | -------> aliases (optional)
- | | |
- | | -----------> symbol (use "_" for no symbol)
- | |
- | ---------------> value
- |
- -------------------> name
- """
-
- name: str
- symbol: Optional[str]
- aliases: Tuple[str, ...]
- value: str
- rhs_parts: Tuple[str, ...]
-
- @classmethod
- def from_string(cls, definition: str) -> PreprocessedDefinition:
- name, definition = definition.split("=", 1)
- name = name.strip()
-
- rhs_parts = tuple(res.strip() for res in definition.split("="))
-
- value, aliases = rhs_parts[0], tuple([x for x in rhs_parts[1:] if x != ""])
- symbol, aliases = (aliases[0], aliases[1:]) if aliases else (None, aliases)
- if symbol == "_":
- symbol = None
- aliases = tuple([x for x in aliases if x != "_"])
-
- return cls(name, symbol, aliases, value, rhs_parts)
-
-
-@dataclass(frozen=True)
class Definition:
- """Base class for definitions.
-
- Parameters
- ----------
- name : str
- Canonical name of the unit/prefix/etc.
- defined_symbol : str or None
- A short name or symbol for the definition.
- aliases : iterable of str
- Other names for the unit/prefix/etc.
- converter : callable or Converter or None
- """
-
- name: str
- defined_symbol: Optional[str]
- aliases: Tuple[str, ...]
- converter: Optional[Union[Callable, Converter]]
-
- _subclasses = []
- _default_subclass = None
-
- def __init_subclass__(cls, **kwargs):
- if kwargs.pop("default", False):
- if cls._default_subclass is not None:
- raise ValueError("There is already a registered default definition.")
- Definition._default_subclass = cls
- super().__init_subclass__(**kwargs)
- cls._subclasses.append(cls)
-
- def __post_init__(self):
- if isinstance(self.converter, str):
- raise TypeError(
- "The converter parameter cannot be an instance of `str`. Use `from_string` method"
- )
-
- @property
- def is_multiplicative(self) -> bool:
- return self.converter.is_multiplicative
-
- @property
- def is_logarithmic(self) -> bool:
- return self.converter.is_logarithmic
-
- @classmethod
- def accept_to_parse(cls, preprocessed: PreprocessedDefinition):
- return False
+ """This is kept for backwards compatibility"""
@classmethod
- def from_string(
- cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
- ) -> Definition:
- """Parse a definition.
-
- Parameters
- ----------
- definition : str or PreprocessedDefinition
- non_int_type : type
-
- Returns
- -------
- Definition or subclass of Definition
- """
-
- if isinstance(definition, str):
- definition = PreprocessedDefinition.from_string(definition)
-
- for subclass in cls._subclasses:
- if subclass.accept_to_parse(definition):
- return subclass.from_string(definition, non_int_type)
-
- if cls._default_subclass is None:
- raise ValueError("No matching definition (and no default parser).")
-
- return cls._default_subclass.from_string(definition, non_int_type)
-
- @property
- def symbol(self) -> str:
- return self.defined_symbol or self.name
-
- @property
- def has_symbol(self) -> bool:
- return bool(self.defined_symbol)
-
- def add_aliases(self, *alias: str) -> None:
- raise Exception("Cannot add aliases, definitions are inmutable.")
-
- def __str__(self) -> str:
- return self.name
+ def from_string(cls, s: str, non_int_type=float):
+ cfg = ParserConfig(non_int_type)
+ parser = txt_defparser.DefParser(cfg, None)
+ pp = parser.parse_string(s)
+ for definition in parser.iter_parsed_project(pp):
+ if isinstance(definition, Exception):
+ raise errors.DefinitionSyntaxError(str(definition))
+ if not isinstance(definition, (fp.BOS, fp.BOF, fp.BOS)):
+ return definition
diff --git a/pint/delegates/__init__.py b/pint/delegates/__init__.py
new file mode 100644
index 0000000..363ef9c
--- /dev/null
+++ b/pint/delegates/__init__.py
@@ -0,0 +1,14 @@
+"""
+ pint.delegates
+ ~~~~~~~~~~~~~~
+
+ Defines methods and classes to handle autonomous tasks.
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from . import txt_defparser
+from .base_defparser import ParserConfig, build_disk_cache_class
+
+__all__ = [txt_defparser, ParserConfig, build_disk_cache_class]
diff --git a/pint/delegates/base_defparser.py b/pint/delegates/base_defparser.py
new file mode 100644
index 0000000..b2de999
--- /dev/null
+++ b/pint/delegates/base_defparser.py
@@ -0,0 +1,107 @@
+"""
+ pint.delegates.base_defparser
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Common class and function for all parsers.
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+import functools
+import itertools
+import numbers
+import pathlib
+import typing as ty
+from dataclasses import dataclass, field
+
+from pint import errors
+from pint.facets.plain.definitions import NotNumeric
+from pint.util import ParserHelper, UnitsContainer
+
+from .._vendor import flexcache as fc
+from .._vendor import flexparser as fp
+
+
+@dataclass(frozen=True)
+class ParserConfig:
+ """Configuration used by the parser."""
+
+ #: Indicates the output type of non integer numbers.
+ non_int_type: ty.Type[numbers.Number] = float
+
+ def to_scaled_units_container(self, s: str):
+ return ParserHelper.from_string(s, self.non_int_type)
+
+ def to_units_container(self, s: str):
+ v = self.to_scaled_units_container(s)
+ if v.scale != 1:
+ raise errors.UnexpectedScaleInContainer(str(v.scale))
+ return UnitsContainer(v)
+
+ def to_dimension_container(self, s: str):
+ v = self.to_units_container(s)
+ invalid = tuple(itertools.filterfalse(errors.is_valid_dimension_name, v.keys()))
+ if invalid:
+ raise errors.DefinitionSyntaxError(
+ f"Cannot build a dimension container with {', '.join(invalid)} that "
+ + errors.MSG_INVALID_DIMENSION_NAME
+ )
+ return v
+
+ def to_number(self, s: str) -> numbers.Number:
+ """Try parse a string into a number (without using eval).
+
+ The string can contain a number or a simple equation (3 + 4)
+
+ Raises
+ ------
+ _NotNumeric
+ If the string cannot be parsed as a number.
+ """
+ val = self.to_scaled_units_container(s)
+ if len(val):
+ raise NotNumeric(s)
+ return val.scale
+
+
+@functools.lru_cache()
+def build_disk_cache_class(non_int_type: type):
+ """Build disk cache class, taking into account the non_int_type."""
+
+ @dataclass(frozen=True)
+ class PintHeader(fc.InvalidateByExist, fc.NameByFields, fc.BasicPythonHeader):
+
+ from .. import __version__
+
+ pint_version: str = __version__
+ non_int_type: str = field(default_factory=lambda: non_int_type.__qualname__)
+
+ class PathHeader(fc.NameByFileContent, PintHeader):
+ pass
+
+ class ParsedProjecHeader(fc.NameByHashIter, PintHeader):
+ @classmethod
+ def from_parsed_project(cls, pp: fp.ParsedProject, reader_id):
+ tmp = []
+ for stmt in pp.iter_statements():
+ if isinstance(stmt, fp.BOS):
+ tmp.append(
+ stmt.content_hash.algorithm_name
+ + ":"
+ + stmt.content_hash.hexdigest
+ )
+
+ return cls(tuple(tmp), reader_id)
+
+ class PintDiskCache(fc.DiskCache):
+
+ _header_classes = {
+ pathlib.Path: PathHeader,
+ str: PathHeader.from_string,
+ fp.ParsedProject: ParsedProjecHeader.from_parsed_project,
+ }
+
+ return PintDiskCache
diff --git a/pint/delegates/txt_defparser/__init__.py b/pint/delegates/txt_defparser/__init__.py
new file mode 100644
index 0000000..5572ca1
--- /dev/null
+++ b/pint/delegates/txt_defparser/__init__.py
@@ -0,0 +1,14 @@
+"""
+ pint.delegates.txt_defparser
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Parser for the original textual Pint Definition file.
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+
+from .defparser import DefParser
+
+__all__ = [DefParser]
diff --git a/pint/delegates/txt_defparser/block.py b/pint/delegates/txt_defparser/block.py
new file mode 100644
index 0000000..20ebcba
--- /dev/null
+++ b/pint/delegates/txt_defparser/block.py
@@ -0,0 +1,45 @@
+"""
+ pint.delegates.txt_defparser.block
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Classes for Pint Blocks, which are defined by:
+
+ @<block name>
+ <content>
+ @end
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from ..._vendor import flexparser as fp
+
+
+@dataclass(frozen=True)
+class EndDirectiveBlock(fp.ParsedStatement):
+ """An EndDirectiveBlock is simply an "@end" statement."""
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[EndDirectiveBlock]:
+ if s == "@end":
+ return cls()
+ return None
+
+
+@dataclass(frozen=True)
+class DirectiveBlock(fp.Block):
+ """Directive blocks have beginning statement starting with a @ character.
+ and ending with a "@end" (captured using a EndDirectiveBlock).
+
+ Subclass this class for convenience.
+ """
+
+ closing: EndDirectiveBlock
+
+ def derive_definition(self):
+ pass
diff --git a/pint/delegates/txt_defparser/common.py b/pint/delegates/txt_defparser/common.py
new file mode 100644
index 0000000..961f111
--- /dev/null
+++ b/pint/delegates/txt_defparser/common.py
@@ -0,0 +1,58 @@
+"""
+ pint.delegates.txt_defparser.common
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Definitions for parsing an Import Statement
+
+ Also DefinitionSyntaxError
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+
+from ... import errors
+from ..._vendor import flexparser as fp
+
+
+@dataclass(frozen=True)
+class DefinitionSyntaxError(errors.DefinitionSyntaxError, fp.ParsingError):
+ """A syntax error was found in a definition. Combines:
+
+ DefinitionSyntaxError: which provides a message placeholder.
+ fp.ParsingError: which provides raw text, and start and end column and row
+
+ and an extra location attribute in which the filename or reseource is stored.
+ """
+
+ location: str = field(init=False, default="")
+
+ def __str__(self):
+ msg = (
+ self.msg + "\n " + (self.format_position or "") + " " + (self.raw or "")
+ )
+ if self.location:
+ msg += "\n " + self.location
+ return msg
+
+ def set_location(self, value):
+ super().__setattr__("location", value)
+
+
+@dataclass(frozen=True)
+class ImportDefinition(fp.IncludeStatement):
+
+ value: str
+
+ @property
+ def target(self):
+ return self.value
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[ImportDefinition]:
+ if s.startswith("@import"):
+ return ImportDefinition(s[len("@import") :].strip())
+ return None
diff --git a/pint/delegates/txt_defparser/context.py b/pint/delegates/txt_defparser/context.py
new file mode 100644
index 0000000..5c54b4c
--- /dev/null
+++ b/pint/delegates/txt_defparser/context.py
@@ -0,0 +1,196 @@
+"""
+ pint.delegates.txt_defparser.context
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Definitions for parsing Context and their related objects
+
+ Notices that some of the checks are done within the
+ format agnostic parent definition class.
+
+ See each one for a slighly longer description of the
+ syntax.
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+import numbers
+import re
+import typing as ty
+from dataclasses import dataclass
+from typing import Dict, Tuple
+
+from ..._vendor import flexparser as fp
+from ...facets.context import definitions
+from ..base_defparser import ParserConfig
+from . import block, common, plain
+
+
+@dataclass(frozen=True)
+class Relation(definitions.Relation):
+ @classmethod
+ def _from_string_and_context_sep(
+ cls, s: str, config: ParserConfig, separator: str
+ ) -> fp.FromString[Relation]:
+ if separator not in s:
+ return None
+ if ":" not in s:
+ return None
+
+ rel, eq = s.split(":")
+
+ parts = rel.split(separator)
+
+ src, dst = (config.to_dimension_container(s) for s in parts)
+
+ return cls(src, dst, eq.strip())
+
+
+@dataclass(frozen=True)
+class ForwardRelation(fp.ParsedStatement, definitions.ForwardRelation, Relation):
+ """A relation connecting a dimension to another via a transformation function.
+
+ <source dimension> -> <target dimension>: <transformation function>
+ """
+
+ @classmethod
+ def from_string_and_config(
+ cls, s: str, config: ParserConfig
+ ) -> fp.FromString[ForwardRelation]:
+ return super()._from_string_and_context_sep(s, config, "->")
+
+
+@dataclass(frozen=True)
+class BidirectionalRelation(
+ fp.ParsedStatement, definitions.BidirectionalRelation, Relation
+):
+ """A bidirectional relation connecting a dimension to another
+ via a simple transformation function.
+
+ <source dimension> <-> <target dimension>: <transformation function>
+
+ """
+
+ @classmethod
+ def from_string_and_config(
+ cls, s: str, config: ParserConfig
+ ) -> fp.FromString[BidirectionalRelation]:
+ return super()._from_string_and_context_sep(s, config, "<->")
+
+
+@dataclass(frozen=True)
+class BeginContext(fp.ParsedStatement):
+ """Being of a context directive.
+
+ @context[(defaults)] <canonical name> [= <alias>] [= <alias>]
+ """
+
+ _header_re = re.compile(
+ r"@context\s*(?P<defaults>\(.*\))?\s+(?P<name>\w+)\s*(=(?P<aliases>.*))*"
+ )
+
+ name: str
+ aliases: Tuple[str, ...]
+ defaults: Dict[str, numbers.Number]
+
+ @classmethod
+ def from_string_and_config(
+ cls, s: str, config: ParserConfig
+ ) -> fp.FromString[BeginContext]:
+ try:
+ r = cls._header_re.search(s)
+ if r is None:
+ return None
+ name = r.groupdict()["name"].strip()
+ aliases = r.groupdict()["aliases"]
+ if aliases:
+ aliases = tuple(a.strip() for a in r.groupdict()["aliases"].split("="))
+ else:
+ aliases = ()
+ defaults = r.groupdict()["defaults"]
+ except Exception as exc:
+ return common.DefinitionSyntaxError(
+ f"Could not parse the Context header '{s}': {exc}"
+ )
+
+ if defaults:
+ txt = defaults
+ try:
+ defaults = (part.split("=") for part in defaults.strip("()").split(","))
+ defaults = {str(k).strip(): config.to_number(v) for k, v in defaults}
+ except (ValueError, TypeError) as exc:
+ return common.DefinitionSyntaxError(
+ f"Could not parse Context definition defaults '{txt}' {exc}"
+ )
+ else:
+ defaults = {}
+
+ return cls(name, tuple(aliases), defaults)
+
+
+@dataclass(frozen=True)
+class ContextDefinition(block.DirectiveBlock):
+ """Definition of a Context
+
+ @context[(defaults)] <canonical name> [= <alias>] [= <alias>]
+ # units can be redefined within the context
+ <redefined unit> = <relation to another unit>
+
+ # can establish unidirectional relationships between dimensions
+ <dimension 1> -> <dimension 2>: <transformation function>
+
+ # can establish bidirectionl relationships between dimensions
+ <dimension 3> <-> <dimension 4>: <transformation function>
+ @end
+
+ See BeginContext, Equality, ForwardRelation, BidirectionalRelation and
+ Comment for more parsing related information.
+
+ Example::
+
+ @context(n=1) spectroscopy = sp
+ # n index of refraction of the medium.
+ [length] <-> [frequency]: speed_of_light / n / value
+ [frequency] -> [energy]: planck_constant * value
+ [energy] -> [frequency]: value / planck_constant
+ # allow wavenumber / kayser
+ [wavenumber] <-> [length]: 1 / value
+ @end
+ """
+
+ opening: fp.Single[BeginContext]
+ body: fp.Multi[
+ ty.Union[
+ plain.CommentDefinition,
+ BidirectionalRelation,
+ ForwardRelation,
+ plain.UnitDefinition,
+ ]
+ ]
+
+ def derive_definition(self):
+ return definitions.ContextDefinition(
+ self.name, self.aliases, self.defaults, self.relations, self.redefinitions
+ )
+
+ @property
+ def name(self):
+ return self.opening.name
+
+ @property
+ def aliases(self):
+ return self.opening.aliases
+
+ @property
+ def defaults(self):
+ return self.opening.defaults
+
+ @property
+ def relations(self):
+ return tuple(r for r in self.body if isinstance(r, Relation))
+
+ @property
+ def redefinitions(self):
+ return tuple(r for r in self.body if isinstance(r, plain.UnitDefinition))
diff --git a/pint/delegates/txt_defparser/defaults.py b/pint/delegates/txt_defparser/defaults.py
new file mode 100644
index 0000000..af6e31f
--- /dev/null
+++ b/pint/delegates/txt_defparser/defaults.py
@@ -0,0 +1,77 @@
+"""
+ pint.delegates.txt_defparser.defaults
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Definitions for parsing Default sections.
+
+ See each one for a slighly longer description of the
+ syntax.
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+import typing as ty
+from dataclasses import dataclass, fields
+
+from ..._vendor import flexparser as fp
+from ...facets.plain import definitions
+from . import block, plain
+
+
+@dataclass(frozen=True)
+class BeginDefaults(fp.ParsedStatement):
+ """Being of a defaults directive.
+
+ @defaults
+ """
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[BeginDefaults]:
+ if s.strip() == "@defaults":
+ return cls()
+ return None
+
+
+@dataclass(frozen=True)
+class DefaultsDefinition(block.DirectiveBlock):
+ """Directive to store values.
+
+ @defaults
+ system = mks
+ @end
+
+ See Equality and Comment for more parsing related information.
+ """
+
+ opening: fp.Single[BeginDefaults]
+ body: fp.Multi[
+ ty.Union[
+ plain.CommentDefinition,
+ plain.Equality,
+ ]
+ ]
+
+ @property
+ def _valid_fields(self):
+ return tuple(f.name for f in fields(definitions.DefaultsDefinition))
+
+ def derive_definition(self):
+ for definition in self.filter_by(plain.Equality):
+ if definition.lhs not in self._valid_fields:
+ raise ValueError(
+ f"`{definition.lhs}` is not a valid key "
+ f"for the default section. {self._valid_fields}"
+ )
+
+ return definitions.DefaultsDefinition(
+ *tuple(self.get_key(key) for key in self._valid_fields)
+ )
+
+ def get_key(self, key):
+ for stmt in self.body:
+ if isinstance(stmt, plain.Equality) and stmt.lhs == key:
+ return stmt.rhs
+ raise KeyError(key)
diff --git a/pint/delegates/txt_defparser/defparser.py b/pint/delegates/txt_defparser/defparser.py
new file mode 100644
index 0000000..6112690
--- /dev/null
+++ b/pint/delegates/txt_defparser/defparser.py
@@ -0,0 +1,120 @@
+from __future__ import annotations
+
+import pathlib
+import typing as ty
+
+from ..._vendor import flexcache as fc
+from ..._vendor import flexparser as fp
+from .. import base_defparser
+from . import block, common, context, defaults, group, plain, system
+
+
+class PintRootBlock(fp.RootBlock):
+
+ body: fp.Multi[
+ ty.Union[
+ plain.CommentDefinition,
+ common.ImportDefinition,
+ context.ContextDefinition,
+ defaults.DefaultsDefinition,
+ system.SystemDefinition,
+ group.GroupDefinition,
+ plain.AliasDefinition,
+ plain.DerivedDimensionDefinition,
+ plain.DimensionDefinition,
+ plain.PrefixDefinition,
+ plain.UnitDefinition,
+ ]
+ ]
+
+
+class HashTuple(tuple):
+ pass
+
+
+class _PintParser(fp.Parser):
+ """Parser for the original Pint definition file, with cache."""
+
+ _delimiters = {
+ "#": (
+ fp.DelimiterInclude.SPLIT_BEFORE,
+ fp.DelimiterAction.CAPTURE_NEXT_TIL_EOL,
+ ),
+ **fp.SPLIT_EOL,
+ }
+ _root_block_class = PintRootBlock
+ _strip_spaces = True
+
+ _diskcache: fc.DiskCache
+
+ def __init__(self, config: base_defparser.ParserConfig, *args, **kwargs):
+ self._diskcache = kwargs.pop("diskcache", None)
+ super().__init__(config, *args, **kwargs)
+
+ def parse_file(self, path: pathlib.Path) -> fp.ParsedSource:
+ if self._diskcache is None:
+ return super().parse_file(path)
+ content, basename = self._diskcache.load(path, super().parse_file)
+ return content
+
+
+class DefParser:
+
+ skip_classes = (fp.BOF, fp.BOR, fp.BOS, fp.EOS, plain.CommentDefinition)
+
+ def __init__(self, default_config, diskcache):
+ self._default_config = default_config
+ self._diskcache = diskcache
+
+ def iter_parsed_project(self, parsed_project: fp.ParsedProject):
+ last_location = None
+ for stmt in parsed_project.iter_blocks():
+ if isinstance(stmt, fp.BOF):
+ last_location = str(stmt.path)
+ elif isinstance(stmt, fp.BOR):
+ last_location = (
+ f"[package: {stmt.package}, resource: {stmt.resource_name}]"
+ )
+
+ if isinstance(stmt, self.skip_classes):
+ continue
+
+ if isinstance(stmt, common.DefinitionSyntaxError):
+ stmt.set_location(last_location)
+ raise stmt
+ elif isinstance(stmt, block.DirectiveBlock):
+ for exc in stmt.errors:
+ exc = common.DefinitionSyntaxError(str(exc))
+ exc.set_position(*stmt.get_position())
+ exc.set_raw(
+ (stmt.opening.raw or "") + " [...] " + (stmt.closing.raw or "")
+ )
+ exc.set_location(last_location)
+ raise exc
+
+ try:
+ yield stmt.derive_definition()
+ except Exception as exc:
+ exc = common.DefinitionSyntaxError(str(exc))
+ exc.set_position(*stmt.get_position())
+ exc.set_raw(stmt.opening.raw + " [...] " + stmt.closing.raw)
+ exc.set_location(last_location)
+ raise exc
+ else:
+ yield stmt
+
+ def parse_file(self, filename: pathlib.Path, cfg=None):
+ return fp.parse(
+ filename,
+ _PintParser,
+ cfg or self._default_config,
+ diskcache=self._diskcache,
+ )
+
+ def parse_string(self, content: str, cfg=None):
+ return fp.parse_bytes(
+ content.encode("utf-8"),
+ _PintParser,
+ cfg or self._default_config,
+ diskcache=self._diskcache,
+ )
diff --git a/pint/delegates/txt_defparser/group.py b/pint/delegates/txt_defparser/group.py
new file mode 100644
index 0000000..5be42ac
--- /dev/null
+++ b/pint/delegates/txt_defparser/group.py
@@ -0,0 +1,106 @@
+"""
+ pint.delegates.txt_defparser.group
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Definitions for parsing Group and their related objects
+
+ Notices that some of the checks are done within the
+ format agnostic parent definition class.
+
+ See each one for a slighly longer description of the
+ syntax.
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+import re
+import typing as ty
+from dataclasses import dataclass
+
+from ..._vendor import flexparser as fp
+from ...facets.group import definitions
+from . import block, common, plain
+
+
+@dataclass(frozen=True)
+class BeginGroup(fp.ParsedStatement):
+ """Being of a group directive.
+
+ @group <name> [using <group 1>, ..., <group N>]
+ """
+
+ #: Regex to match the header parts of a definition.
+ _header_re = re.compile(r"@group\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*")
+
+ name: str
+ using_group_names: ty.Tuple[str, ...]
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[BeginGroup]:
+ if not s.startswith("@group"):
+ return None
+
+ r = cls._header_re.search(s)
+
+ if r is None:
+ return common.DefinitionSyntaxError(f"Invalid Group header syntax: '{s}'")
+
+ name = r.groupdict()["name"].strip()
+ groups = r.groupdict()["used_groups"]
+ if groups:
+ parent_group_names = tuple(a.strip() for a in groups.split(","))
+ else:
+ parent_group_names = ()
+
+ return cls(name, parent_group_names)
+
+
+@dataclass(frozen=True)
+class GroupDefinition(block.DirectiveBlock):
+ """Definition of a group.
+
+ @group <name> [using <group 1>, ..., <group N>]
+ <definition 1>
+ ...
+ <definition N>
+ @end
+
+ See UnitDefinition and Comment for more parsing related information.
+
+ Example::
+
+ @group AvoirdupoisUS using Avoirdupois
+ US_hundredweight = hundredweight = US_cwt
+ US_ton = ton
+ US_force_ton = force_ton = _ = US_ton_force
+ @end
+
+ """
+
+ opening: fp.Single[BeginGroup]
+ body: fp.Multi[
+ ty.Union[
+ plain.CommentDefinition,
+ plain.UnitDefinition,
+ ]
+ ]
+
+ def derive_definition(self):
+ return definitions.GroupDefinition(
+ self.name, self.using_group_names, self.definitions
+ )
+
+ @property
+ def name(self):
+ return self.opening.name
+
+ @property
+ def using_group_names(self):
+ return self.opening.using_group_names
+
+ @property
+ def definitions(self) -> ty.Tuple[plain.UnitDefinition, ...]:
+ return tuple(el for el in self.body if isinstance(el, plain.UnitDefinition))
diff --git a/pint/delegates/txt_defparser/plain.py b/pint/delegates/txt_defparser/plain.py
new file mode 100644
index 0000000..428df10
--- /dev/null
+++ b/pint/delegates/txt_defparser/plain.py
@@ -0,0 +1,283 @@
+"""
+ pint.delegates.txt_defparser.plain
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ Definitions for parsing:
+ - Equality
+ - CommentDefinition
+ - PrefixDefinition
+ - UnitDefinition
+ - DimensionDefinition
+ - DerivedDimensionDefinition
+ - AliasDefinition
+
+ Notices that some of the checks are done within the
+ format agnostic parent definition class.
+
+ See each one for a slighly longer description of the
+ syntax.
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+
+from ..._vendor import flexparser as fp
+from ...converters import Converter
+from ...facets.plain import definitions
+from ...util import UnitsContainer
+from ..base_defparser import ParserConfig
+from . import common
+
+
+@dataclass(frozen=True)
+class Equality(fp.ParsedStatement, definitions.Equality):
+ """An equality statement contains a left and right hand separated
+
+ lhs and rhs should be space stripped.
+ """
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[Equality]:
+ if "=" not in s:
+ return None
+ parts = [p.strip() for p in s.split("=")]
+ if len(parts) != 2:
+ return common.DefinitionSyntaxError(
+ f"Exactly two terms expected, not {len(parts)} (`{s}`)"
+ )
+ return cls(*parts)
+
+
+@dataclass(frozen=True)
+class CommentDefinition(fp.ParsedStatement, definitions.CommentDefinition):
+ """Comments start with a # character.
+
+ # This is a comment.
+ ## This is also a comment.
+
+ Captured value does not include the leading # character and space stripped.
+ """
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[fp.ParsedStatement]:
+ if not s.startswith("#"):
+ return None
+ return cls(s[1:].strip())
+
+
+@dataclass(frozen=True)
+class PrefixDefinition(fp.ParsedStatement, definitions.PrefixDefinition):
+ """Definition of a prefix::
+
+ <prefix>- = <value> [= <symbol>] [= <alias>] [ = <alias> ] [...]
+
+ Example::
+
+ deca- = 1e+1 = da- = deka-
+ """
+
+ @classmethod
+ def from_string_and_config(
+ cls, s: str, config: ParserConfig
+ ) -> fp.FromString[PrefixDefinition]:
+ if "=" not in s:
+ return None
+
+ name, value, *aliases = s.split("=")
+
+ name = name.strip()
+ if not name.endswith("-"):
+ return None
+
+ name = name.rstrip("-")
+ aliases = tuple(alias.strip().rstrip("-") for alias in aliases)
+
+ defined_symbol = None
+ if aliases:
+ if aliases[0] == "_":
+ aliases = aliases[1:]
+ else:
+ defined_symbol, *aliases = aliases
+
+ aliases = tuple(alias for alias in aliases if alias not in ("", "_"))
+
+ try:
+ value = config.to_number(value)
+ except definitions.NotNumeric as ex:
+ return common.DefinitionSyntaxError(
+ f"Prefix definition ('{name}') must contain only numbers, not {ex.value}"
+ )
+
+ try:
+ return cls(name, value, defined_symbol, aliases)
+ except Exception as exc:
+ return common.DefinitionSyntaxError(str(exc))
+
+
+@dataclass(frozen=True)
+class UnitDefinition(fp.ParsedStatement, definitions.UnitDefinition):
+ """Definition of a unit::
+
+ <canonical name> = <relation to another unit or dimension> [= <symbol>] [= <alias>] [ = <alias> ] [...]
+
+ Example::
+
+ millennium = 1e3 * year = _ = millennia
+
+ Parameters
+ ----------
+ reference : UnitsContainer
+ Reference units.
+ is_base : bool
+ Indicates if it is a base unit.
+
+ """
+
+ @classmethod
+ def from_string_and_config(
+ cls, s: str, config: ParserConfig
+ ) -> fp.FromString[UnitDefinition]:
+ if "=" not in s:
+ return None
+
+ name, value, *aliases = (p.strip() for p in s.split("="))
+
+ defined_symbol = None
+ if aliases:
+ if aliases[0] == "_":
+ aliases = aliases[1:]
+ else:
+ defined_symbol, *aliases = aliases
+
+ aliases = tuple(alias for alias in aliases if alias not in ("", "_"))
+
+ if ";" in value:
+ [converter, modifiers] = value.split(";", 1)
+
+ try:
+ modifiers = dict(
+ (key.strip(), config.to_number(value))
+ for key, value in (part.split(":") for part in modifiers.split(";"))
+ )
+ except definitions.NotNumeric as ex:
+ return common.DefinitionSyntaxError(
+ f"Unit definition ('{name}') must contain only numbers in modifier, not {ex.value}"
+ )
+
+ else:
+ converter = value
+ modifiers = {}
+
+ converter = config.to_scaled_units_container(converter)
+
+ try:
+ reference = UnitsContainer(converter)
+ # reference = converter.to_units_container()
+ except common.DefinitionSyntaxError as ex:
+ return common.DefinitionSyntaxError(f"While defining {name}: {ex}")
+
+ try:
+ converter = Converter.from_arguments(scale=converter.scale, **modifiers)
+ except Exception as ex:
+ return common.DefinitionSyntaxError(
+ f"Unable to assign a converter to the unit {ex}"
+ )
+
+ try:
+ return cls(name, defined_symbol, tuple(aliases), converter, reference)
+ except Exception as ex:
+ return common.DefinitionSyntaxError(str(ex))
+
+
+@dataclass(frozen=True)
+class DimensionDefinition(fp.ParsedStatement, definitions.DimensionDefinition):
+ """Definition of a root dimension::
+
+ [dimension name]
+
+ Example::
+
+ [volume]
+ """
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[DimensionDefinition]:
+ s = s.strip()
+
+ if not (s.startswith("[") and "=" not in s):
+ return None
+
+ try:
+ s = definitions.check_dim(s)
+ except common.DefinitionSyntaxError as ex:
+ return ex
+
+ return cls(s)
+
+
+@dataclass(frozen=True)
+class DerivedDimensionDefinition(
+ fp.ParsedStatement, definitions.DerivedDimensionDefinition
+):
+ """Definition of a derived dimension::
+
+ [dimension name] = <relation to other dimensions>
+
+ Example::
+
+ [density] = [mass] / [volume]
+ """
+
+ @classmethod
+ def from_string_and_config(
+ cls, s: str, config: ParserConfig
+ ) -> fp.FromString[DerivedDimensionDefinition]:
+ if not (s.startswith("[") and "=" in s):
+ return None
+
+ name, value, *aliases = s.split("=")
+
+ if aliases:
+ return common.DefinitionSyntaxError(
+ "Derived dimensions cannot have aliases."
+ )
+
+ try:
+ reference = config.to_dimension_container(value)
+ except common.DefinitionSyntaxError as exc:
+ return common.DefinitionSyntaxError(
+ f"In {name} derived dimensions must only be referenced "
+ f"to dimensions. {exc}"
+ )
+
+ try:
+ return cls(name.strip(), reference)
+ except Exception as exc:
+ return common.DefinitionSyntaxError(str(exc))
+
+
+@dataclass(frozen=True)
+class AliasDefinition(fp.ParsedStatement, definitions.AliasDefinition):
+ """Additional alias(es) for an already existing unit::
+
+ @alias <canonical name or previous alias> = <alias> [ = <alias> ] [...]
+
+ Example::
+
+ @alias meter = my_meter
+ """
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[AliasDefinition]:
+ if not s.startswith("@alias "):
+ return None
+ name, *aliases = s[len("@alias ") :].split("=")
+
+ try:
+ return cls(name.strip(), tuple(alias.strip() for alias in aliases))
+ except Exception as exc:
+ return common.DefinitionSyntaxError(str(exc))
diff --git a/pint/delegates/txt_defparser/system.py b/pint/delegates/txt_defparser/system.py
new file mode 100644
index 0000000..b21fd7a
--- /dev/null
+++ b/pint/delegates/txt_defparser/system.py
@@ -0,0 +1,110 @@
+"""
+ pint.delegates.txt_defparser.system
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ :copyright: 2022 by Pint Authors, see AUTHORS for more details.
+ :license: BSD, see LICENSE for more details.
+"""
+
+from __future__ import annotations
+
+import re
+import typing as ty
+from dataclasses import dataclass
+
+from ..._vendor import flexparser as fp
+from ...facets.system import definitions
+from . import block, common, plain
+
+
+@dataclass(frozen=True)
+class BaseUnitRule(fp.ParsedStatement, definitions.BaseUnitRule):
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[BaseUnitRule]:
+ if ":" not in s:
+ return cls(s.strip())
+ parts = [p.strip() for p in s.split(":")]
+ if len(parts) != 2:
+ return common.DefinitionSyntaxError(
+ f"Exactly two terms expected for rule, not {len(parts)} (`{s}`)"
+ )
+ return cls(*parts)
+
+
+@dataclass(frozen=True)
+class BeginSystem(fp.ParsedStatement):
+ """Being of a system directive.
+
+ @system <name> [using <group 1>, ..., <group N>]
+ """
+
+ #: Regex to match the header parts of a context.
+ _header_re = re.compile(r"@system\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*")
+
+ name: str
+ using_group_names: ty.Tuple[str, ...]
+
+ @classmethod
+ def from_string(cls, s: str) -> fp.FromString[BeginSystem]:
+ if not s.startswith("@system"):
+ return None
+
+ r = cls._header_re.search(s)
+
+ if r is None:
+ raise ValueError("Invalid System header syntax '%s'" % s)
+
+ name = r.groupdict()["name"].strip()
+ groups = r.groupdict()["used_groups"]
+
+ # If the systems has no group, it automatically uses the root group.
+ if groups:
+ group_names = tuple(a.strip() for a in groups.split(","))
+ else:
+ group_names = ("root",)
+
+ return cls(name, group_names)
+
+
+@dataclass(frozen=True)
+class SystemDefinition(block.DirectiveBlock):
+ """Definition of a System:
+
+ @system <name> [using <group 1>, ..., <group N>]
+ <rule 1>
+ ...
+ <rule N>
+ @end
+
+ See Rule and Comment for more parsing related information.
+
+ The syntax for the rule is:
+
+ new_unit_name : old_unit_name
+
+ where:
+ - old_unit_name: a root unit part which is going to be removed from the system.
+ - new_unit_name: a non root unit which is going to replace the old_unit.
+
+ If the new_unit_name and the old_unit_name, the later and the colon can be omitted.
+ """
+
+ opening: fp.Single[BeginSystem]
+ body: fp.Multi[ty.Union[plain.CommentDefinition, BaseUnitRule]]
+
+ def derive_definition(self):
+ return definitions.SystemDefinition(
+ self.name, self.using_group_names, self.rules
+ )
+
+ @property
+ def name(self):
+ return self.opening.name
+
+ @property
+ def using_group_names(self):
+ return self.opening.using_group_names
+
+ @property
+ def rules(self):
+ return tuple(el for el in self.body if isinstance(el, BaseUnitRule))
diff --git a/pint/errors.py b/pint/errors.py
index 22b55f0..0cd3590 100644
--- a/pint/errors.py
+++ b/pint/errors.py
@@ -10,91 +10,151 @@
from __future__ import annotations
+import typing as ty
+from dataclasses import dataclass, fields
+
OFFSET_ERROR_DOCS_HTML = "https://pint.readthedocs.io/en/latest/nonmult.html"
LOG_ERROR_DOCS_HTML = "https://pint.readthedocs.io/en/latest/nonmult.html"
+MSG_INVALID_UNIT_NAME = "is not a valid unit name (must follow Python identifier rules)"
+MSG_INVALID_UNIT_SYMBOL = "is not a valid unit symbol (must not contain spaces)"
+MSG_INVALID_UNIT_ALIAS = "is not a valid unit alias (must not contain spaces)"
+
+MSG_INVALID_PREFIX_NAME = (
+ "is not a valid prefix name (must follow Python identifier rules)"
+)
+MSG_INVALID_PREFIX_SYMBOL = "is not a valid prefix symbol (must not contain spaces)"
+MSG_INVALID_PREFIX_ALIAS = "is not a valid prefix alias (must not contain spaces)"
+
+MSG_INVALID_DIMENSION_NAME = "is not a valid dimension name (must follow Python identifier rules and enclosed by square brackets)"
+MSG_INVALID_CONTEXT_NAME = (
+ "is not a valid context name (must follow Python identifier rules)"
+)
+MSG_INVALID_GROUP_NAME = "is not a valid group name (must not contain spaces)"
+MSG_INVALID_SYSTEM_NAME = (
+ "is not a valid system name (must follow Python identifier rules)"
+)
+
+
+def is_dim(name):
+ return name[0] == "[" and name[-1] == "]"
+
+
+def is_valid_prefix_name(name):
+ return str.isidentifier(name) or name == ""
+
+
+is_valid_unit_name = is_valid_system_name = is_valid_context_name = str.isidentifier
+
+
+def _no_space(name):
+ return name.strip() == name and " " not in name
-def _file_prefix(filename=None, lineno=None):
- if filename and lineno is not None:
- return f"While opening {filename}, in line {lineno}: "
- elif filename:
- return f"While opening {filename}: "
- elif lineno is not None:
- return f"In line {lineno}: "
- else:
- return ""
+is_valid_group_name = _no_space
+is_valid_unit_alias = (
+ is_valid_prefix_alias
+) = is_valid_unit_symbol = is_valid_prefix_symbol = _no_space
+
+
+def is_valid_dimension_name(name):
+ return name == "[]" or (
+ len(name) > 1 and is_dim(name) and str.isidentifier(name[1:-1])
+ )
+
+
+class WithDefErr:
+ """Mixing class to make some classes more readable."""
+
+ def def_err(self, msg):
+ return DefinitionError(self.name, self.__class__.__name__, msg)
+
+
+@dataclass(frozen=False)
class PintError(Exception):
"""Base exception for all Pint errors."""
-class DefinitionSyntaxError(SyntaxError, PintError):
- """Raised when a textual definition has a syntax error."""
+@dataclass(frozen=False)
+class DefinitionError(ValueError, PintError):
+ """Raised when a definition is not properly constructed."""
- def __init__(self, msg, *, filename=None, lineno=None):
- super().__init__(msg)
- self.filename = filename
- self.lineno = lineno
+ name: str
+ definition_type: ty.Type
+ msg: str
def __str__(self):
- return _file_prefix(self.filename, self.lineno) + str(self.args[0])
+ msg = f"Cannot define '{self.name}' ({self.definition_type}): {self.msg}"
+ return msg
- @property
- def __dict__(self):
- # SyntaxError.filename and lineno are special fields that don't appear in
- # the __dict__. This messes up pickling and deepcopy, as well
- # as any other Python library that expects sane behaviour.
- return {"filename": self.filename, "lineno": self.lineno}
+ def __reduce__(self):
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
+
+
+@dataclass(frozen=False)
+class DefinitionSyntaxError(ValueError, PintError):
+ """Raised when a textual definition has a syntax error."""
+
+ msg: str
+
+ def __str__(self):
+ return self.msg
def __reduce__(self):
- return DefinitionSyntaxError, self.args, self.__dict__
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
+@dataclass(frozen=False)
class RedefinitionError(ValueError, PintError):
"""Raised when a unit or prefix is redefined."""
- def __init__(self, name, definition_type, *, filename=None, lineno=None):
- super().__init__(name, definition_type)
- self.filename = filename
- self.lineno = lineno
+ name: str
+ definition_type: ty.Type
def __str__(self):
- msg = f"Cannot redefine '{self.args[0]}' ({self.args[1]})"
- return _file_prefix(self.filename, self.lineno) + msg
+ msg = f"Cannot redefine '{self.name}' ({self.definition_type})"
+ return msg
def __reduce__(self):
- return RedefinitionError, self.args, self.__dict__
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
+@dataclass(frozen=False)
class UndefinedUnitError(AttributeError, PintError):
"""Raised when the units are not defined in the unit registry."""
- def __init__(self, *unit_names):
- if len(unit_names) == 1 and not isinstance(unit_names[0], str):
- unit_names = unit_names[0]
- super().__init__(*unit_names)
+ unit_names: ty.Union[str, ty.Tuple[str, ...]]
def __str__(self):
- if len(self.args) == 1:
- return f"'{self.args[0]}' is not defined in the unit registry"
- return f"{self.args} are not defined in the unit registry"
+ if isinstance(self.unit_names, str):
+ return f"'{self.unit_names}' is not defined in the unit registry"
+ if (
+ isinstance(self.unit_names, (tuple, list, set))
+ and len(self.unit_names) == 1
+ ):
+ return f"'{tuple(self.unit_names)[0]}' is not defined in the unit registry"
+ return f"{tuple(self.unit_names)} are not defined in the unit registry"
+
+ def __reduce__(self):
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
+@dataclass(frozen=False)
class PintTypeError(TypeError, PintError):
- pass
+ def __reduce__(self):
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
+@dataclass(frozen=False)
class DimensionalityError(PintTypeError):
"""Raised when trying to convert between incompatible units."""
- def __init__(self, units1, units2, dim1="", dim2="", *, extra_msg=""):
- super().__init__()
- self.units1 = units1
- self.units2 = units2
- self.dim1 = dim1
- self.dim2 = dim2
- self.extra_msg = extra_msg
+ units1: ty.Any
+ units2: ty.Any
+ dim1: str = ""
+ dim2: str = ""
+ extra_msg: str = ""
def __str__(self):
if self.dim1 or self.dim2:
@@ -110,34 +170,69 @@ class DimensionalityError(PintTypeError):
)
def __reduce__(self):
- return TypeError.__new__, (DimensionalityError,), self.__dict__
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
+@dataclass(frozen=False)
class OffsetUnitCalculusError(PintTypeError):
"""Raised on ambiguous operations with offset units."""
+ units1: ty.Any
+ units2: ty.Optional[ty.Any] = None
+
+ def yield_units(self):
+ yield self.units1
+ if self.units2:
+ yield self.units2
+
def __str__(self):
return (
"Ambiguous operation with offset unit (%s)."
- % ", ".join(str(u) for u in self.args)
+ % ", ".join(str(u) for u in self.yield_units())
+ " See "
+ OFFSET_ERROR_DOCS_HTML
+ " for guidance."
)
+ def __reduce__(self):
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
+
+@dataclass(frozen=False)
class LogarithmicUnitCalculusError(PintTypeError):
"""Raised on inappropriate operations with logarithmic units."""
+ units1: ty.Any
+ units2: ty.Optional[ty.Any] = None
+
+ def yield_units(self):
+ yield self.units1
+ if self.units2:
+ yield self.units2
+
def __str__(self):
return (
"Ambiguous operation with logarithmic unit (%s)."
- % ", ".join(str(u) for u in self.args)
+ % ", ".join(str(u) for u in self.yield_units())
+ " See "
+ LOG_ERROR_DOCS_HTML
+ " for guidance."
)
+ def __reduce__(self):
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
+
+@dataclass(frozen=False)
class UnitStrippedWarning(UserWarning, PintError):
- pass
+
+ msg: str
+
+ def __reduce__(self):
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
+
+
+@dataclass(frozen=False)
+class UnexpectedScaleInContainer(Exception):
+ def __reduce__(self):
+ return self.__class__, tuple(getattr(self, f.name) for f in fields(self))
diff --git a/pint/facets/__init__.py b/pint/facets/__init__.py
index a3f2439..d669b9f 100644
--- a/pint/facets/__init__.py
+++ b/pint/facets/__init__.py
@@ -7,8 +7,8 @@
keeping each part small enough to be hackable.
Each facet contains one or more of the following modules:
- - definitions: classes and functions to parse a string into an specific object.
- These objects must be immutable and pickable. (e.g. ContextDefinition)
+ - definitions: classes describing an specific unit related definiton.
+ These objects must be immutable, pickable and not reference the registry (e.g. ContextDefinition)
- objects: classes and functions that encapsulate behavior (e.g. Context)
- registry: implements a subclass of PlainRegistry or class that can be
mixed with it (e.g. ContextRegistry)
@@ -16,8 +16,7 @@
In certain cases, some of these modules might be collapsed into a single one
as the code is very short (like in dask) or expanded as the code is too long
(like in plain, where quantity and unit object are in their own module).
- Additionally, certain facets might not have one of them (e.g. dask adds no
- feature in relation to parsing).
+ Additionally, certain facets might not have one of them.
An important part of this scheme is that each facet should export only a few
classes in the __init__.py and everything else should not be accessed by any
diff --git a/pint/facets/context/definitions.py b/pint/facets/context/definitions.py
index 96f1e90..a24977b 100644
--- a/pint/facets/context/definitions.py
+++ b/pint/facets/context/definitions.py
@@ -8,182 +8,149 @@
from __future__ import annotations
+import itertools
import numbers
import re
from dataclasses import dataclass
-from typing import TYPE_CHECKING, Any, Callable, Dict, Tuple
+from typing import TYPE_CHECKING, Any, Callable, Dict, Set, Tuple
-from ...definitions import Definition
-from ...errors import DefinitionSyntaxError
-from ...util import ParserHelper, SourceIterator
+from ... import errors
from ..plain import UnitDefinition
if TYPE_CHECKING:
- from pint import Quantity
+ from pint import Quantity, UnitsContainer
-_header_re = re.compile(
- r"@context\s*(?P<defaults>\(.*\))?\s+(?P<name>\w+)\s*(=(?P<aliases>.*))*"
-)
-_varname_re = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
-# TODO: Put back annotation when possible
-# registry_cache: "UnitRegistry"
+@dataclass(frozen=True)
+class Relation:
+ """Base class for a relation between different dimensionalities."""
+
+ _varname_re = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
+
+ #: Source dimensionality
+ src: UnitsContainer
+ #: Destination dimensionality
+ dst: UnitsContainer
+ #: Equation connecting both dimensionalities from which the tranformation
+ #: will be built.
+ equation: str
+
+ # Instead of defining __post_init__ here,
+ # it will be added to the container class
+ # so that the name and a meaningfull class
+ # could be used.
+
+ @property
+ def variables(self) -> Set[str, ...]:
+ """Find all variables names in the equation."""
+ return set(self._varname_re.findall(self.equation))
+
+ @property
+ def transformation(self) -> Callable[..., Quantity[Any]]:
+ """Return a transformation callable that uses the registry
+ to parse the transformation equation.
+ """
+ return lambda ureg, value, **kwargs: ureg.parse_expression(
+ self.equation, value=value, **kwargs
+ )
+ @property
+ def bidirectional(self):
+ raise NotImplementedError
+
+
+@dataclass(frozen=True)
+class ForwardRelation(Relation):
+ """A relation connecting a dimension to another via a transformation function.
-class Expression:
- def __init__(self, eq):
- self._eq = eq
+ <source dimension> -> <target dimension>: <transformation function>
+ """
- def __call__(self, ureg, value: Any, **kwargs: Any):
- return ureg.parse_expression(self._eq, value=value, **kwargs)
+ @property
+ def bidirectional(self):
+ return False
@dataclass(frozen=True)
-class Relation:
+class BidirectionalRelation(Relation):
+ """A bidirectional relation connecting a dimension to another
+ via a simple transformation function.
+
+ <source dimension> <-> <target dimension>: <transformation function>
- bidirectional: True
- src: ParserHelper
- dst: ParserHelper
- tranformation: Callable[..., Quantity[Any]]
+ """
+
+ @property
+ def bidirectional(self):
+ return True
@dataclass(frozen=True)
-class ContextDefinition:
- """Definition of a Context
-
- @context[(defaults)] <canonical name> [= <alias>] [= <alias>]
- # units can be redefined within the context
- <redefined unit> = <relation to another unit>
-
- # can establish unidirectional relationships between dimensions
- <dimension 1> -> <dimension 2>: <transformation function>
-
- # can establish bidirectionl relationships between dimensions
- <dimension 3> <-> <dimension 4>: <transformation function>
- @end
-
- Example::
-
- @context(n=1) spectroscopy = sp
- # n index of refraction of the medium.
- [length] <-> [frequency]: speed_of_light / n / value
- [frequency] -> [energy]: planck_constant * value
- [energy] -> [frequency]: value / planck_constant
- # allow wavenumber / kayser
- [wavenumber] <-> [length]: 1 / value
- @end
- """
+class ContextDefinition(errors.WithDefErr):
+ """Definition of a Context"""
+ #: name of the context
name: str
+ #: other na
aliases: Tuple[str, ...]
- variables: Tuple[str, ...]
defaults: Dict[str, numbers.Number]
+ relations: Tuple[Relation, ...]
+ redefinitions: Tuple[UnitDefinition, ...]
- # Each element indicates: line number, is_bidirectional, src, dst, transformation func
- relations: Tuple[Tuple[int, Relation], ...]
- redefinitions: Tuple[Tuple[int, UnitDefinition], ...]
+ @property
+ def variables(self) -> Set[str, ...]:
+ """Return all variable names in all transformations."""
+ return set().union(*(r.variables for r in self.relations))
- @staticmethod
- def parse_definition(line, non_int_type) -> UnitDefinition:
- definition = Definition.from_string(line, non_int_type)
- if not isinstance(definition, UnitDefinition):
- raise DefinitionSyntaxError(
- "Expected <unit> = <converter>; got %s" % line.strip()
- )
- if definition.symbol != definition.name or definition.aliases:
- raise DefinitionSyntaxError(
- "Can't change a unit's symbol or aliases within a context"
+ @classmethod
+ def from_lines(cls, lines, non_int_type):
+ # TODO: this is to keep it backwards compatible
+ from ...delegates import ParserConfig, txt_defparser
+
+ cfg = ParserConfig(non_int_type)
+ parser = txt_defparser.DefParser(cfg, None)
+ pp = parser.parse_string("\n".join(lines) + "\n@end")
+ for definition in parser.iter_parsed_project(pp):
+ if isinstance(definition, cls):
+ return definition
+
+ def __post_init__(self):
+ if not errors.is_valid_context_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_GROUP_NAME)
+
+ for k in self.aliases:
+ if not errors.is_valid_context_name(k):
+ raise self.def_err(
+ f"refers to '{k}' that " + errors.MSG_INVALID_CONTEXT_NAME
+ )
+
+ for relation in self.relations:
+ invalid = tuple(
+ itertools.filterfalse(
+ errors.is_valid_dimension_name, relation.src.keys()
+ )
+ ) + tuple(
+ itertools.filterfalse(
+ errors.is_valid_dimension_name, relation.dst.keys()
+ )
)
- if definition.is_base:
- raise DefinitionSyntaxError("Can't define plain units within a context")
- return definition
- @classmethod
- def from_lines(cls, lines, non_int_type=float) -> ContextDefinition:
- lines = SourceIterator(lines)
-
- lineno, header = next(lines)
- try:
- r = _header_re.search(header)
- name = r.groupdict()["name"].strip()
- aliases = r.groupdict()["aliases"]
- if aliases:
- aliases = tuple(a.strip() for a in r.groupdict()["aliases"].split("="))
- else:
- aliases = ()
- defaults = r.groupdict()["defaults"]
- except Exception as exc:
- raise DefinitionSyntaxError(
- "Could not parse the Context header '%s'" % header, lineno=lineno
- ) from exc
-
- if defaults:
-
- def to_num(val):
- val = complex(val)
- if not val.imag:
- return val.real
- return val
-
- txt = defaults
- try:
- defaults = (part.split("=") for part in defaults.strip("()").split(","))
- defaults = {str(k).strip(): to_num(v) for k, v in defaults}
- except (ValueError, TypeError) as exc:
- raise DefinitionSyntaxError(
- f"Could not parse Context definition defaults: '{txt}'",
- lineno=lineno,
- ) from exc
- else:
- defaults = {}
-
- variables = set()
- redefitions = []
- relations = []
- for lineno, line in lines:
- try:
- if "=" in line:
- definition = cls.parse_definition(line, non_int_type)
- redefitions.append((lineno, definition))
- elif ":" in line:
- rel, eq = line.split(":")
- variables.update(_varname_re.findall(eq))
-
- func = Expression(eq)
-
- bidir = True
- parts = rel.split("<->")
- if len(parts) != 2:
- bidir = False
- parts = rel.split("->")
- if len(parts) != 2:
- raise Exception
-
- src, dst = (
- ParserHelper.from_string(s, non_int_type) for s in parts
- )
- relation = Relation(bidir, src, dst, func)
- relations.append((lineno, relation))
- else:
- raise Exception
- except Exception as exc:
- raise DefinitionSyntaxError(
- "Could not parse Context %s relation '%s': %s" % (name, line, exc),
- lineno=lineno,
- ) from exc
-
- if defaults:
- missing_pars = defaults.keys() - set(variables)
- if missing_pars:
- raise DefinitionSyntaxError(
- f"Context parameters {missing_pars} not found in any equation"
+ if invalid:
+ raise self.def_err(
+ f"relation refers to {', '.join(invalid)} that "
+ + errors.MSG_INVALID_DIMENSION_NAME
)
- return cls(
- name,
- aliases,
- tuple(variables),
- defaults,
- tuple(relations),
- tuple(redefitions),
- )
+ for definition in self.redefinitions:
+ if definition.symbol != definition.name or definition.aliases:
+ raise self.def_err(
+ "can't change a unit's symbol or aliases within a context"
+ )
+ if definition.is_base:
+ raise self.def_err("can't define plain units within a context")
+
+ missing_pars = set(self.defaults.keys()) - self.variables
+ if missing_pars:
+ raise self.def_err(
+ f"Context parameters {missing_pars} not found in any equation"
+ )
diff --git a/pint/facets/context/objects.py b/pint/facets/context/objects.py
index 0aca430..6f2307a 100644
--- a/pint/facets/context/objects.py
+++ b/pint/facets/context/objects.py
@@ -12,7 +12,6 @@ import weakref
from collections import ChainMap, defaultdict
from typing import Optional, Tuple
-from ...errors import DefinitionSyntaxError, RedefinitionError
from ...facets.plain import UnitDefinition
from ...util import UnitsContainer, to_units_container
from .definitions import ContextDefinition
@@ -133,29 +132,22 @@ class Context:
def from_definition(cls, cd: ContextDefinition, to_base_func=None) -> Context:
ctx = cls(cd.name, cd.aliases, cd.defaults)
- for lineno, definition in cd.redefinitions:
- try:
- ctx._redefine(definition)
- except (RedefinitionError, DefinitionSyntaxError) as ex:
- if ex.lineno is None:
- ex.lineno = lineno
- raise ex
+ for definition in cd.redefinitions:
+ ctx._redefine(definition)
- for lineno, relation in cd.relations:
+ for relation in cd.relations:
try:
if to_base_func:
src = to_base_func(relation.src)
dst = to_base_func(relation.dst)
else:
src, dst = relation.src, relation.dst
- ctx.add_transformation(src, dst, relation.tranformation)
+ ctx.add_transformation(src, dst, relation.transformation)
if relation.bidirectional:
- ctx.add_transformation(dst, src, relation.tranformation)
+ ctx.add_transformation(dst, src, relation.transformation)
except Exception as exc:
- raise DefinitionSyntaxError(
- "Could not add Context %s relation on line '%s'"
- % (cd.name, lineno),
- lineno=lineno,
+ raise ValueError(
+ f"Could not add Context {cd.name} relation {relation}"
) from exc
return ctx
@@ -192,11 +184,16 @@ class Context:
definition : str
<unit> = <new definition>``, e.g. ``pound = 0.5 kg``
"""
-
- for line in definition.splitlines():
- # TODO: What is the right non_int_type value.
- definition = ContextDefinition.parse_definition(line, float)
- self._redefine(definition)
+ from ...delegates import ParserConfig, txt_defparser
+
+ # TODO: kept for backwards compatibility.
+ # this is not a good idea as we have no way of known the correct non_int_type
+ cfg = ParserConfig(float)
+ parser = txt_defparser.DefParser(cfg, None)
+ pp = parser.parse_string(definition)
+ for definition in parser.iter_parsed_project(pp):
+ if isinstance(definition, UnitDefinition):
+ self._redefine(definition)
def _redefine(self, definition: UnitDefinition):
self.redefinitions.append(definition)
diff --git a/pint/facets/context/registry.py b/pint/facets/context/registry.py
index 4290e3e..2b56299 100644
--- a/pint/facets/context/registry.py
+++ b/pint/facets/context/registry.py
@@ -14,7 +14,7 @@ from contextlib import contextmanager
from typing import Any, Callable, ContextManager, Dict, Union
from ..._typing import F
-from ...errors import DefinitionSyntaxError, UndefinedUnitError
+from ...errors import UndefinedUnitError
from ...util import find_connected_nodes, find_shortest_path, logger
from ..plain import PlainRegistry, UnitDefinition
from .definitions import ContextDefinition
@@ -67,17 +67,11 @@ class ContextRegistry(PlainRegistry):
# Allow contexts to add override layers to the units
self._units = ChainMap(self._units)
- def _register_directives(self) -> None:
- super()._register_directives()
- self._register_directive("@context", self._load_context, ContextDefinition)
+ def _register_definition_adders(self) -> None:
+ super()._register_definition_adders()
+ self._register_adder(ContextDefinition, self.add_context)
- def _load_context(self, cd: ContextDefinition) -> None:
- try:
- self.add_context(Context.from_definition(cd, self.get_dimensionality))
- except KeyError as e:
- raise DefinitionSyntaxError(f"unknown dimension {e} in context")
-
- def add_context(self, context: Context) -> None:
+ def add_context(self, context: Union[Context, ContextDefinition]) -> None:
"""Add a context object to the registry.
The context will be accessible by its name and aliases.
@@ -85,6 +79,9 @@ class ContextRegistry(PlainRegistry):
Notice that this method will NOT enable the context;
see :meth:`enable_contexts`.
"""
+ if isinstance(context, ContextDefinition):
+ context = Context.from_definition(context, self.get_dimensionality)
+
if not context.name:
raise ValueError("Can't add unnamed context to registry")
if context.name in self._contexts:
@@ -189,7 +186,6 @@ class ContextRegistry(PlainRegistry):
name=basedef.name,
defined_symbol=basedef.symbol,
aliases=basedef.aliases,
- is_base=False,
reference=definition.reference,
converter=definition.converter,
)
diff --git a/pint/facets/dask/__init__.py b/pint/facets/dask/__init__.py
index 46fb38a..f99e8a2 100644
--- a/pint/facets/dask/__init__.py
+++ b/pint/facets/dask/__init__.py
@@ -1,6 +1,6 @@
"""
- pint.facets.numpy
- ~~~~~~~~~~~~~~~~~
+ pint.facets.dask
+ ~~~~~~~~~~~~~~~~
Adds pint the capability to interoperate with Dask
diff --git a/pint/facets/formatting/objects.py b/pint/facets/formatting/objects.py
index a32b41a..7435c37 100644
--- a/pint/facets/formatting/objects.py
+++ b/pint/facets/formatting/objects.py
@@ -21,8 +21,7 @@ from ...formatting import (
siunitx_format_unit,
split_format,
)
-from ...util import iterable
-from ..plain import UnitsContainer
+from ...util import UnitsContainer, iterable
class FormattingQuantity:
diff --git a/pint/facets/group/definitions.py b/pint/facets/group/definitions.py
index d2fa3f2..c0abced 100644
--- a/pint/facets/group/definitions.py
+++ b/pint/facets/group/definitions.py
@@ -1,6 +1,6 @@
"""
- pint.facets.group.defintions
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ pint.facets.group.definitions
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:copyright: 2022 by Pint Authors, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
@@ -8,88 +8,46 @@
from __future__ import annotations
-import re
+import typing as ty
from dataclasses import dataclass
-from typing import Tuple
-from ...definitions import Definition
-from ...errors import DefinitionSyntaxError
-from ...util import SourceIterator
-from ..plain import UnitDefinition
+from ... import errors
+from .. import plain
@dataclass(frozen=True)
-class GroupDefinition:
- """Definition of a group.
-
- @group <name> [using <group 1>, ..., <group N>]
- <definition 1>
- ...
- <definition N>
- @end
-
- Example::
-
- @group AvoirdupoisUS using Avoirdupois
- US_hundredweight = hundredweight = US_cwt
- US_ton = ton
- US_force_ton = force_ton = _ = US_ton_force
- @end
-
- """
-
- #: Regex to match the header parts of a definition.
- _header_re = re.compile(r"@group\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*")
+class GroupDefinition(errors.WithDefErr):
+ """Definition of a group."""
+ #: name of the group
name: str
- units: Tuple[Tuple[int, UnitDefinition], ...]
- using_group_names: Tuple[str, ...]
-
- @property
- def unit_names(self) -> Tuple[str, ...]:
- return tuple(u.name for lineno, u in self.units)
+ #: unit groups that will be included within the group
+ using_group_names: ty.Tuple[str, ...]
+ #: definitions for the units existing within the group
+ definitions: ty.Tuple[plain.UnitDefinition, ...]
@classmethod
- def from_lines(cls, lines, non_int_type=float):
- """Return a Group object parsing an iterable of lines.
+ def from_lines(cls, lines, non_int_type):
+ # TODO: this is to keep it backwards compatible
+ from ...delegates import ParserConfig, txt_defparser
- Parameters
- ----------
- lines : list[str]
- iterable
- define_func : callable
- Function to define a unit in the registry; it must accept a single string as
- a parameter.
+ cfg = ParserConfig(non_int_type)
+ parser = txt_defparser.DefParser(cfg, None)
+ pp = parser.parse_string("\n".join(lines) + "\n@end")
+ for definition in parser.iter_parsed_project(pp):
+ if isinstance(definition, cls):
+ return definition
- Returns
- -------
-
- """
-
- lines = SourceIterator(lines)
- lineno, header = next(lines)
-
- r = cls._header_re.search(header)
-
- if r is None:
- raise ValueError("Invalid Group header syntax: '%s'" % header)
+ @property
+ def unit_names(self) -> ty.Tuple[str, ...]:
+ return tuple(el.name for el in self.definitions)
- name = r.groupdict()["name"].strip()
- groups = r.groupdict()["used_groups"]
- if groups:
- parent_group_names = tuple(a.strip() for a in groups.split(","))
- else:
- parent_group_names = ()
+ def __post_init__(self):
+ if not errors.is_valid_group_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_GROUP_NAME)
- units = []
- for lineno, line in lines:
- definition = Definition.from_string(line, non_int_type=non_int_type)
- if not isinstance(definition, UnitDefinition):
- raise DefinitionSyntaxError(
- "Only UnitDefinition are valid inside _used_groups, not "
- + str(definition),
- lineno=lineno,
+ for k in self.using_group_names:
+ if not errors.is_valid_group_name(k):
+ raise self.def_err(
+ f"refers to '{k}' that " + errors.MSG_INVALID_GROUP_NAME
)
- units.append((lineno, definition))
-
- return cls(name, tuple(units), parent_group_names)
diff --git a/pint/facets/group/objects.py b/pint/facets/group/objects.py
index 4ff775c..67fa136 100644
--- a/pint/facets/group/objects.py
+++ b/pint/facets/group/objects.py
@@ -8,7 +8,6 @@
from __future__ import annotations
-from ...errors import DefinitionSyntaxError, RedefinitionError
from ...util import SharedRegistryObject, getattr_maybe_raise
from .definitions import GroupDefinition
@@ -169,19 +168,24 @@ class Group(SharedRegistryObject):
return cls.from_definition(group_definition, define_func)
@classmethod
- def from_definition(cls, group_definition: GroupDefinition, define_func) -> Group:
- for lineno, definition in group_definition.units:
- try:
- define_func(definition)
- except (RedefinitionError, DefinitionSyntaxError) as ex:
- if ex.lineno is None:
- ex.lineno = lineno
- raise ex
-
+ def from_definition(
+ cls, group_definition: GroupDefinition, add_unit_func=None
+ ) -> Group:
grp = cls(group_definition.name)
- grp.add_units(*(unit.name for lineno, unit in group_definition.units))
+ add_unit_func = add_unit_func or grp._REGISTRY._add_unit
+
+ # We first add all units defined within the group
+ # to the registry.
+ for definition in group_definition.definitions:
+ add_unit_func(definition)
+
+ # Then we add all units defined within the group
+ # to this group (by name)
+ grp.add_units(*group_definition.unit_names)
+ # Finally, we add all grou0ps used by this group
+ # tho this group (by name)
if group_definition.using_group_names:
grp.add_groups(*group_definition.using_group_names)
diff --git a/pint/facets/group/registry.py b/pint/facets/group/registry.py
index f8da191..c4ed0be 100644
--- a/pint/facets/group/registry.py
+++ b/pint/facets/group/registry.py
@@ -10,6 +10,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Dict, FrozenSet
+from ... import errors
+
if TYPE_CHECKING:
from pint import Unit
@@ -29,6 +31,9 @@ class GroupRegistry(PlainRegistry):
- Parse @group directive.
"""
+ # TODO: Change this to Group: Group to specify class
+ # and use introspection to get system class as a way
+ # to enjoy typing goodies
_group_class = Group
def __init__(self, **kwargs):
@@ -69,13 +74,25 @@ class GroupRegistry(PlainRegistry):
all_units = self.get_group("root", False).members
grp.add_units(*(all_units - group_units))
- def _register_directives(self) -> None:
- super()._register_directives()
- self._register_directive(
- "@group",
- lambda gd: self.Group.from_definition(gd, self.define),
- GroupDefinition,
- )
+ def _register_definition_adders(self) -> None:
+ super()._register_definition_adders()
+ self._register_adder(GroupDefinition, self._add_group)
+
+ def _add_unit(self, definition: UnitDefinition):
+ super()._add_unit(definition)
+ # TODO: delta units are missing
+ self.get_group("root").add_units(definition.name)
+
+ def _add_group(self, gd: GroupDefinition):
+
+ if gd.name in self._groups:
+ raise ValueError(f"Group {gd.name} already present in registry")
+ try:
+ # As a Group is a SharedRegistryObject
+ # it adds itself to the registry.
+ self.Group.from_definition(gd)
+ except KeyError as e:
+ raise errors.DefinitionSyntaxError(f"unknown dimension {e} in context")
def get_group(self, name: str, create_if_needed: bool = True) -> Group:
"""Return a Group.
@@ -101,19 +118,6 @@ class GroupRegistry(PlainRegistry):
return self.Group(name)
- def _define(self, definition):
-
- # In addition to the what is done by the PlainRegistry,
- # this adds all units to the `root` group.
-
- definition, d, di = super()._define(definition)
-
- if isinstance(definition, UnitDefinition):
- # We add all units to the root group
- self.get_group("root").add_units(definition.name)
-
- return definition, d, di
-
def _get_compatible_units(self, input_units, group) -> FrozenSet["Unit"]:
ret = super()._get_compatible_units(input_units, group)
diff --git a/pint/facets/measurement/registry.py b/pint/facets/measurement/registry.py
index 9f051d7..f5c9621 100644
--- a/pint/facets/measurement/registry.py
+++ b/pint/facets/measurement/registry.py
@@ -1,6 +1,6 @@
"""
- pint.facets.measurement.objects
- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ pint.facets.measurement.registry
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:copyright: 2022 by Pint Authors, see AUTHORS for more details.
:license: BSD, see LICENSE for more details.
diff --git a/pint/facets/nonmultiplicative/registry.py b/pint/facets/nonmultiplicative/registry.py
index ae6b1b0..fc71bc5 100644
--- a/pint/facets/nonmultiplicative/registry.py
+++ b/pint/facets/nonmultiplicative/registry.py
@@ -8,12 +8,12 @@
from __future__ import annotations
-from typing import Any, Optional, Union
+from typing import Any, Optional
-from ...definitions import Definition
from ...errors import DimensionalityError, UndefinedUnitError
-from ...util import UnitsContainer
-from ..plain import PlainRegistry
+from ...util import UnitsContainer, logger
+from ..plain import PlainRegistry, UnitDefinition
+from .definitions import OffsetConverter, ScaleConverter
from .objects import NonMultiplicativeQuantity
@@ -65,31 +65,44 @@ class NonMultiplicativeRegistry(PlainRegistry):
return super()._parse_units(input_string, as_delta, case_sensitive)
- def _define(self, definition: Union[str, Definition]):
- """Add unit to the registry.
+ def _add_unit(self, definition: UnitDefinition):
+ super()._add_unit(definition)
- In addition to what is done by the PlainRegistry,
- registers also non-multiplicative units.
-
- Parameters
- ----------
- definition : str or Definition
- A dimension, unit or prefix definition.
+ if definition.is_multiplicative:
+ return
- Returns
- -------
- Definition, dict, dict
- Definition instance, case sensitive unit dict, case insensitive unit dict.
+ if definition.is_logarithmic:
+ return
- """
-
- definition, d, di = super()._define(definition)
-
- # define additional units for units with an offset
- if getattr(definition.converter, "offset", 0) != 0:
- self._define_adder(definition, d, di)
-
- return definition, d, di
+ if not isinstance(definition.converter, OffsetConverter):
+ logger.debug(
+ "Cannot autogenerate delta version for a unit in "
+ "which the converter is not an OffsetConverter"
+ )
+ return
+
+ delta_name = "delta_" + definition.name
+ if definition.symbol:
+ delta_symbol = "Δ" + definition.symbol
+ else:
+ delta_symbol = None
+
+ delta_aliases = tuple("Δ" + alias for alias in definition.aliases) + tuple(
+ "delta_" + alias for alias in definition.aliases
+ )
+
+ delta_reference = self.UnitsContainer(
+ {ref: value for ref, value in definition.reference.items()}
+ )
+
+ delta_def = UnitDefinition(
+ delta_name,
+ delta_symbol,
+ delta_aliases,
+ ScaleConverter(definition.converter.scale),
+ delta_reference,
+ )
+ super()._add_unit(delta_def)
def _is_multiplicative(self, u) -> bool:
if u in self._units:
diff --git a/pint/facets/numpy/numpy_func.py b/pint/facets/numpy/numpy_func.py
index 59c2f98..7bce41e 100644
--- a/pint/facets/numpy/numpy_func.py
+++ b/pint/facets/numpy/numpy_func.py
@@ -77,6 +77,8 @@ def convert_arg(arg, pre_calc_units):
Helper function for convert_to_consistent_units. pre_calc_units must be given as a
pint Unit or None.
"""
+ if isinstance(arg, bool):
+ return arg
if pre_calc_units is not None:
if _is_quantity(arg):
return arg.m_as(pre_calc_units)
@@ -101,7 +103,7 @@ def convert_to_consistent_units(*args, pre_calc_units=None, **kwargs):
If pre_calc_units is not None, takes the args and kwargs for a NumPy function and
converts any Quantity or Sequence of Quantities into the units of the first
- Quantity/Sequence of Quantities and returns the magnitudes. Other args/kwargs are
+ Quantity/Sequence of Quantities and returns the magnitudes. Other args/kwargs (except booleans) are
treated as dimensionless Quantities. If pre_calc_units is None, units are simply
stripped.
"""
@@ -419,6 +421,7 @@ matching_input_copy_units_output_ufuncs = [
"nextafter",
"trunc",
"absolute",
+ "positive",
"negative",
"maximum",
"minimum",
@@ -883,7 +886,14 @@ for func_str in ["cumprod", "cumproduct", "nancumprod"]:
implement_single_dimensionless_argument_func(func_str)
# Handle single-argument consistent unit functions
-for func_str in ["block", "hstack", "vstack", "dstack", "column_stack"]:
+for func_str in [
+ "block",
+ "hstack",
+ "vstack",
+ "dstack",
+ "column_stack",
+ "broadcast_arrays",
+]:
implement_func(
"function", func_str, input_units="all_consistent", output_unit="match_input"
)
diff --git a/pint/facets/plain/__init__.py b/pint/facets/plain/__init__.py
index 604cb42..211d017 100644
--- a/pint/facets/plain/__init__.py
+++ b/pint/facets/plain/__init__.py
@@ -18,7 +18,7 @@ from .definitions import (
ScaleConverter,
UnitDefinition,
)
-from .objects import PlainQuantity, PlainUnit, UnitsContainer
+from .objects import PlainQuantity, PlainUnit
from .registry import PlainRegistry
__all__ = [
@@ -31,5 +31,4 @@ __all__ = [
"PrefixDefinition",
"ScaleConverter",
"UnitDefinition",
- "UnitsContainer",
]
diff --git a/pint/facets/plain/definitions.py b/pint/facets/plain/definitions.py
index c1d1d9a..11a3095 100644
--- a/pint/facets/plain/definitions.py
+++ b/pint/facets/plain/definitions.py
@@ -8,259 +8,267 @@
from __future__ import annotations
+import itertools
+import numbers
+import typing as ty
from dataclasses import dataclass
-from typing import Iterable, Optional, Tuple, Union
+from functools import cached_property
+from typing import Callable, Optional
+from ... import errors
from ...converters import Converter
-from ...definitions import Definition, PreprocessedDefinition
-from ...errors import DefinitionSyntaxError
-from ...util import ParserHelper, SourceIterator, UnitsContainer, _is_dim
+from ...util import UnitsContainer
-class _NotNumeric(Exception):
+class NotNumeric(Exception):
"""Internal exception. Do not expose outside Pint"""
def __init__(self, value):
self.value = value
-def numeric_parse(s: str, non_int_type: type = float):
- """Try parse a string into a number (without using eval).
-
- Parameters
- ----------
- s : str
- non_int_type : type
-
- Returns
- -------
- Number
-
- Raises
- ------
- _NotNumeric
- If the string cannot be parsed as a number.
- """
- ph = ParserHelper.from_string(s, non_int_type)
-
- if len(ph):
- raise _NotNumeric(s)
-
- return ph.scale
+########################
+# Convenience functions
+########################
@dataclass(frozen=True)
-class PrefixDefinition(Definition):
- """Definition of a prefix::
-
- <prefix>- = <amount> [= <symbol>] [= <alias>] [ = <alias> ] [...]
+class Equality:
+ """An equality statement contains a left and right hand separated
+ by and equal (=) sign.
- Example::
+ lhs = rhs
- deca- = 1e+1 = da- = deka-
+ lhs and rhs are space stripped.
"""
- @classmethod
- def accept_to_parse(cls, preprocessed: PreprocessedDefinition):
- return preprocessed.name.endswith("-")
+ lhs: str
+ rhs: str
- @classmethod
- def from_string(
- cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
- ) -> PrefixDefinition:
- if isinstance(definition, str):
- definition = PreprocessedDefinition.from_string(definition)
- aliases = tuple(alias.strip("-") for alias in definition.aliases)
- if definition.symbol:
- symbol = definition.symbol.strip("-")
- else:
- symbol = definition.symbol
-
- try:
- converter = ScaleConverter(numeric_parse(definition.value, non_int_type))
- except _NotNumeric as ex:
- raise ValueError(
- f"Prefix definition ('{definition.name}') must contain only numbers, not {ex.value}"
- )
+@dataclass(frozen=True)
+class CommentDefinition:
+ """A comment"""
- return cls(definition.name.rstrip("-"), symbol, aliases, converter)
+ comment: str
@dataclass(frozen=True)
-class UnitDefinition(Definition, default=True):
- """Definition of a unit::
-
- <canonical name> = <relation to another unit or dimension> [= <symbol>] [= <alias>] [ = <alias> ] [...]
+class DefaultsDefinition:
+ """Directive to store default values."""
- Example::
+ group: ty.Optional[str]
+ system: ty.Optional[str]
- millennium = 1e3 * year = _ = millennia
+ def items(self):
+ if self.group is not None:
+ yield "group", self.group
+ if self.system is not None:
+ yield "system", self.system
- Parameters
- ----------
- reference : UnitsContainer
- Reference units.
- is_base : bool
- Indicates if it is a plain unit.
- """
+@dataclass(frozen=True)
+class PrefixDefinition(errors.WithDefErr):
+ """Definition of a prefix."""
+
+ #: name of the prefix
+ name: str
+ #: scaling value for this prefix
+ value: numbers.Number
+ #: canonical symbol
+ defined_symbol: Optional[str] = ""
+ #: additional names for the same prefix
+ aliases: ty.Tuple[str, ...] = ()
+
+ @property
+ def symbol(self) -> str:
+ return self.defined_symbol or self.name
+
+ @property
+ def has_symbol(self) -> bool:
+ return bool(self.defined_symbol)
+
+ @cached_property
+ def converter(self):
+ return Converter.from_arguments(scale=self.value)
+
+ def __post_init__(self):
+ if not errors.is_valid_prefix_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_PREFIX_NAME)
+
+ if self.defined_symbol and not errors.is_valid_prefix_symbol(self.name):
+ raise self.def_err(
+ f"the symbol {self.defined_symbol} " + errors.MSG_INVALID_PREFIX_SYMBOL
+ )
- reference: Optional[UnitsContainer] = None
- is_base: bool = False
+ for alias in self.aliases:
+ if not errors.is_valid_prefix_alias(alias):
+ raise self.def_err(
+ f"the alias {alias} " + errors.MSG_INVALID_PREFIX_ALIAS
+ )
- @classmethod
- def from_string(
- cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
- ) -> "UnitDefinition":
- if isinstance(definition, str):
- definition = PreprocessedDefinition.from_string(definition)
- if ";" in definition.value:
- [converter, modifiers] = definition.value.split(";", 1)
+@dataclass(frozen=True)
+class UnitDefinition(errors.WithDefErr):
+ """Definition of a unit."""
+
+ #: canonical name of the unit
+ name: str
+ #: canonical symbol
+ defined_symbol: ty.Optional[str]
+ #: additional names for the same unit
+ aliases: ty.Tuple[str, ...]
+ #: A functiont that converts a value in these units into the reference units
+ converter: ty.Optional[ty.Union[Callable, Converter]]
+ #: Reference units.
+ reference: ty.Optional[UnitsContainer]
+
+ def __post_init__(self):
+ if not errors.is_valid_unit_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_UNIT_NAME)
+
+ if not any(map(errors.is_dim, self.reference.keys())):
+ invalid = tuple(
+ itertools.filterfalse(errors.is_valid_unit_name, self.reference.keys())
+ )
+ if invalid:
+ raise self.def_err(
+ f"refers to {', '.join(invalid)} that "
+ + errors.MSG_INVALID_UNIT_NAME
+ )
+ is_base = False
- try:
- modifiers = dict(
- (key.strip(), numeric_parse(value, non_int_type))
- for key, value in (part.split(":") for part in modifiers.split(";"))
+ elif all(map(errors.is_dim, self.reference.keys())):
+ invalid = tuple(
+ itertools.filterfalse(
+ errors.is_valid_dimension_name, self.reference.keys()
)
- except _NotNumeric as ex:
- raise ValueError(
- f"Unit definition ('{definition.name}') must contain only numbers in modifier, not {ex.value}"
+ )
+ if invalid:
+ raise self.def_err(
+ f"refers to {', '.join(invalid)} that "
+ + errors.MSG_INVALID_DIMENSION_NAME
)
- else:
- converter = definition.value
- modifiers = {}
-
- converter = ParserHelper.from_string(converter, non_int_type)
-
- if not any(_is_dim(key) for key in converter.keys()):
- is_base = False
- elif all(_is_dim(key) for key in converter.keys()):
is_base = True
+ scale = getattr(self.converter, "scale", 1)
+ if scale != 1:
+ return self.def_err(
+ "Base unit definitions cannot have a scale different to 1. "
+ f"(`{scale}` found)"
+ )
else:
- raise DefinitionSyntaxError(
+ raise self.def_err(
"Cannot mix dimensions and units in the same definition. "
"Base units must be referenced only to dimensions. "
"Derived units must be referenced only to units."
)
- reference = UnitsContainer(converter)
-
- try:
- converter = Converter.from_arguments(scale=converter.scale, **modifiers)
- except Exception as ex:
- raise DefinitionSyntaxError(
- "Unable to assign a converter to the unit"
- ) from ex
-
- return cls(
- definition.name,
- definition.symbol,
- definition.aliases,
- converter,
- reference,
- is_base,
- )
+ super.__setattr__(self, "_is_base", is_base)
-@dataclass(frozen=True)
-class DimensionDefinition(Definition):
- """Definition of a dimension::
-
- [dimension name] = <relation to other dimensions>
+ if self.defined_symbol and not errors.is_valid_unit_symbol(self.name):
+ raise self.def_err(
+ f"the symbol {self.defined_symbol} " + errors.MSG_INVALID_UNIT_SYMBOL
+ )
- Example::
+ for alias in self.aliases:
+ if not errors.is_valid_unit_alias(alias):
+ raise self.def_err(
+ f"the alias {alias} " + errors.MSG_INVALID_UNIT_ALIAS
+ )
- [density] = [mass] / [volume]
- """
+ @property
+ def is_base(self) -> bool:
+ """Indicates if it is a base unit."""
+ return self._is_base
- reference: Optional[UnitsContainer] = None
- is_base: bool = False
+ @property
+ def is_multiplicative(self) -> bool:
+ return self.converter.is_multiplicative
- @classmethod
- def accept_to_parse(cls, preprocessed: PreprocessedDefinition):
- return preprocessed.name.startswith("[")
+ @property
+ def is_logarithmic(self) -> bool:
+ return self.converter.is_logarithmic
- @classmethod
- def from_string(
- cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
- ) -> DimensionDefinition:
- if isinstance(definition, str):
- definition = PreprocessedDefinition.from_string(definition)
+ @property
+ def symbol(self) -> str:
+ return self.defined_symbol or self.name
- converter = ParserHelper.from_string(definition.value, non_int_type)
+ @property
+ def has_symbol(self) -> bool:
+ return bool(self.defined_symbol)
- if not converter:
- is_base = True
- elif all(_is_dim(key) for key in converter.keys()):
- is_base = False
- else:
- raise DefinitionSyntaxError(
- "Base dimensions must be referenced to None. "
- "Derived dimensions must only be referenced "
- "to dimensions."
- )
- reference = UnitsContainer(converter, non_int_type=non_int_type)
-
- return cls(
- definition.name,
- definition.symbol,
- definition.aliases,
- converter,
- reference,
- is_base,
- )
+@dataclass(frozen=True)
+class DimensionDefinition(errors.WithDefErr):
+ """Definition of a root dimension"""
-class AliasDefinition(Definition):
- """Additional alias(es) for an already existing unit::
+ #: name of the dimension
+ name: str
- @alias <canonical name or previous alias> = <alias> [ = <alias> ] [...]
+ @property
+ def is_base(self):
+ return True
- Example::
+ def __post_init__(self):
+ if not errors.is_valid_dimension_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_DIMENSION_NAME)
- @alias meter = my_meter
- """
- def __init__(self, name: str, aliases: Iterable[str]) -> None:
- super().__init__(
- name=name, defined_symbol=None, aliases=aliases, converter=None
- )
+@dataclass(frozen=True)
+class DerivedDimensionDefinition(DimensionDefinition):
+ """Definition of a derived dimension."""
- @classmethod
- def from_string(
- cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
- ) -> AliasDefinition:
+ #: reference dimensions.
+ reference: UnitsContainer
- if isinstance(definition, str):
- definition = PreprocessedDefinition.from_string(definition)
+ @property
+ def is_base(self):
+ return False
- name = definition.name[len("@alias ") :].lstrip()
- return AliasDefinition(name, tuple(definition.rhs_parts))
+ def __post_init__(self):
+ if not errors.is_valid_dimension_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_DIMENSION_NAME)
+ if not all(map(errors.is_dim, self.reference.keys())):
+ return self.def_err(
+ "derived dimensions must only reference other dimensions"
+ )
-@dataclass(frozen=True)
-class DefaultsDefinition:
- """Definition for the @default directive"""
+ invalid = tuple(
+ itertools.filterfalse(errors.is_valid_dimension_name, self.reference.keys())
+ )
- content: Tuple[Tuple[str, str], ...]
+ if invalid:
+ raise self.def_err(
+ f"refers to {', '.join(invalid)} that "
+ + errors.MSG_INVALID_DIMENSION_NAME
+ )
- @classmethod
- def from_lines(cls, lines, non_int_type=float) -> DefaultsDefinition:
- source_iterator = SourceIterator(lines)
- next(source_iterator)
- out = []
- for lineno, part in source_iterator:
- k, v = part.split("=")
- out.append((k.strip(), v.strip()))
- return DefaultsDefinition(tuple(out))
+@dataclass(frozen=True)
+class AliasDefinition(errors.WithDefErr):
+ """Additional alias(es) for an already existing unit."""
+
+ #: name of the already existing unit
+ name: str
+ #: aditional names for the same unit
+ aliases: ty.Tuple[str, ...]
+
+ def __post_init__(self):
+ if not errors.is_valid_unit_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_UNIT_NAME)
+
+ for alias in self.aliases:
+ if not errors.is_valid_unit_alias(alias):
+ raise self.def_err(
+ f"the alias {alias} " + errors.MSG_INVALID_UNIT_ALIAS
+ )
@dataclass(frozen=True)
class ScaleConverter(Converter):
- """A linear transformation."""
+ """A linear transformation without offset."""
scale: float
diff --git a/pint/facets/plain/quantity.py b/pint/facets/plain/quantity.py
index d4c1a55..314cc3a 100644
--- a/pint/facets/plain/quantity.py
+++ b/pint/facets/plain/quantity.py
@@ -144,6 +144,12 @@ class PlainQuantity(PrettyIPython, SharedRegistryObject, Generic[_MagnitudeType]
_magnitude: _MagnitudeType
@property
+ def ndim(self) -> int:
+ if isinstance(self.magnitude, numbers.Number):
+ return 0
+ return self.magnitude.ndim
+
+ @property
def force_ndarray(self) -> bool:
return self._REGISTRY.force_ndarray
diff --git a/pint/facets/plain/registry.py b/pint/facets/plain/registry.py
index 8572fec..8b98965 100644
--- a/pint/facets/plain/registry.py
+++ b/pint/facets/plain/registry.py
@@ -10,12 +10,12 @@ from __future__ import annotations
import copy
import functools
+import inspect
import itertools
import locale
import pathlib
import re
from collections import defaultdict
-from dataclasses import dataclass
from decimal import Decimal
from fractions import Fraction
from numbers import Number
@@ -41,19 +41,12 @@ if TYPE_CHECKING:
from ..context import Context
from pint import Quantity, Unit
-from ... import parser
from ..._typing import QuantityOrUnitLike, UnitLike
from ..._vendor import appdirs
from ...compat import HAS_BABEL, babel_parse, tokenizer
-from ...definitions import Definition
-from ...errors import (
- DefinitionSyntaxError,
- DimensionalityError,
- RedefinitionError,
- UndefinedUnitError,
-)
+from ...errors import DimensionalityError, RedefinitionError, UndefinedUnitError
from ...pint_eval import build_eval_tree
-from ...util import ParserHelper, SourceIterator
+from ...util import ParserHelper
from ...util import UnitsContainer
from ...util import UnitsContainer as UnitsContainerT
from ...util import (
@@ -68,9 +61,11 @@ from ...util import (
)
from .definitions import (
AliasDefinition,
+ CommentDefinition,
+ DefaultsDefinition,
+ DerivedDimensionDefinition,
DimensionDefinition,
PrefixDefinition,
- ScaleConverter,
UnitDefinition,
)
from .objects import PlainQuantity, PlainUnit
@@ -89,24 +84,6 @@ T = TypeVar("T")
_BLOCK_RE = re.compile(r"[ (]")
-@dataclass(frozen=True)
-class DefaultsDefinition:
- """Definition for the @default directive"""
-
- content: Tuple[Tuple[str, str], ...]
-
- @classmethod
- def from_lines(cls, lines, non_int_type=float) -> DefaultsDefinition:
- source_iterator = SourceIterator(lines)
- next(source_iterator)
- out = []
- for lineno, part in source_iterator:
- k, v = part.split("=")
- out.append((k.strip(), v.strip()))
-
- return DefaultsDefinition(tuple(out))
-
-
@functools.lru_cache()
def pattern_to_regex(pattern):
if hasattr(pattern, "finditer"):
@@ -212,6 +189,8 @@ class PlainRegistry(metaclass=RegistryMeta):
_quantity_class = PlainQuantity
_unit_class = PlainUnit
+ _def_parser = None
+
def __init__(
self,
filename="",
@@ -226,17 +205,25 @@ class PlainRegistry(metaclass=RegistryMeta):
cache_folder: Union[str, pathlib.Path, None] = None,
separate_format_defaults: Optional[bool] = None,
):
- #: Map context prefix to (loader function, parser function, single_line)
- #: type: Dict[str, Tuple[Callable[[Any], None]], Any]
- self._directives = {}
- self._register_directives()
+ #: Map a definition class to a adder methods.
+ self._adders = dict()
+ self._register_definition_adders()
self._init_dynamic_classes()
if cache_folder == ":auto:":
cache_folder = appdirs.user_cache_dir(appname="pint", appauthor=False)
+ cache_folder = pathlib.Path(cache_folder)
+
+ from ... import delegates # TODO: change thiss
if cache_folder is not None:
- self._diskcache = parser.build_disk_cache_class(non_int_type)(cache_folder)
+ self._diskcache = delegates.build_disk_cache_class(non_int_type)(
+ cache_folder
+ )
+
+ self._def_parser = delegates.txt_defparser.DefParser(
+ delegates.ParserConfig(non_int_type), diskcache=self._diskcache
+ )
self._filename = filename
self.force_ndarray = force_ndarray
@@ -256,7 +243,7 @@ class PlainRegistry(metaclass=RegistryMeta):
self.set_fmt_locale(fmt_locale)
#: Numerical type used for non integer values.
- self.non_int_type = non_int_type
+ self._non_int_type = non_int_type
#: Default unit case sensitivity
self.case_sensitive = case_sensitive
@@ -266,7 +253,9 @@ class PlainRegistry(metaclass=RegistryMeta):
self._defaults: Dict[str, str] = {}
#: Map dimension name (string) to its definition (DimensionDefinition).
- self._dimensions: Dict[str, DimensionDefinition] = {}
+ self._dimensions: Dict[
+ str, Union[DimensionDefinition, DerivedDimensionDefinition]
+ ] = {}
#: Map unit name (string) to its definition (UnitDefinition).
#: Might contain prefixed units.
@@ -279,9 +268,7 @@ class PlainRegistry(metaclass=RegistryMeta):
self._units_casei: Dict[str, Set[str]] = defaultdict(set)
#: Map prefix name (string) to its definition (PrefixDefinition).
- self._prefixes: Dict[str, PrefixDefinition] = {
- "": PrefixDefinition("", "", (), 1)
- }
+ self._prefixes: Dict[str, PrefixDefinition] = {"": PrefixDefinition("", 1)}
#: Map suffix name (string) to canonical , and unit alias to canonical unit name
self._suffixes: Dict[str, str] = {"": "", "s": ""}
@@ -306,7 +293,8 @@ class PlainRegistry(metaclass=RegistryMeta):
"""This should be called after all __init__"""
if self._filename == "":
- loaded_files = self.load_definitions("default_en.txt", True)
+ path = pathlib.Path(__file__).parent.parent.parent / "default_en.txt"
+ loaded_files = self.load_definitions(path, True)
elif self._filename is not None:
loaded_files = self.load_definitions(self._filename)
else:
@@ -315,19 +303,18 @@ class PlainRegistry(metaclass=RegistryMeta):
self._build_cache(loaded_files)
self._initialized = True
- def _register_directives(self) -> None:
- self._register_directive("@alias", self._load_alias, AliasDefinition)
- self._register_directive("@defaults", self._load_defaults, DefaultsDefinition)
+ def _register_adder(self, definition_class, adder_func):
+ """Register a block definition."""
+ self._adders[definition_class] = adder_func
- def _load_defaults(self, defaults_definition: DefaultsDefinition) -> None:
- """Loader for a @default section."""
-
- for k, v in defaults_definition.content:
- self._defaults[k] = v
-
- def _load_alias(self, alias_definition: AliasDefinition) -> None:
- """Loader for an @alias directive"""
- self._define_alias(alias_definition)
+ def _register_definition_adders(self) -> None:
+ self._register_adder(AliasDefinition, self._add_alias)
+ self._register_adder(DefaultsDefinition, self._add_defaults)
+ self._register_adder(CommentDefinition, lambda o: o)
+ self._register_adder(PrefixDefinition, self._add_prefix)
+ self._register_adder(UnitDefinition, self._add_unit)
+ self._register_adder(DimensionDefinition, self._add_dimension)
+ self._register_adder(DerivedDimensionDefinition, self._add_derived_dimension)
def __deepcopy__(self, memo) -> "PlainRegistry":
new = object.__new__(type(self))
@@ -405,7 +392,11 @@ class PlainRegistry(metaclass=RegistryMeta):
return self._diskcache.cache_folder
return None
- def define(self, definition: Union[str, Definition]) -> None:
+ @property
+ def non_int_type(self):
+ return self._non_int_type
+
+ def define(self, definition):
"""Add unit to the registry.
Parameters
@@ -415,162 +406,99 @@ class PlainRegistry(metaclass=RegistryMeta):
"""
if isinstance(definition, str):
- for line in definition.split("\n"):
- if line.startswith("@alias"):
- # TODO why alias can be defined like this but not other directives?
- self._define_alias(
- AliasDefinition.from_string(line, self.non_int_type)
- )
- else:
- self._define(Definition.from_string(line, self.non_int_type))
- else:
- self._define(definition)
-
- def _define(self, definition: Definition) -> Tuple[Definition, dict, dict]:
- """Add unit to the registry.
-
- This method defines only multiplicative units, converting any other type
- to `delta_` units.
+ parsed_project = self._def_parser.parse_string(definition)
- Parameters
- ----------
- definition : Definition
- a dimension, unit or prefix definition.
+ for definition in self._def_parser.iter_parsed_project(parsed_project):
+ self._helper_dispatch_adder(definition)
+ else:
+ self._helper_dispatch_adder(definition)
- Returns
- -------
- Definition, dict, dict
- Definition instance, case sensitive unit dict, case insensitive unit dict.
+ ############
+ # Adders
+ # - we first provide some helpers that deal with repetitive task.
+ # - then we define specific adder for each definition class. :-D
+ ############
+ def _helper_dispatch_adder(self, definition):
+ """Helper function to add a single definition,
+ choosing the appropiate method by class.
"""
-
- if isinstance(definition, DimensionDefinition):
- d, di = self._dimensions, None
-
- elif isinstance(definition, UnitDefinition):
- d, di = self._units, self._units_casei
-
- # For a plain units, we need to define the related dimension
- # (making sure there is only one to define)
- if definition.is_base:
- for dimension in definition.reference.keys():
- if dimension in self._dimensions:
- if dimension != "[]":
- raise DefinitionSyntaxError(
- "Only one unit per dimension can be a plain unit"
- )
- continue
-
- self.define(
- DimensionDefinition(dimension, "", (), None, None, True)
- )
-
- elif isinstance(definition, PrefixDefinition):
- d, di = self._prefixes, None
-
+ for cls in inspect.getmro(definition.__class__):
+ if cls in self._adders:
+ adder_func = self._adders[cls]
+ break
else:
- raise TypeError("{} is not a valid definition.".format(definition))
-
- # define "delta_" units for units with an offset
- if getattr(definition.converter, "offset", 0) != 0:
-
- if definition.name.startswith("["):
- d_name = "[delta_" + definition.name[1:]
- else:
- d_name = "delta_" + definition.name
-
- if definition.symbol:
- d_symbol = "Δ" + definition.symbol
- else:
- d_symbol = None
-
- d_aliases = tuple("Δ" + alias for alias in definition.aliases) + tuple(
- "delta_" + alias for alias in definition.aliases
- )
-
- d_reference = self.UnitsContainer(
- {ref: value for ref, value in definition.reference.items()}
- )
-
- d_def = UnitDefinition(
- d_name,
- d_symbol,
- d_aliases,
- ScaleConverter(definition.converter.scale),
- d_reference,
- definition.is_base,
+ raise TypeError(
+ f"No loader function defined " f"for {definition.__class__.__name__}"
)
- else:
- d_def = definition
-
- self._define_adder(d_def, d, di)
- return definition, d, di
+ adder_func(definition)
- def _define_adder(self, definition, unit_dict, casei_unit_dict):
+ def _helper_adder(self, definition, target_dict, casei_target_dict):
"""Helper function to store a definition in the internal dictionaries.
It stores the definition under its name, symbol and aliases.
"""
- self._define_single_adder(
- definition.name, definition, unit_dict, casei_unit_dict
+ self._helper_single_adder(
+ definition.name, definition, target_dict, casei_target_dict
)
- if definition.has_symbol:
- self._define_single_adder(
- definition.symbol, definition, unit_dict, casei_unit_dict
+ if getattr(definition, "has_symbol", ""):
+ self._helper_single_adder(
+ definition.symbol, definition, target_dict, casei_target_dict
)
- for alias in definition.aliases:
+ for alias in getattr(definition, "aliases", ()):
if " " in alias:
logger.warn("Alias cannot contain a space: " + alias)
- self._define_single_adder(alias, definition, unit_dict, casei_unit_dict)
+ self._helper_single_adder(alias, definition, target_dict, casei_target_dict)
- def _define_single_adder(self, key, value, unit_dict, casei_unit_dict):
+ def _helper_single_adder(self, key, value, target_dict, casei_target_dict):
"""Helper function to store a definition in the internal dictionaries.
It warns or raise error on redefinition.
"""
- if key in unit_dict:
+ if key in target_dict:
if self._on_redefinition == "raise":
raise RedefinitionError(key, type(value))
elif self._on_redefinition == "warn":
logger.warning("Redefining '%s' (%s)" % (key, type(value)))
- unit_dict[key] = value
- if casei_unit_dict is not None:
- casei_unit_dict[key.lower()].add(key)
+ target_dict[key] = value
+ if casei_target_dict is not None:
+ casei_target_dict[key.lower()].add(key)
- def _define_alias(self, definition):
- if not isinstance(definition, AliasDefinition):
- raise TypeError(
- "Not a valid input type for _define_alias. "
- f"(expected: AliasDefinition, found: {type(definition)}"
- )
+ def _add_defaults(self, defaults_definition: DefaultsDefinition):
+ for k, v in defaults_definition.items():
+ self._defaults[k] = v
+ def _add_alias(self, definition: AliasDefinition):
unit_dict = self._units
unit = unit_dict[definition.name]
while not isinstance(unit, UnitDefinition):
unit = unit_dict[unit.name]
for alias in definition.aliases:
- self._define_single_adder(alias, unit, self._units, self._units_casei)
+ self._helper_single_adder(alias, unit, self._units, self._units_casei)
- def _register_directive(self, prefix: str, loaderfunc, definition_class):
- """Register a loader for a given @ directive.
+ def _add_dimension(self, definition: DimensionDefinition):
+ self._helper_adder(definition, self._dimensions, None)
- Parameters
- ----------
- prefix
- string identifying the section (e.g. @context).
- loaderfunc
- function to load the definition into the registry.
- definition_class
- a class that represents the directive content.
- """
- if prefix and prefix[0] == "@":
- self._directives[prefix] = (loaderfunc, definition_class)
- else:
- raise ValueError("Prefix directives must start with '@'")
+ def _add_derived_dimension(self, definition: DerivedDimensionDefinition):
+ for dim_name in definition.reference.keys():
+ if dim_name not in self._dimensions:
+ self._add_dimension(DimensionDefinition(dim_name))
+ self._helper_adder(definition, self._dimensions, None)
+
+ def _add_prefix(self, definition: PrefixDefinition):
+ self._helper_adder(definition, self._prefixes, None)
+
+ def _add_unit(self, definition: UnitDefinition):
+ if definition.is_base:
+ for dim_name in definition.reference.keys():
+ if dim_name not in self._dimensions:
+ self._add_dimension(DimensionDefinition(dim_name))
+
+ self._helper_adder(definition, self._units, self._units_casei)
def load_definitions(self, file, is_resource: bool = False):
"""Add units and prefixes defined in a definition text file.
@@ -584,50 +512,26 @@ class PlainRegistry(metaclass=RegistryMeta):
and therefore should be loaded from the package. (Default value = False)
"""
- loaders = {
- AliasDefinition: self._define,
- UnitDefinition: self._define,
- DimensionDefinition: self._define,
- PrefixDefinition: self._define,
- }
-
- p = parser.Parser(self.non_int_type, cache_folder=self._diskcache)
- for prefix, (loaderfunc, definition_class) in self._directives.items():
- loaders[definition_class] = loaderfunc
- p.register_class(prefix, definition_class)
-
- if isinstance(file, (str, pathlib.Path)):
- try:
- parsed_files = p.parse(file, is_resource)
- except Exception as ex:
- # TODO: Change this is in the future
- # this is kept for backwards compatibility
- msg = getattr(ex, "message", "") or str(ex)
- raise ValueError("While opening {}\n{}".format(file, msg))
+ if isinstance(file, (list, tuple)):
+ # TODO: this hack was to keep it backwards compatible.
+ parsed_project = self._def_parser.parse_string("\n".join(file))
else:
- parsed_files = parser.DefinitionFiles([p.parse_lines(file)])
+ parsed_project = self._def_parser.parse_file(file)
- for lineno, definition in parsed_files.iter_definitions():
- if definition.__class__ in p.handled_classes:
- continue
- loaderfunc = loaders.get(definition.__class__, None)
- if not loaderfunc:
- raise ValueError(
- f"No loader function defined "
- f"for {definition.__class__.__name__}"
- )
- loaderfunc(definition)
+ for definition in self._def_parser.iter_parsed_project(parsed_project):
+ self._helper_dispatch_adder(definition)
- return parsed_files
+ return parsed_project
def _build_cache(self, loaded_files=None) -> None:
"""Build a cache of dimensionality and plain units."""
- if loaded_files and self._diskcache and all(loaded_files):
- cache, cache_basename = self._diskcache.load(loaded_files, "build_cache")
+ diskcache = self._diskcache
+ if loaded_files and diskcache:
+ cache, cache_basename = diskcache.load(loaded_files, "build_cache")
if cache is None:
self._build_cache()
- self._diskcache.save(self._cache, loaded_files, "build_cache")
+ diskcache.save(self._cache, loaded_files, "build_cache")
return
self._cache = RegistryCache()
@@ -1226,9 +1130,9 @@ class PlainRegistry(metaclass=RegistryMeta):
if token_text == "dimensionless":
return 1 * self.dimensionless
elif token_text.lower() in ("inf", "infinity"):
- return float("inf")
+ return self.non_int_type("inf")
elif token_text.lower() == "nan":
- return float("nan")
+ return self.non_int_type("nan")
elif token_text in values:
return self.Quantity(values[token_text])
else:
diff --git a/pint/facets/system/definitions.py b/pint/facets/system/definitions.py
index 368dd71..8243324 100644
--- a/pint/facets/system/definitions.py
+++ b/pint/facets/system/definitions.py
@@ -6,82 +6,76 @@
:license: BSD, see LICENSE for more details.
"""
-
from __future__ import annotations
-import re
+import typing as ty
from dataclasses import dataclass
-from typing import Tuple
-from ...util import SourceIterator
+from ... import errors
@dataclass(frozen=True)
-class SystemDefinition:
- """Definition of a System:
-
- @system <name> [using <group 1>, ..., <group N>]
- <rule 1>
- ...
- <rule N>
- @end
+class BaseUnitRule:
+ """A rule to define a base unit within a system."""
- The syntax for the rule is:
+ #: name of the unit to become base unit
+ #: (must exist in the registry)
+ new_unit_name: str
+ #: name of the unit to be kicked out to make room for the new base uni
+ #: If None, the current base unit with the same dimensionality will be used
+ old_unit_name: ty.Optional[str] = None
- new_unit_name : old_unit_name
+ # Instead of defining __post_init__ here,
+ # it will be added to the container class
+ # so that the name and a meaningfull class
+ # could be used.
- where:
- - old_unit_name: a root unit part which is going to be removed from the system.
- - new_unit_name: a non root unit which is going to replace the old_unit.
- If the new_unit_name and the old_unit_name, the later and the colon can be omitted.
- """
-
- #: Regex to match the header parts of a context.
- _header_re = re.compile(r"@system\s+(?P<name>\w+)\s*(using\s(?P<used_groups>.*))*")
+@dataclass(frozen=True)
+class SystemDefinition(errors.WithDefErr):
+ """Definition of a System."""
+ #: name of the system
name: str
- unit_replacements: Tuple[Tuple[int, str, str], ...]
- using_group_names: Tuple[str, ...]
+ #: unit groups that will be included within the system
+ using_group_names: ty.Tuple[str, ...]
+ #: rules to define new base unit within the system.
+ rules: ty.Tuple[BaseUnitRule, ...]
@classmethod
- def from_lines(cls, lines, non_int_type=float):
- lines = SourceIterator(lines)
-
- lineno, header = next(lines)
-
- r = cls._header_re.search(header)
-
- if r is None:
- raise ValueError("Invalid System header syntax '%s'" % header)
-
- name = r.groupdict()["name"].strip()
- groups = r.groupdict()["used_groups"]
-
- # If the systems has no group, it automatically uses the root group.
- if groups:
- group_names = tuple(a.strip() for a in groups.split(","))
- else:
- group_names = ("root",)
-
- unit_replacements = []
- for lineno, line in lines:
- line = line.strip()
-
- # We would identify a
- # - old_unit: a root unit part which is going to be removed from the system.
- # - new_unit: a non root unit which is going to replace the old_unit.
-
- if ":" in line:
- # The syntax is new_unit:old_unit
-
- new_unit, old_unit = line.split(":")
- new_unit, old_unit = new_unit.strip(), old_unit.strip()
-
- unit_replacements.append((lineno, new_unit, old_unit))
- else:
- # The syntax is new_unit
- # old_unit is inferred as the root unit with the same dimensionality.
- unit_replacements.append((lineno, line, None))
-
- return cls(name, tuple(unit_replacements), group_names)
+ def from_lines(cls, lines, non_int_type):
+ # TODO: this is to keep it backwards compatible
+ from ...delegates import ParserConfig, txt_defparser
+
+ cfg = ParserConfig(non_int_type)
+ parser = txt_defparser.DefParser(cfg, None)
+ pp = parser.parse_string("\n".join(lines) + "\n@end")
+ for definition in parser.iter_parsed_project(pp):
+ if isinstance(definition, cls):
+ return definition
+
+ @property
+ def unit_replacements(self) -> ty.Tuple[ty.Tuple[str, str], ...]:
+ return tuple((el.new_unit_name, el.old_unit_name) for el in self.rules)
+
+ def __post_init__(self):
+ if not errors.is_valid_system_name(self.name):
+ raise self.def_err(errors.MSG_INVALID_SYSTEM_NAME)
+
+ for k in self.using_group_names:
+ if not errors.is_valid_group_name(k):
+ raise self.def_err(
+ f"refers to '{k}' that " + errors.MSG_INVALID_GROUP_NAME
+ )
+
+ for ndx, rule in enumerate(self.rules, 1):
+ if not errors.is_valid_unit_name(rule.new_unit_name):
+ raise self.def_err(
+ f"rule #{ndx} refers to '{rule.new_unit_name}' that "
+ + errors.MSG_INVALID_UNIT_NAME
+ )
+ if rule.old_unit_name and not errors.is_valid_unit_name(rule.old_unit_name):
+ raise self.def_err(
+ f"rule #{ndx} refers to '{rule.old_unit_name}' that "
+ + errors.MSG_INVALID_UNIT_NAME
+ )
diff --git a/pint/facets/system/objects.py b/pint/facets/system/objects.py
index b81dfc5..829fb5c 100644
--- a/pint/facets/system/objects.py
+++ b/pint/facets/system/objects.py
@@ -119,16 +119,19 @@ class System(SharedRegistryObject):
return cls.from_definition(system_definition, get_root_func)
@classmethod
- def from_definition(cls, system_definition: SystemDefinition, get_root_func):
+ def from_definition(cls, system_definition: SystemDefinition, get_root_func=None):
+ if get_root_func is None:
+ # TODO: kept for backwards compatibility
+ get_root_func = cls._REGISTRY.get_root_units
base_unit_names = {}
derived_unit_names = []
- for lineno, new_unit, old_unit in system_definition.unit_replacements:
+ for new_unit, old_unit in system_definition.unit_replacements:
if old_unit is None:
old_unit_dict = to_units_container(get_root_func(new_unit)[1])
if len(old_unit_dict) != 1:
raise ValueError(
- "The new plain must be a root dimension if not discarded unit is specified."
+ "The new unit must be a root dimension if not discarded unit is specified."
)
old_unit, value = dict(old_unit_dict).popitem()
@@ -138,8 +141,8 @@ class System(SharedRegistryObject):
# The old unit MUST be a root unit, if not raise an error.
if old_unit != str(get_root_func(old_unit)[1]):
raise ValueError(
- "In `%s`, the unit at the right of the `:` (%s) must be a root unit."
- % (lineno, old_unit)
+ f"The old unit {old_unit} must be a root unit "
+ f"in order to be replaced by new unit {new_unit}"
)
# Here we find new_unit expanded in terms of root_units
diff --git a/pint/facets/system/registry.py b/pint/facets/system/registry.py
index ca60766..2bab44b 100644
--- a/pint/facets/system/registry.py
+++ b/pint/facets/system/registry.py
@@ -11,6 +11,8 @@ from __future__ import annotations
from numbers import Number
from typing import TYPE_CHECKING, Dict, FrozenSet, Tuple, Union
+from ... import errors
+
if TYPE_CHECKING:
from pint import Quantity, Unit
@@ -41,6 +43,9 @@ class SystemRegistry(GroupRegistry):
- Parse @group directive.
"""
+ # TODO: Change this to System: System to specify class
+ # and use introspection to get system class as a way
+ # to enjoy typing goodies
_system_class = System
def __init__(self, system=None, **kwargs):
@@ -77,13 +82,22 @@ class SystemRegistry(GroupRegistry):
"system", None
)
- def _register_directives(self) -> None:
- super()._register_directives()
- self._register_directive(
- "@system",
- lambda gd: self.System.from_definition(gd, self.get_root_units),
- SystemDefinition,
- )
+ def _register_definition_adders(self) -> None:
+ super()._register_definition_adders()
+ self._register_adder(SystemDefinition, self._add_system)
+
+ def _add_system(self, sd: SystemDefinition):
+
+ if sd.name in self._systems:
+ raise ValueError(f"System {sd.name} already present in registry")
+
+ try:
+ # As a System is a SharedRegistryObject
+ # it adds itself to the registry.
+ self.System.from_definition(sd)
+ except KeyError as e:
+ # TODO: fix this error message
+ raise errors.DefinitionError(f"unknown dimension {e} in context")
@property
def sys(self):
diff --git a/pint/formatting.py b/pint/formatting.py
index b8b3370..554b381 100644
--- a/pint/formatting.py
+++ b/pint/formatting.py
@@ -157,7 +157,7 @@ def register_unit_format(name):
def wrapper(func):
if name in _FORMATTERS:
- raise ValueError(f"format {name:!r} already exists") # or warn instead
+ raise ValueError(f"format {name!r} already exists") # or warn instead
_FORMATTERS[name] = func
return wrapper
@@ -439,8 +439,9 @@ def siunitx_format_unit(units, registry):
lpick = lpos if power >= 0 else lneg
prefix = None
+ # TODO: fix this to be fore efficient and detect also aliases.
for p in registry._prefixes.values():
- p = str(p)
+ p = str(p.name)
if len(p) > 0 and unit.find(p) == 0:
prefix = p
unit = unit.replace(prefix, "", 1)
diff --git a/pint/parser.py b/pint/parser.py
deleted file mode 100644
index e73e578..0000000
--- a/pint/parser.py
+++ /dev/null
@@ -1,374 +0,0 @@
-"""
- pint.parser
- ~~~~~~~~~~~
-
- Classes and methods to parse a definition text file into a DefinitionFile.
-
- :copyright: 2019 by Pint Authors, see AUTHORS for more details.
- :license: BSD, see LICENSE for more details.
-"""
-
-from __future__ import annotations
-
-import pathlib
-import re
-from dataclasses import dataclass, field
-from functools import cached_property
-from importlib import resources
-from io import StringIO
-from typing import Any, Callable, Dict, Generator, Iterable, Optional, Tuple
-
-from ._vendor import flexcache as fc
-from .definitions import Definition
-from .errors import DefinitionSyntaxError
-from .util import SourceIterator, logger
-
-_BLOCK_RE = re.compile(r"[ (]")
-
-ParserFuncT = Callable[[SourceIterator, type], Any]
-
-
-@dataclass(frozen=True)
-class DefinitionFile:
- """Represents a definition file after parsing."""
-
- # Fullpath of the original file, None if a text was provided
- filename: Optional[pathlib.Path]
- is_resource: bool
-
- # Modification time of the file or None.
- mtime: Optional[float]
-
- # SHA-1 hash
- content_hash: Optional[str]
-
- # collection of line number and corresponding definition.
- parsed_lines: Tuple[Tuple[int, Any], ...]
-
- def filter_by(self, *klass):
- yield from (
- (lineno, d) for lineno, d in self.parsed_lines if isinstance(d, klass)
- )
-
- @cached_property
- def errors(self):
- return tuple(self.filter_by(Exception))
-
- def has_errors(self):
- return bool(self.errors)
-
-
-class DefinitionFiles(tuple):
- """Wrapper class that allows handling a tuple containing DefinitionFile."""
-
- @staticmethod
- def _iter_definitions(
- pending_files: list[DefinitionFile],
- ) -> Generator[Tuple[int, Definition]]:
- """Internal method to iterate definitions.
-
- pending_files is a mutable list of definitions files
- and elements are being removed as they are yielded.
- """
- if not pending_files:
- return
- current_file = pending_files.pop(0)
- for lineno, definition in current_file.parsed_lines:
- if isinstance(definition, ImportDefinition):
- if not pending_files:
- raise ValueError(
- f"No more files while trying to import {definition.path}."
- )
-
- if not str(pending_files[0].filename).endswith(str(definition.path)):
- raise ValueError(
- "The order of the files do not match. "
- f"(expected: {definition.path}, "
- f"found {pending_files[0].filename})"
- )
-
- yield from DefinitionFiles._iter_definitions(pending_files)
- else:
- yield lineno, definition
-
- def iter_definitions(self):
- """Iter all definitions in the order they appear,
- going into the included files.
-
- Important: This assumes that the order of the imported files
- is the one that they will appear in the definitions.
- """
- yield from self._iter_definitions(list(self))
-
-
-def build_disk_cache_class(non_int_type: type):
- """Build disk cache class, taking into account the non_int_type."""
-
- @dataclass(frozen=True)
- class PintHeader(fc.InvalidateByExist, fc.NameByFields, fc.BasicPythonHeader):
-
- from . import __version__
-
- pint_version: str = __version__
- non_int_type: str = field(default_factory=lambda: non_int_type.__qualname__)
-
- class PathHeader(fc.NameByFileContent, PintHeader):
- pass
-
- class DefinitionFilesHeader(fc.NameByHashIter, PintHeader):
- @classmethod
- def from_definition_files(cls, dfs: DefinitionFiles, reader_id):
- return cls(tuple(df.content_hash for df in dfs), reader_id)
-
- class PintDiskCache(fc.DiskCache):
-
- _header_classes = {
- pathlib.Path: PathHeader,
- str: PathHeader.from_string,
- DefinitionFiles: DefinitionFilesHeader.from_definition_files,
- }
-
- return PintDiskCache
-
-
-@dataclass(frozen=True)
-class ImportDefinition:
- """Definition for the @import directive"""
-
- path: pathlib.Path
-
- @classmethod
- def from_string(
- cls, definition: str, non_int_type: type = float
- ) -> ImportDefinition:
- return ImportDefinition(pathlib.Path(definition[7:].strip()))
-
-
-class Parser:
- """Class to parse a definition file into an intermediate object representation.
-
- non_int_type
- numerical type used for non integer values. (Default: float)
- raise_on_error
- if True, an exception will be raised as soon as a Definition Error it is found.
- if False, the exception will be added to the ParedDefinitionFile
- """
-
- #: Map context prefix to function
- _directives: Dict[str, ParserFuncT]
-
- _diskcache: fc.DiskCache
-
- handled_classes = (ImportDefinition,)
-
- def __init__(self, non_int_type=float, raise_on_error=True, cache_folder=None):
- self._directives = {}
- self._non_int_type = non_int_type
- self._raise_on_error = raise_on_error
- self.register_class("@import", ImportDefinition)
-
- if isinstance(cache_folder, (str, pathlib.Path)):
- self._diskcache = build_disk_cache_class(non_int_type)(cache_folder)
- else:
- self._diskcache = cache_folder
-
- def register_directive(
- self, prefix: str, parserfunc: ParserFuncT, single_line: bool
- ):
- """Register a parser for a given @ directive..
-
- Parameters
- ----------
- prefix
- string identifying the section (e.g. @context)
- parserfunc
- function that is able to parse a definition into a DefinitionObject
- single_line
- indicates that the directive spans in a single line, i.e. and @end is not required.
- """
- if prefix and prefix[0] == "@":
- if single_line:
- self._directives[prefix] = lambda si, non_int_type: parserfunc(
- si.last[1], non_int_type
- )
- else:
- self._directives[prefix] = lambda si, non_int_type: parserfunc(
- si.block_iter(), non_int_type
- )
- else:
- raise ValueError("Prefix directives must start with '@'")
-
- def register_class(self, prefix: str, klass):
- """Register a definition class for a directive and try to guess
- if it is a line or block directive from the signature.
- """
- if hasattr(klass, "from_string"):
- self.register_directive(prefix, klass.from_string, True)
- elif hasattr(klass, "from_lines"):
- self.register_directive(prefix, klass.from_lines, False)
- else:
- raise ValueError(
- f"While registering {prefix}, {klass} does not have `from_string` or from_lines` method"
- )
-
- def parse(self, file, is_resource: bool = False) -> DefinitionFiles:
- """Parse a file or resource into a collection of DefinitionFile that will
- include all other files imported.
-
- Parameters
- ----------
- file
- definitions or file containing definition.
- is_resource
- indicates that the file is a resource file
- and therefore should be loaded from the package.
- (Default value = False)
- """
-
- if is_resource:
- parsed = self.parse_single_resource(file)
- else:
- path = pathlib.Path(file)
- if self._diskcache is None:
- parsed = self.parse_single(path, None)
- else:
- parsed, content_hash = self._diskcache.load(
- path, self.parse_single, True
- )
-
- out = [parsed]
- for lineno, content in parsed.filter_by(ImportDefinition):
- if parsed.is_resource:
- path = content.path
- else:
- try:
- basedir = parsed.filename.parent
- except AttributeError:
- basedir = pathlib.Path.cwd()
- path = basedir.joinpath(content.path)
- out.extend(self.parse(path, parsed.is_resource))
- return DefinitionFiles(out)
-
- def parse_single_resource(self, resource_name: str) -> DefinitionFile:
- """Parse a resource in the package into a DefinitionFile.
-
- Imported files will appear as ImportDefinition objects and
- will not be followed.
-
- This method will try to load it first as a regular file
- (with a path and mtime) to allow caching.
- If this files (i.e. the resource is not filesystem file)
- it will use python importlib.resources.read_binary
- """
-
- with resources.path(__package__, resource_name) as p:
- filepath = p.resolve()
-
- if filepath.exists():
- if self._diskcache is None:
- return self.parse_single(filepath, None)
- else:
- definition_file, content_hash = self._diskcache.load(
- filepath, self.parse_single, True
- )
- return definition_file
-
- logger.debug("Cannot use_cache resource (yet) without a real path")
- return self._parse_single_resource(resource_name)
-
- def _parse_single_resource(self, resource_name: str) -> DefinitionFile:
- rbytes = resources.read_binary(__package__, resource_name)
- if self._diskcache:
- hdr = self._diskcache.PathHeader(rbytes)
- content_hash = self._diskcache.cache_stem_for(hdr)
- else:
- content_hash = None
-
- si = SourceIterator(
- StringIO(rbytes.decode("utf-8")), resource_name, is_resource=True
- )
- parsed_lines = tuple(self.yield_from_source_iterator(si))
- return DefinitionFile(
- filename=pathlib.Path(resource_name),
- is_resource=True,
- mtime=None,
- content_hash=content_hash,
- parsed_lines=parsed_lines,
- )
-
- def parse_single(
- self, filepath: pathlib.Path, content_hash: Optional[str]
- ) -> DefinitionFile:
- """Parse a filepath without nesting into dependent files.
-
- Imported files will appear as ImportDefinition objects and
- will not be followed.
-
- Parameters
- ----------
- filepath
- definitions or file containing definition.
- """
- with filepath.open(encoding="utf-8") as fp:
- si = SourceIterator(fp, filepath, is_resource=False)
- parsed_lines = tuple(self.yield_from_source_iterator(si))
-
- filename = filepath.resolve()
- mtime = filepath.stat().st_mtime
-
- return DefinitionFile(
- filename=filename,
- is_resource=False,
- mtime=mtime,
- content_hash=content_hash,
- parsed_lines=parsed_lines,
- )
-
- def parse_lines(self, lines: Iterable[str]) -> DefinitionFile:
- """Parse an iterable of strings into a dependent file"""
- si = SourceIterator(lines, None, False)
- parsed_lines = tuple(self.yield_from_source_iterator(si))
- df = DefinitionFile(None, False, None, "", parsed_lines=parsed_lines)
- if any(df.filter_by(ImportDefinition)):
- raise ValueError(
- "Cannot use the @import directive when parsing "
- "an iterable of strings."
- )
- return df
-
- def yield_from_source_iterator(
- self, source_iterator: SourceIterator
- ) -> Generator[Tuple[int, Any]]:
- """Iterates through the source iterator, yields line numbers and
- the coresponding parsed definition object.
-
- Parameters
- ----------
- source_iterator
- """
- for lineno, line in source_iterator:
- try:
- if line.startswith("@"):
- # Handle @ directives dispatching to the appropriate parsers
- parts = _BLOCK_RE.split(line)
-
- subparser = self._directives.get(parts[0], None)
-
- if subparser is None:
- raise DefinitionSyntaxError(
- "Unknown directive %s" % line, lineno=lineno
- )
-
- d = subparser(source_iterator, self._non_int_type)
- yield lineno, d
- else:
- yield lineno, Definition.from_string(line, self._non_int_type)
- except DefinitionSyntaxError as ex:
- if ex.lineno is None:
- ex.lineno = lineno
- if self._raise_on_error:
- raise ex
- yield lineno, ex
- except Exception as ex:
- logger.error("In line {}, cannot add '{}' {}".format(lineno, line, ex))
- raise ex
diff --git a/pint/testsuite/test_contexts.py b/pint/testsuite/test_contexts.py
index d8f5d50..ea6eadc 100644
--- a/pint/testsuite/test_contexts.py
+++ b/pint/testsuite/test_contexts.py
@@ -783,6 +783,7 @@ def test_redefine(subtests):
# Note how we're redefining a symbol, not the plain name, as a
# function of another name
b = 5 f
+ @end
""".splitlines()
)
# Units that are somehow directly or indirectly defined as a function of the
@@ -933,7 +934,7 @@ def test_err_change_base_unit():
def test_err_to_base_unit():
- expected = "Can't define plain units within a context"
+ expected = ".*can't define plain units within a context"
with pytest.raises(DefinitionSyntaxError, match=expected):
Context.from_lines(["@context c", "x = [d]"])
@@ -980,19 +981,17 @@ def test_err_cyclic_dependency():
def test_err_dimension_redefinition():
- expected = re.escape("Expected <unit> = <converter>; got [d1] = [d2] * [d3]")
- with pytest.raises(DefinitionSyntaxError, match=expected):
+ with pytest.raises(DefinitionSyntaxError):
Context.from_lines(["@context c", "[d1] = [d2] * [d3]"])
def test_err_prefix_redefinition():
- expected = re.escape("Expected <unit> = <converter>; got [d1] = [d2] * [d3]")
- with pytest.raises(DefinitionSyntaxError, match=expected):
+ with pytest.raises(DefinitionSyntaxError):
Context.from_lines(["@context c", "[d1] = [d2] * [d3]"])
def test_err_redefine_alias(subtests):
- expected = "Can't change a unit's symbol or aliases within a context"
+ expected = ".*can't change a unit's symbol or aliases within a context"
for s in ("foo = bar = f", "foo = bar = _ = baz"):
with subtests.test(s):
with pytest.raises(DefinitionSyntaxError, match=expected):
diff --git a/pint/testsuite/test_definitions.py b/pint/testsuite/test_definitions.py
index 8f5becd..fbf7450 100644
--- a/pint/testsuite/test_definitions.py
+++ b/pint/testsuite/test_definitions.py
@@ -35,7 +35,6 @@ class TestDefinition:
assert x.aliases == ()
assert x.converter.to_reference(1000) == 1
assert x.converter.from_reference(0.001) == 1
- assert str(x) == "m"
x = Definition.from_string("kilo- = 1e-3 = k-")
assert isinstance(x, PrefixDefinition)
@@ -161,7 +160,7 @@ class TestDefinition:
assert x.reference == UnitsContainer()
def test_dimension_definition(self):
- x = DimensionDefinition("[time]", "", (), None, is_base=True)
+ x = DimensionDefinition("[time]")
assert x.is_base
assert x.name == "[time]"
@@ -170,7 +169,7 @@ class TestDefinition:
assert x.reference == UnitsContainer({"[length]": 1, "[time]": -1})
def test_alias_definition(self):
- x = AliasDefinition.from_string("@alias meter = metro = metr")
+ x = Definition.from_string("@alias meter = metro = metr")
assert isinstance(x, AliasDefinition)
assert x.name == "meter"
assert x.aliases == ("metro", "metr")
diff --git a/pint/testsuite/test_diskcache.py b/pint/testsuite/test_diskcache.py
index 22f5e8f..399f9f7 100644
--- a/pint/testsuite/test_diskcache.py
+++ b/pint/testsuite/test_diskcache.py
@@ -5,8 +5,8 @@ import time
import pytest
import pint
+from pint._vendor import flexparser as fp
from pint.facets.plain import UnitDefinition
-from pint.parser import DefinitionFile
FS_SLEEP = 0.010
@@ -53,11 +53,11 @@ def test_decimal(tmp_path, float_cache_filename):
for p in files:
with p.open(mode="rb") as fi:
obj = pickle.load(fi)
- if not isinstance(obj, DefinitionFile):
+ if not isinstance(obj, fp.ParsedSource):
continue
- for lineno, adef in obj.filter_by(UnitDefinition):
- if adef.name == "pi":
- assert isinstance(adef.converter.scale, decimal.Decimal)
+ for definition in obj.parsed_source.filter_by(UnitDefinition):
+ if definition.name == "pi":
+ assert isinstance(definition.converter.scale, decimal.Decimal)
return
assert False
diff --git a/pint/testsuite/test_errors.py b/pint/testsuite/test_errors.py
index cc19bef..6a42eec 100644
--- a/pint/testsuite/test_errors.py
+++ b/pint/testsuite/test_errors.py
@@ -21,42 +21,10 @@ class TestErrors:
ex = DefinitionSyntaxError("foo")
assert str(ex) == "foo"
- # filename and lineno can be attached after init
- ex.filename = "a.txt"
- ex.lineno = 123
- assert str(ex) == "While opening a.txt, in line 123: foo"
-
- ex = DefinitionSyntaxError("foo", lineno=123)
- assert str(ex) == "In line 123: foo"
-
- ex = DefinitionSyntaxError("foo", filename="a.txt")
- assert str(ex) == "While opening a.txt: foo"
-
- ex = DefinitionSyntaxError("foo", filename="a.txt", lineno=123)
- assert str(ex) == "While opening a.txt, in line 123: foo"
-
def test_redefinition_error(self):
ex = RedefinitionError("foo", "bar")
assert str(ex) == "Cannot redefine 'foo' (bar)"
- # filename and lineno can be attached after init
- ex.filename = "a.txt"
- ex.lineno = 123
- assert (
- str(ex) == "While opening a.txt, in line 123: Cannot redefine 'foo' (bar)"
- )
-
- ex = RedefinitionError("foo", "bar", lineno=123)
- assert str(ex) == "In line 123: Cannot redefine 'foo' (bar)"
-
- ex = RedefinitionError("foo", "bar", filename="a.txt")
- assert str(ex) == "While opening a.txt: Cannot redefine 'foo' (bar)"
-
- ex = RedefinitionError("foo", "bar", filename="a.txt", lineno=123)
- assert (
- str(ex) == "While opening a.txt, in line 123: Cannot redefine 'foo' (bar)"
- )
-
with pytest.raises(PintError):
raise ex
@@ -149,7 +117,7 @@ class TestErrors:
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
for ex in [
- DefinitionSyntaxError("foo", filename="a.txt", lineno=123),
+ DefinitionSyntaxError("foo"),
RedefinitionError("foo", "bar"),
UndefinedUnitError("meter"),
DimensionalityError("a", "b", "c", "d", extra_msg=": msg"),
@@ -165,9 +133,11 @@ class TestErrors:
# assert False, ex.__reduce__()
ex2 = pickle.loads(pickle.dumps(ex, protocol))
+ print(ex)
+ print(ex2)
assert type(ex) is type(ex2)
- assert ex.args == ex2.args
- assert ex.__dict__ == ex2.__dict__
+ assert ex == ex
+ # assert ex.__dict__ == ex2.__dict__
assert str(ex) == str(ex2)
with pytest.raises(PintError):
diff --git a/pint/testsuite/test_formatting.py b/pint/testsuite/test_formatting.py
index d287948..48e770b 100644
--- a/pint/testsuite/test_formatting.py
+++ b/pint/testsuite/test_formatting.py
@@ -52,3 +52,18 @@ def test_split_format(format, default, flag, expected):
result = fmt.split_format(format, default, flag)
assert result == expected
+
+
+def test_register_unit_format(func_registry):
+ @fmt.register_unit_format("custom")
+ def format_custom(unit, registry, **options):
+ return "<formatted unit>"
+
+ quantity = 1.0 * func_registry.meter
+ assert f"{quantity:custom}" == "1.0 <formatted unit>"
+
+ with pytest.raises(ValueError, match="format 'custom' already exists"):
+
+ @fmt.register_unit_format("custom")
+ def format_custom_redefined(unit, registry, **options):
+ return "<overwritten>"
diff --git a/pint/testsuite/test_issues.py b/pint/testsuite/test_issues.py
index 51d33b1..a07e850 100644
--- a/pint/testsuite/test_issues.py
+++ b/pint/testsuite/test_issues.py
@@ -928,7 +928,7 @@ def test_issue1498(tmp_path):
f"""
foo = [FOO]
- @import {str(def2)}
+ @import {def2.name}
"""
)
@@ -941,9 +941,8 @@ def test_issue1498(tmp_path):
"""
)
- # Succeeds with pint 0.18; fails with pint 0.19
ureg1 = UnitRegistry()
- ureg1.load_definitions(def1) # ← FAILS
+ ureg1.load_definitions(def1)
assert 12.0 == ureg1("1.2 foo").to("kg", "BAR").magnitude
@@ -1009,3 +1008,30 @@ def test_issue1498b(tmp_path):
ureg1.load_definitions(def0) # ← FAILS
assert 12.0 == ureg1("1.2 foo").to("kg", "BAR").magnitude
+
+
+def test_backcompat_speed_velocity(func_registry):
+ get = func_registry.get_dimensionality
+ assert get("[velocity]") == UnitsContainer({"[length]": 1, "[time]": -1})
+ assert get("[speed]") == UnitsContainer({"[length]": 1, "[time]": -1})
+
+
+def test_issue1631():
+ import pint
+
+ # Test registry subclassing
+ class MyRegistry(pint.UnitRegistry):
+ pass
+
+ assert MyRegistry.Quantity is pint.UnitRegistry.Quantity
+ assert MyRegistry.Unit is pint.UnitRegistry.Unit
+
+ ureg = MyRegistry()
+
+ u = ureg.meter
+ assert isinstance(u, ureg.Unit)
+ assert isinstance(u, pint.Unit)
+
+ q = 2 * ureg.meter
+ assert isinstance(q, ureg.Quantity)
+ assert isinstance(q, pint.Quantity)
diff --git a/pint/testsuite/test_non_int.py b/pint/testsuite/test_non_int.py
index 5ca6c52..f616622 100644
--- a/pint/testsuite/test_non_int.py
+++ b/pint/testsuite/test_non_int.py
@@ -77,6 +77,27 @@ class _TestBasic(NonIntTypeTestCase):
== "Creating new PlainQuantity using a non unity PlainQuantity as units."
)
+ def test_nan_creation(self):
+ if self.SUPPORTS_NAN:
+ value = self.kwargs["non_int_type"]("nan")
+
+ for args in (
+ (value, "meter"),
+ (value, UnitsContainer(meter=1)),
+ (value, self.ureg.meter),
+ ("NaN*meter",),
+ ("nan/meter**(-1)",),
+ (self.Q_(value, "meter"),),
+ ):
+ x = self.Q_(*args)
+ assert math.isnan(x.magnitude)
+ assert type(x.magnitude) == self.kwargs["non_int_type"]
+ assert x.units == self.ureg.UnitsContainer(meter=1)
+
+ else:
+ with pytest.raises(ValueError):
+ self.Q_("NaN meters")
+
def test_quantity_comparison(self):
x = self.QP_("4.2", "meter")
y = self.QP_("4.2", "meter")
@@ -1137,6 +1158,7 @@ class _TestOffsetUnitMath(NonIntTypeTestCase):
class TestNonIntTypeQuantityFloat(_TestBasic):
kwargs = dict(non_int_type=float)
+ SUPPORTS_NAN = True
class TestNonIntTypeQuantityBasicMathFloat(_TestQuantityBasicMath):
@@ -1152,6 +1174,7 @@ class TestNonIntTypeOffsetUnitMathFloat(_TestOffsetUnitMath):
class TestNonIntTypeQuantityDecimal(_TestBasic):
kwargs = dict(non_int_type=Decimal)
+ SUPPORTS_NAN = True
class TestNonIntTypeQuantityBasicMathDecimal(_TestQuantityBasicMath):
@@ -1167,6 +1190,7 @@ class TestNonIntTypeOffsetUnitMathDecimal(_TestOffsetUnitMath):
class TestNonIntTypeQuantityFraction(_TestBasic):
kwargs = dict(non_int_type=Fraction)
+ SUPPORTS_NAN = False
class TestNonIntTypeQuantityBasicMathFraction(_TestQuantityBasicMath):
diff --git a/pint/testsuite/test_numpy.py b/pint/testsuite/test_numpy.py
index 4e178c6..ecc5157 100644
--- a/pint/testsuite/test_numpy.py
+++ b/pint/testsuite/test_numpy.py
@@ -82,7 +82,7 @@ class TestNumpyArrayManipulation(TestNumpyMethods):
# TODO
# https://www.numpy.org/devdocs/reference/routines.array-manipulation.html
# copyto
- # broadcast , broadcast_arrays
+ # broadcast
# asarray asanyarray asmatrix asfarray asfortranarray ascontiguousarray asarray_chkfinite asscalar require
# Changing array shape
@@ -271,6 +271,22 @@ class TestNumpyArrayManipulation(TestNumpyMethods):
def test_item(self):
helpers.assert_quantity_equal(self.Q_([[0]], "m").item(), 0 * self.ureg.m)
+ def test_broadcast_arrays(self):
+ x = self.Q_(np.array([[1, 2, 3]]), "m")
+ y = self.Q_(np.array([[4], [5]]), "nm")
+ result = np.broadcast_arrays(x, y)
+ expected = self.Q_(
+ [
+ [[1.0, 2.0, 3.0], [1.0, 2.0, 3.0]],
+ [[4e-09, 4e-09, 4e-09], [5e-09, 5e-09, 5e-09]],
+ ],
+ "m",
+ )
+ helpers.assert_quantity_equal(result, expected)
+
+ result = np.broadcast_arrays(x, y, subok=True)
+ helpers.assert_quantity_equal(result, expected)
+
class TestNumpyMathematicalFunctions(TestNumpyMethods):
# https://www.numpy.org/devdocs/reference/routines.math.html
diff --git a/pint/testsuite/test_quantity.py b/pint/testsuite/test_quantity.py
index c9b3fe9..6da4f34 100644
--- a/pint/testsuite/test_quantity.py
+++ b/pint/testsuite/test_quantity.py
@@ -1131,7 +1131,7 @@ class TestDimensions(QuantityTestCase):
assert get(UnitsContainer({"[time]": 1})) == UnitsContainer({"[time]": 1})
assert get("seconds") == UnitsContainer({"[time]": 1})
assert get(UnitsContainer({"seconds": 1})) == UnitsContainer({"[time]": 1})
- assert get("[speed]") == UnitsContainer({"[length]": 1, "[time]": -1})
+ assert get("[velocity]") == UnitsContainer({"[length]": 1, "[time]": -1})
assert get("[acceleration]") == UnitsContainer({"[length]": 1, "[time]": -2})
def test_dimensionality(self):
diff --git a/pint/testsuite/test_umath.py b/pint/testsuite/test_umath.py
index a3e69c7..6f32ab5 100644
--- a/pint/testsuite/test_umath.py
+++ b/pint/testsuite/test_umath.py
@@ -279,34 +279,43 @@ class TestMathUfuncs(TestUFuncs):
http://docs.scipy.org/doc/numpy/reference/ufuncs.html#math-operations
- add(x1, x2[, out]) Add arguments element-wise.
- subtract(x1, x2[, out]) Subtract arguments, element-wise.
- multiply(x1, x2[, out]) Multiply arguments element-wise.
- divide(x1, x2[, out]) Divide arguments element-wise.
- logaddexp(x1, x2[, out]) Logarithm of the sum of exponentiations of the inputs.
- logaddexp2(x1, x2[, out]) Logarithm of the sum of exponentiations of the inputs in plain-2.
- true_divide(x1, x2[, out]) Returns a true division of the inputs, element-wise.
- floor_divide(x1, x2[, out]) Return the largest integer smaller or equal to the division of the inputs.
- negative(x[, out]) Returns an array with the negative of each element of the original array.
- power(x1, x2[, out]) First array elements raised to powers from second array, element-wise. NOT IMPLEMENTED
- remainder(x1, x2[, out]) Return element-wise remainder of division.
- mod(x1, x2[, out]) Return element-wise remainder of division.
- fmod(x1, x2[, out]) Return the element-wise remainder of division.
- absolute(x[, out]) Calculate the absolute value element-wise.
- rint(x[, out]) Round elements of the array to the nearest integer.
- sign(x[, out]) Returns an element-wise indication of the sign of a number.
- conj(x[, out]) Return the complex conjugate, element-wise.
- exp(x[, out]) Calculate the exponential of all elements in the input array.
- exp2(x[, out]) Calculate 2**p for all p in the input array.
- log(x[, out]) Natural logarithm, element-wise.
- log2(x[, out]) Base-2 logarithm of x.
- log10(x[, out]) Return the plain 10 logarithm of the input array, element-wise.
- expm1(x[, out]) Calculate exp(x) - 1 for all elements in the array.
- log1p(x[, out]) Return the natural logarithm of one plus the input array, element-wise.
- sqrt(x[, out]) Return the positive square-root of an array, element-wise.
- square(x[, out]) Return the element-wise square of the input.
- reciprocal(x[, out]) Return the reciprocal of the argument, element-wise.
- ones_like(x[, out]) Returns an array of ones with the same shape and type as a given array.
+ add(x1, x2, /[, out, where, casting, order, ...] Add arguments element-wise.
+ subtract(x1, x2, /[, out, where, casting, ...] Subtract arguments, element-wise.
+ multiply(x1, x2, /[, out, where, casting, ...] Multiply arguments element-wise.
+ matmul(x1, x2, /[, out, casting, order, ...] Matrix product of two arrays.
+ divide(x1, x2, /[, out, where, casting, ...] Divide arguments element-wise.
+ logaddexp(x1, x2, /[, out, where, casting, ...] Logarithm of the sum of exponentiations of the inputs.
+ logaddexp2(x1, x2, /[, out, where, casting, ...] Logarithm of the sum of exponentiations of the inputs in base-2.
+ true_divide(x1, x2, /[, out, where, ...] Divide arguments element-wise.
+ floor_divide(x1, x2, /[, out, where, ...] Return the largest integer smaller or equal to the division of the inputs.
+ negative(x, /[, out, where, casting, order, ...] Numerical negative, element-wise.
+ positive(x, /[, out, where, casting, order, ...] Numerical positive, element-wise.
+ power(x1, x2, /[, out, where, casting, ...] First array elements raised to powers from second array, element-wise.
+ float_power(x1, x2, /[, out, where, ...] First array elements raised to powers from second array, element-wise.
+ remainder(x1, x2, /[, out, where, casting, ...] Returns the element-wise remainder of division.
+ mod(x1, x2, /[, out, where, casting, order, ...] Returns the element-wise remainder of division.
+ fmod(x1, x2, /[, out, where, casting, ...] Returns the element-wise remainder of division.
+ divmod(x1, x2[, out1, out2], / [[, out, ...] Return element-wise quotient and remainder simultaneously.
+ absolute(x, /[, out, where, casting, order, ...] Calculate the absolute value element-wise.
+ fabs(x, /[, out, where, casting, order, ...] Compute the absolute values element-wise.
+ rint(x, /[, out, where, casting, order, ...] Round elements of the array to the nearest integer.
+ sign(x, /[, out, where, casting, order, ...] Returns an element-wise indication of the sign of a number.
+ heaviside(x1, x2, /[, out, where, casting, ...] Compute the Heaviside step function.
+ conj(x, /[, out, where, casting, order, ...] Return the complex conjugate, element-wise.
+ conjugate(x, /[, out, where, casting, ...] Return the complex conjugate, element-wise.
+ exp(x, /[, out, where, casting, order, ...] Calculate the exponential of all elements in the input array.
+ exp2(x, /[, out, where, casting, order, ...] Calculate 2**p for all p in the input array.
+ log(x, /[, out, where, casting, order, ...] Natural logarithm, element-wise.
+ log2(x, /[, out, where, casting, order, ...] Base-2 logarithm of x.
+ log10(x, /[, out, where, casting, order, ...] Return the base 10 logarithm of the input array, element-wise.
+ expm1(x, /[, out, where, casting, order, ...] Calculate exp(x) - 1 for all elements in the array.
+ log1p(x, /[, out, where, casting, order, ...] Return the natural logarithm of one plus the input array, element-wise.
+ sqrt(x, /[, out, where, casting, order, ...] Return the non-negative square-root of an array, element-wise.
+ square(x, /[, out, where, casting, order, ...] Return the element-wise square of the input.
+ cbrt(x, /[, out, where, casting, order, ...] Return the cube-root of an array, element-wise.
+ reciprocal(x, /[, out, where, casting, ...] Return the reciprocal of the argument, element-wise.
+ gcd(x1, x2, /[, out, where, casting, order, ...] Returns the greatest common divisor of |x1| and |x2|
+ lcm(x1, x2, /[, out, where, casting, order, ...] Returns the lowest common multiple of |x1| and |x2|
Parameters
----------
@@ -364,6 +373,9 @@ class TestMathUfuncs(TestUFuncs):
def test_negative(self):
self._test1(np.negative, (self.qless, self.q1), ())
+ def test_positive(self):
+ self._test1(np.positive, (self.qless, self.q1), ())
+
def test_remainder(self):
self._test2(
np.remainder,
diff --git a/pint/testsuite/test_unit.py b/pint/testsuite/test_unit.py
index b3a6219..96db871 100644
--- a/pint/testsuite/test_unit.py
+++ b/pint/testsuite/test_unit.py
@@ -7,12 +7,7 @@ from contextlib import nullcontext as does_not_raise
import pytest
-from pint import (
- DefinitionSyntaxError,
- DimensionalityError,
- RedefinitionError,
- UndefinedUnitError,
-)
+from pint import DimensionalityError, RedefinitionError, UndefinedUnitError, errors
from pint.compat import np
from pint.registry import LazyRegistry, UnitRegistry
from pint.testsuite import QuantityTestCase, assert_no_warnings, helpers
@@ -260,9 +255,9 @@ class TestRegistry(QuantityTestCase):
cls.ureg.autoconvert_offset_to_baseunit = False
def test_base(self):
- ureg = UnitRegistry(None)
+ ureg = UnitRegistry(None, on_redefinition="raise")
ureg.define("meter = [length]")
- with pytest.raises(DefinitionSyntaxError):
+ with pytest.raises(errors.RedefinitionError):
ureg.define("meter = [length]")
with pytest.raises(TypeError):
ureg.define(list())
@@ -282,7 +277,7 @@ class TestRegistry(QuantityTestCase):
ureg1 = UnitRegistry()
ureg2 = UnitRegistry(data)
assert dir(ureg1) == dir(ureg2)
- with pytest.raises(ValueError):
+ with pytest.raises(FileNotFoundError):
UnitRegistry(None).load_definitions("notexisting")
def test_default_format(self):
@@ -630,7 +625,7 @@ class TestRegistry(QuantityTestCase):
assert g0(6, 2) == 3
assert g0(6 * ureg.parsec, 2) == 3 * ureg.parsec
- g1 = ureg.check("[speed]", "[time]")(gfunc)
+ g1 = ureg.check("[velocity]", "[time]")(gfunc)
with pytest.raises(DimensionalityError):
g1(3.0, 1)
with pytest.raises(DimensionalityError):
@@ -643,9 +638,9 @@ class TestRegistry(QuantityTestCase):
)
with pytest.raises(TypeError):
- ureg.check("[speed]")(gfunc)
+ ureg.check("[velocity]")(gfunc)
with pytest.raises(TypeError):
- ureg.check("[speed]", "[time]", "[mass]")(gfunc)
+ ureg.check("[velocity]", "[time]", "[mass]")(gfunc)
def test_to_ref_vs_to(self):
self.ureg.autoconvert_offset_to_baseunit = True
@@ -668,7 +663,7 @@ class TestRegistry(QuantityTestCase):
with caplog.at_level(logging.DEBUG):
d("meter = [fruits]")
d("kilo- = 1000")
- d("[speed] = [vegetables]")
+ d("[velocity] = [vegetables]")
# aliases
d("bla = 3.2 meter = inch")
@@ -875,14 +870,14 @@ class TestRegistryWithDefaultRegistry(TestRegistry):
def test_redefinition(self):
d = self.ureg.define
- with pytest.raises(DefinitionSyntaxError):
+ with pytest.raises(RedefinitionError):
d("meter = [time]")
with pytest.raises(RedefinitionError):
d("meter = [newdim]")
with pytest.raises(RedefinitionError):
d("kilo- = 1000")
with pytest.raises(RedefinitionError):
- d("[speed] = [length]")
+ d("[velocity] = [length]")
# aliases
assert "inch" in self.ureg._units
diff --git a/pint/util.py b/pint/util.py
index 54a7755..3d00175 100644
--- a/pint/util.py
+++ b/pint/util.py
@@ -10,6 +10,7 @@
from __future__ import annotations
+import functools
import inspect
import logging
import math
@@ -641,7 +642,7 @@ class ParserHelper(UnitsContainer):
for k in list(ret):
if k.lower() == "nan":
del ret._d[k]
- ret.scale = math.nan
+ ret.scale = non_int_type(math.nan)
return ret
@@ -980,80 +981,6 @@ def getattr_maybe_raise(self, item):
raise AttributeError("%r object has no attribute %r" % (self, item))
-class SourceIterator:
- """Iterator to facilitate reading the definition files.
-
- Accepts any sequence (like a list of lines, a file or another SourceIterator)
-
- The iterator yields the line number and line (skipping comments and empty lines)
- and stripping white spaces.
-
- for lineno, line in SourceIterator(sequence):
- # do something here
-
- """
-
- def __new__(cls, sequence, filename=None, is_resource=False):
- if isinstance(sequence, SourceIterator):
- return sequence
-
- obj = object.__new__(cls)
-
- if sequence is not None:
- obj.internal = enumerate(sequence, 1)
- obj.last = (None, None)
- obj.filename = filename or getattr(sequence, "name", None)
- obj.is_resource = is_resource
-
- return obj
-
- def __iter__(self):
- return self
-
- def __next__(self):
- line = ""
- while not line or line.startswith("#"):
- lineno, line = next(self.internal)
- line = line.split("#", 1)[0].strip()
-
- self.last = lineno, line
- return lineno, line
-
- next = __next__
-
- def block_iter(self):
- """Iterate block including header."""
- return BlockIterator(self)
-
-
-class BlockIterator(SourceIterator):
- """Like SourceIterator but stops when it finds '@end'
- It also raises an error if another '@' directive is found inside.
- """
-
- def __new__(cls, line_iterator):
- obj = SourceIterator.__new__(cls, None)
- obj.internal = line_iterator.internal
- obj.last = line_iterator.last
- obj.done_last = False
- return obj
-
- def __next__(self):
- if not self.done_last:
- self.done_last = True
- return self.last
-
- lineno, line = SourceIterator.__next__(self)
- if line.startswith("@end"):
- raise StopIteration
- elif line.startswith("@"):
- raise DefinitionSyntaxError("cannot nest @ directives", lineno=lineno)
-
- return lineno, line
-
- next = __next__
-
-
def iterable(y) -> bool:
"""Check whether or not an object can be iterated over.
@@ -1095,6 +1022,13 @@ def sized(y) -> bool:
return True
+@functools.lru_cache(
+ maxsize=None
+) # TODO: replace with cache when Python 3.8 is dropped.
+def _build_type(class_name: str, bases):
+ return type(class_name, bases, dict())
+
+
def build_dependent_class(registry_class, class_name: str, attribute_name: str) -> Type:
"""Creates a class specifically for the given registry that
subclass all the classes named by the registry bases in a
@@ -1110,9 +1044,10 @@ def build_dependent_class(registry_class, class_name: str, attribute_name: str)
for base in inspect.getmro(registry_class)
if attribute_name in base.__dict__
)
- bases = dict.fromkeys(bases, None)
- newcls = type(class_name, tuple(bases.keys()), dict())
- return newcls
+ bases = tuple(dict.fromkeys(bases, None).keys())
+ if len(bases) == 1 and bases[0].__name__ == class_name:
+ return bases[0]
+ return _build_type(class_name, bases)
def create_class_with_registry(registry, base_class) -> Type: