Skip to content

Commit

Permalink
Add pint typing module
Browse files Browse the repository at this point in the history
- Quantity as Generic class
- Add overloaded signature for __new__ Quantity
- Add typing module as private
- Add py.typed for PEP561 supports
- Add overloaded signature for __new__ Quantity
- Quantity as Generic class
- Add type hints throughout the project
- Add py.typed in package data in setup.cfg
- Add type hints for decorators
- Add type hints for public API of registry.py
- Add type hints for units.py
  • Loading branch information
jules-ch committed Jul 25, 2021
1 parent abe3840 commit 727fc87
Show file tree
Hide file tree
Showing 13 changed files with 529 additions and 230 deletions.
16 changes: 16 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
[run]
omit = pint/testsuite/*

[report]
# Regexes for lines to exclude from consideration
exclude_lines =
# Have to re-enable the standard pragma
pragma: no cover

# Don't complain about missing debug-only code:
def __repr__

# Don't complain if tests don't hit defensive assertion code:
raise AssertionError
raise NotImplementedError
AbstractMethodError

# Don't complain if non-runnable code isn't run:
if TYPE_CHECKING:
2 changes: 2 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Pint Changelog

- pint no longer supports Python 3.6
- Minimum Numpy version supported is 1.17+
- Add supports for type hints for Quantity class. Quantity is now a Generic (PEP560).
- Add support for [PEP561](https://www.python.org/dev/peps/pep-0561/) (Package Type information)


0.17 (2021-03-22)
Expand Down
18 changes: 18 additions & 0 deletions pint/_typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import TYPE_CHECKING, Any, Callable, Tuple, TypeVar, Union

if TYPE_CHECKING:
from .quantity import Quantity
from .unit import Unit
from .util import UnitsContainer

UnitLike = Union[str, "UnitsContainer", "Unit"]

QuantityOrUnitLike = Union["Quantity", UnitLike]

Shape = Tuple[int, ...]

_MagnitudeType = TypeVar("_MagnitudeType")
S = TypeVar("S")

FuncType = Callable[..., Any]
F = TypeVar("F", bound=FuncType)
33 changes: 24 additions & 9 deletions pint/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@
:license: BSD, see LICENSE for more details.
"""

from __future__ import annotations

import re
import weakref
from collections import ChainMap, defaultdict
from typing import TYPE_CHECKING, Any, Callable, Optional, Tuple

from .definitions import Definition, UnitDefinition
from .errors import DefinitionSyntaxError
from .util import ParserHelper, SourceIterator, to_units_container

if TYPE_CHECKING:
from .quantity import Quantity
from .registry import UnitRegistry
from .util import UnitsContainer

#: Regex to match the header parts of a context.
_header_re = re.compile(
r"@context\s*(?P<defaults>\(.*\))?\s+(?P<name>\w+)\s*(=(?P<aliases>.*))*"
Expand All @@ -25,8 +33,8 @@
_varname_re = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")


def _expression_to_function(eq):
def func(ureg, value, **kwargs):
def _expression_to_function(eq: str) -> Callable[..., Quantity[Any]]:
def func(ureg: UnitRegistry, value: Any, **kwargs: Any) -> Quantity[Any]:
return ureg.parse_expression(eq, value=value, **kwargs)

return func
Expand Down Expand Up @@ -84,7 +92,12 @@ class Context:
>>> c.redefine("pound = 0.5 kg")
"""

def __init__(self, name=None, aliases=(), defaults=None):
def __init__(
self,
name: Optional[str] = None,
aliases: Tuple[str, ...] = (),
defaults: Optional[dict] = None,
) -> None:

self.name = name
self.aliases = aliases
Expand All @@ -106,7 +119,7 @@ def __init__(self, name=None, aliases=(), defaults=None):
self.relation_to_context = weakref.WeakValueDictionary()

@classmethod
def from_context(cls, context, **defaults):
def from_context(cls, context: Context, **defaults) -> Context:
"""Creates a new context that shares the funcs dictionary with the
original context. The default values are copied from the original
context and updated with the new defaults.
Expand Down Expand Up @@ -135,7 +148,7 @@ def from_context(cls, context, **defaults):
return context

@classmethod
def from_lines(cls, lines, to_base_func=None, non_int_type=float):
def from_lines(cls, lines, to_base_func=None, non_int_type=float) -> Context:
lines = SourceIterator(lines)

lineno, header = next(lines)
Expand Down Expand Up @@ -223,22 +236,22 @@ def to_num(val):

return ctx

def add_transformation(self, src, dst, func):
def add_transformation(self, src, dst, func) -> None:
"""Add a transformation function to the context."""

_key = self.__keytransform__(src, dst)
self.funcs[_key] = func
self.relation_to_context[_key] = self

def remove_transformation(self, src, dst):
def remove_transformation(self, src, dst) -> None:
"""Add a transformation function to the context."""

_key = self.__keytransform__(src, dst)
del self.funcs[_key]
del self.relation_to_context[_key]

@staticmethod
def __keytransform__(src, dst):
def __keytransform__(src, dst) -> Tuple[UnitsContainer, UnitsContainer]:
return to_units_container(src), to_units_container(dst)

def transform(self, src, dst, registry, value):
Expand Down Expand Up @@ -270,7 +283,9 @@ def redefine(self, definition: str) -> None:
raise DefinitionSyntaxError("Can't define base units within a context")
self.redefinitions.append(d)

def hashable(self):
def hashable(
self,
) -> Tuple[Optional[str], Tuple[str, ...], frozenset, frozenset, tuple]:
"""Generate a unique hashable and comparable representation of self, which can
be used as a key in a dict. This class cannot define ``__hash__`` because it is
mutable, and the Python interpreter does cache the output of ``__hash__``.
Expand Down
79 changes: 57 additions & 22 deletions pint/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
:license: BSD, see LICENSE for more details.
"""

from __future__ import annotations

from collections import namedtuple
from typing import Callable, Iterable, Optional, Union

from .converters import LogarithmicConverter, OffsetConverter, ScaleConverter
from .converters import Converter, LogarithmicConverter, OffsetConverter, ScaleConverter
from .errors import DefinitionSyntaxError
from .util import ParserHelper, UnitsContainer, _is_dim

Expand Down Expand Up @@ -42,7 +45,7 @@ class PreprocessedDefinition(
"""

@classmethod
def from_string(cls, definition):
def from_string(cls, definition: str) -> PreprocessedDefinition:
name, definition = definition.split("=", 1)
name = name.strip()

Expand All @@ -64,7 +67,7 @@ def __init__(self, value):
self.value = value


def numeric_parse(s, non_int_type=float):
def numeric_parse(s: str, non_int_type: type = float):
"""Try parse a string into a number (without using eval).
Parameters
Expand Down Expand Up @@ -103,7 +106,13 @@ class Definition:
converter : callable or Converter or None
"""

def __init__(self, name, symbol, aliases, converter):
def __init__(
self,
name: str,
symbol: Optional[str],
aliases: Iterable[str],
converter: Optional[Union[Callable, Converter]],
):

if isinstance(converter, str):
raise TypeError(
Expand All @@ -112,19 +121,21 @@ def __init__(self, name, symbol, aliases, converter):

self._name = name
self._symbol = symbol
self._aliases = aliases
self._aliases = tuple(aliases)
self._converter = converter

@property
def is_multiplicative(self):
def is_multiplicative(self) -> bool:
return self._converter.is_multiplicative

@property
def is_logarithmic(self):
def is_logarithmic(self) -> bool:
return self._converter.is_logarithmic

@classmethod
def from_string(cls, definition, non_int_type=float):
def from_string(
cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
) -> "Definition":
"""Parse a definition.
Parameters
Expand All @@ -150,30 +161,30 @@ def from_string(cls, definition, non_int_type=float):
return UnitDefinition.from_string(definition, non_int_type)

@property
def name(self):
def name(self) -> str:
return self._name

@property
def symbol(self):
def symbol(self) -> str:
return self._symbol or self._name

@property
def has_symbol(self):
def has_symbol(self) -> bool:
return bool(self._symbol)

@property
def aliases(self):
def aliases(self) -> Iterable[str]:
return self._aliases

def add_aliases(self, *alias):
def add_aliases(self, *alias: str) -> None:
alias = tuple(a for a in alias if a not in self._aliases)
self._aliases = self._aliases + alias

@property
def converter(self):
def converter(self) -> Converter:
return self._converter

def __str__(self):
def __str__(self) -> str:
return self.name


Expand All @@ -188,7 +199,9 @@ class PrefixDefinition(Definition):
"""

@classmethod
def from_string(cls, definition, non_int_type=float):
def from_string(
cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
) -> "PrefixDefinition":
if isinstance(definition, str):
definition = PreprocessedDefinition.from_string(definition)

Expand Down Expand Up @@ -226,14 +239,24 @@ class UnitDefinition(Definition):
"""

def __init__(self, name, symbol, aliases, converter, reference=None, is_base=False):
def __init__(
self,
name: str,
symbol: Optional[str],
aliases: Iterable[str],
converter: Converter,
reference: Optional[UnitsContainer] = None,
is_base: bool = False,
) -> None:
self.reference = reference
self.is_base = is_base

super().__init__(name, symbol, aliases, converter)

@classmethod
def from_string(cls, definition, non_int_type=float):
def from_string(
cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
) -> "UnitDefinition":
if isinstance(definition, str):
definition = PreprocessedDefinition.from_string(definition)

Expand Down Expand Up @@ -305,14 +328,24 @@ class DimensionDefinition(Definition):
[density] = [mass] / [volume]
"""

def __init__(self, name, symbol, aliases, converter, reference=None, is_base=False):
def __init__(
self,
name: str,
symbol: Optional[str],
aliases: Iterable[str],
converter: Optional[Converter],
reference: Optional[UnitsContainer] = None,
is_base: bool = False,
) -> None:
self.reference = reference
self.is_base = is_base

super().__init__(name, symbol, aliases, converter=None)

@classmethod
def from_string(cls, definition, non_int_type=float):
def from_string(
cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
) -> "DimensionDefinition":
if isinstance(definition, str):
definition = PreprocessedDefinition.from_string(definition)

Expand Down Expand Up @@ -350,11 +383,13 @@ class AliasDefinition(Definition):
@alias meter = my_meter
"""

def __init__(self, name, aliases):
def __init__(self, name: str, aliases: Iterable[str]) -> None:
super().__init__(name=name, symbol=None, aliases=aliases, converter=None)

@classmethod
def from_string(cls, definition, non_int_type=float):
def from_string(
cls, definition: Union[str, PreprocessedDefinition], non_int_type: type = float
) -> AliasDefinition:

if isinstance(definition, str):
definition = PreprocessedDefinition.from_string(definition)
Expand Down
3 changes: 2 additions & 1 deletion pint/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"""

import re
from typing import Dict

from .babel_names import _babel_lengths, _babel_units
from .compat import babel_parse
Expand Down Expand Up @@ -71,7 +72,7 @@ def _pretty_fmt_exponent(num):

#: _FORMATS maps format specifications to the corresponding argument set to
#: formatter().
_FORMATS = {
_FORMATS: Dict[str, dict] = {
"P": { # Pretty format.
"as_ratio": True,
"single_denominator": False,
Expand Down
Empty file added pint/py.typed
Empty file.
Loading

0 comments on commit 727fc87

Please sign in to comment.