Skip to content

Commit

Permalink
WIP : pint typing
Browse files Browse the repository at this point in the history
- Add Quantity as Generic
- Type quantity.py
- Type registry.py
- create dynamic class Quantity through classmethod
- Infer magnitude type through constructor
  • Loading branch information
jules-ch committed Mar 14, 2021
1 parent dd63fbd commit b6cad1d
Show file tree
Hide file tree
Showing 10 changed files with 487 additions and 264 deletions.
13 changes: 13 additions & 0 deletions pint/_typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from typing import TYPE_CHECKING, Any, Callable, Tuple, TypeVar, Union

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

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

FuncType = Callable[..., Any]
F = TypeVar("F", bound=FuncType)

ScalarLike = TypeVar("ScalarLike", float, int, complex)
Shape = Tuple[int, ...]
32 changes: 19 additions & 13 deletions pint/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
import re
import weakref
from collections import ChainMap, defaultdict
from typing import Dict, Hashable, Iterable, List, Optional, Tuple

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

#: Regex to match the header parts of a context.
_header_re = re.compile(
Expand Down Expand Up @@ -84,7 +85,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: Iterable[str] = (),
defaults: Dict[str, float] = None,
) -> None:

self.name = name
self.aliases = aliases
Expand All @@ -96,7 +102,7 @@ def __init__(self, name=None, aliases=(), defaults=None):
self.defaults = defaults or {}

# Store Definition objects that are context-specific
self.redefinitions = []
self.redefinitions: List[Definition] = []

# Flag set to True by the Registry the first time the context is enabled
self.checked = False
Expand All @@ -106,7 +112,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 @@ -223,22 +229,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 +276,7 @@ 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[Hashable, ...]:
"""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 All @@ -293,13 +299,13 @@ class ContextChain(ChainMap):
to transform from one dimension to another.
"""

def __init__(self):
def __init__(self) -> None:
super().__init__()
self.contexts = []
self.contexts: List[Context] = []
self.maps.clear() # Remove default empty map
self._graph = None

def insert_contexts(self, *contexts):
def insert_contexts(self, *contexts: Context) -> None:
"""Insert one or more contexts in reversed order the chained map.
(A rule in last context will take precedence)
Expand All @@ -311,7 +317,7 @@ def insert_contexts(self, *contexts):
self.maps = [ctx.relation_to_context for ctx in reversed(contexts)] + self.maps
self._graph = None

def remove_contexts(self, n: int = None):
def remove_contexts(self, n: Optional[int] = None) -> None:
"""Remove the last n inserted contexts from the chain.
Parameters
Expand Down Expand Up @@ -345,7 +351,7 @@ def transform(self, src, dst, registry, value):
"""
return self[(src, dst)].transform(src, dst, registry, value)

def hashable(self):
def hashable(self) -> Tuple[Hashable, ...]:
"""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
8 changes: 4 additions & 4 deletions pint/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ class Converter:
"""Base class for value converters."""

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

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

def to_reference(self, value, inplace=False):
Expand Down Expand Up @@ -116,11 +116,11 @@ def __init__(self, scale, logbase, logfactor):
self.logfactor = logfactor

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

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

def from_reference(self, value, inplace=False):
Expand Down
77 changes: 55 additions & 22 deletions pint/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
"""

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 +43,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 +65,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 +104,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 +119,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 +159,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 +197,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 +237,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 +326,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[Union[Callable, 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 +381,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
6 changes: 3 additions & 3 deletions pint/formatting.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,9 @@ def _pretty_fmt_exponent(num):


def formatter(
items,
as_ratio=True,
single_denominator=False,
items: list,
as_ratio: bool = True,
single_denominator: bool = False,
product_fmt=" * ",
division_fmt=" / ",
power_fmt="{} ** {}",
Expand Down
Loading

0 comments on commit b6cad1d

Please sign in to comment.