Skip to content

Commit

Permalink
Address review comments
Browse files Browse the repository at this point in the history
Signed-off-by: Nathaniel Starkman (@nstarman) <nstarkman@protonmail.com>
  • Loading branch information
nstarman committed Oct 29, 2021
1 parent 55aef2e commit e6d147f
Show file tree
Hide file tree
Showing 13 changed files with 111 additions and 278 deletions.
15 changes: 15 additions & 0 deletions astropy/units/_typing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Support for ``typing`` py3.9+ features while min version is py3.8.
"""

try: # py 3.9+
from typing import Annotated
except (ImportError, ModuleNotFoundError): # optional dependency
try:
from typing_extensions import Annotated
except (ImportError, ModuleNotFoundError):

Annotated = NotImplemented

HAS_ANNOTATED = Annotated is not NotImplemented
36 changes: 1 addition & 35 deletions astropy/units/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
'set_enabled_units', 'add_enabled_units',
'set_enabled_equivalencies', 'add_enabled_equivalencies',
'set_enabled_aliases', 'add_enabled_aliases',
'dimensionless_unscaled', 'one', "is_unitlike",
'dimensionless_unscaled', 'one',
]

UNITY = 1.0
Expand Down Expand Up @@ -2369,40 +2369,6 @@ def is_unity(self):
return len(unit.bases) == 0 and unit.scale == 1.0


def is_unitlike(target, allow_structured=True):
"""Check if target is `~astropy.units.Unit`-like.
Parameters
----------
target : Any
allow_structured : bool
Whether to count a `~astropy.units.StructuredUnit` as a
`~astropy.units.Unit`, allowing for a distinction between a list of
units and a structured unit.
Returns
-------
`~astropy.units.Unit` or `False`
Unit if target is `~astropy.units.Unit`-like, False otherwise.
"""
# check if unit-like
try:
unit = Unit(target)
except (TypeError, ValueError):
return False

# distinguish between list of units and a structured unit
ulike = True
if not allow_structured:
from astropy.units.structured import StructuredUnit
# don't import unless really needed

if isinstance(unit, StructuredUnit):
ulike = False

return unit if ulike else False


si_prefixes = [
(['Y'], ['yotta'], 1e24),
(['Z'], ['zetta'], 1e21),
Expand Down
44 changes: 28 additions & 16 deletions astropy/units/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@

import numpy as np

from .core import (Unit, UnitBase, UnitsError, add_enabled_equivalencies,
dimensionless_unscaled, is_unitlike)
from ._typing import Annotated
from .core import (Unit, UnitBase, UnitsError,
add_enabled_equivalencies, dimensionless_unscaled)
from .function.core import FunctionUnitBase
from .physical import PhysicalType, is_physicaltypelike
from .quantity import Quantity, HAS_ANNOTATED, Annotated
from .physical import PhysicalType, get_physical_type
from .quantity import Quantity
from .structured import StructuredUnit


NoneType = type(None)
Expand All @@ -28,12 +30,14 @@ def _get_allowed_units(targets):
"""
allowed_units = []
for target in targets:
if unit := is_unitlike(target, True):
pass
elif ptype := is_physicaltypelike(target):
unit = ptype._unit
else:
raise ValueError(f"Invalid unit or physical type '{target}'.")

try:
unit = Unit(target)
except (TypeError, ValueError):
try:
unit = get_physical_type(target)._unit
except (TypeError, ValueError, KeyError): # KeyError for Enum
raise ValueError(f"Invalid unit or physical type {target!r}.") from None

allowed_units.append(unit)

Expand Down Expand Up @@ -95,12 +99,20 @@ def _parse_annotation(target):

if target in (None, NoneType, inspect._empty):
return target
elif unit := is_unitlike(target, allow_structured=False):
return unit
elif unit := is_physicaltypelike(target):

# check if unit-like
try:
unit = Unit(target)
except (TypeError, ValueError):
try:
ptype = get_physical_type(target)
except (TypeError, ValueError, KeyError): # KeyError for Enum
if isinstance(target, str):
raise ValueError(f"invalid unit or physical type {target!r}.") from None
else:
return ptype
else:
return unit
elif isinstance(target, str):
raise ValueError(f"Invalid unit or physical type '{target}'.")

# could be a type hint
origin = T.get_origin(target)
Expand Down Expand Up @@ -305,7 +317,7 @@ def wrapper(*func_args, **func_kwargs):
_validate_arg_value("return", wrapped_function.__name__,
return_, valid_targets, self.equivalencies,
self.strict_dimensionless)
if len(valid_targets) == 1 and isinstance(valid_targets[0], UnitBase):
if len(valid_targets) > 0:
return_ <<= valid_targets[0]
return return_

Expand Down
28 changes: 1 addition & 27 deletions astropy/units/physical.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
from . import quantity
from astropy.utils.exceptions import AstropyDeprecationWarning

__all__ = ["def_physical_type", "get_physical_type", "PhysicalType",
"is_physicaltypelike"]
__all__ = ["def_physical_type", "get_physical_type", "PhysicalType"]

_units_and_physical_types = [
(core.dimensionless_unscaled, "dimensionless"),
Expand Down Expand Up @@ -551,31 +550,6 @@ def get_physical_type(obj):
return PhysicalType(unit, "unknown")


def is_physicaltypelike(target):
"""Check if target is `~astropy.units.PhysicalType`-like.
Parameters
----------
target : Any
recognized types are:
- `~astropy.units.PhysicalType`
- `str`
- `~astropy.units.Unit`
- `~astropy.units.Quantity`
Returns
-------
`~astropy.units.PhysicalType` or `False`
PhysicalType if target is `~astropy.units.PhysicalType`-like,
False otherwise.
"""
try:
ptype = get_physical_type(target)
except (TypeError, ValueError, KeyError): # KeyError for Enum
return False
return ptype


# ------------------------------------------------------------------------------
# Script section creating the physical types and the documentation

Expand Down
44 changes: 19 additions & 25 deletions astropy/units/quantity.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@

# AstroPy
from .core import (Unit, dimensionless_unscaled, get_current_unit_registry,
UnitBase, UnitsError, UnitConversionError, UnitTypeError,
is_unitlike)
UnitBase, UnitsError, UnitConversionError, UnitTypeError)
from .structured import StructuredUnit
from .utils import is_effectively_unity
from .format.latex import Latex
from astropy.utils.compat import NUMPY_LT_1_22
from astropy.utils.compat.misc import override__dir__
from astropy.utils.exceptions import AstropyDeprecationWarning, AstropyWarning
from astropy.utils.introspection import minversion
Expand All @@ -35,20 +35,6 @@
SUBCLASS_SAFE_FUNCTIONS, FUNCTION_HELPERS, DISPATCHED_FUNCTIONS,
UNSUPPORTED_FUNCTIONS)

HAS_ANNOTATED = True # assume, then check
try: # py 3.9+
from typing import Annotated
except (ImportError, ModuleNotFoundError): # optional dependency
try:
from typing_extensions import Annotated
except (ImportError, ModuleNotFoundError):
HAS_ANNOTATED = False

Annotated = NotImplemented

warnings.warn("Python is not v3.9+ and the package `typing_extensions`"
" is not installed. Quantity annotations will not work.")


__all__ = ["Quantity", "SpecificTypeQuantity",
"QuantityInfoBase", "QuantityInfo", "allclose", "isclose"]
Expand Down Expand Up @@ -357,9 +343,9 @@ def __class_getitem__(cls, unit_shape_dtype):
-------
`typing.Annotated`, `typing_extensions.Annotated`, `astropy.units.Unit`, or `astropy.units.PhysicalType`
Return type in this preference order:
`typing.Annotated` if python v3.9+; `typing_extensions.Annotated`
if :mod:`typing_extensions` is installed, a `astropy.units.Unit`
or `astropy.units.PhysicalType` else-wise, depending on the input.
* if python v3.9+ : `typing.Annotated`
* if :mod:`typing_extensions` is installed : `typing_extensions.Annotated`
* `astropy.units.Unit` or `astropy.units.PhysicalType`
Raises
------
Expand All @@ -385,7 +371,7 @@ def __class_getitem__(cls, unit_shape_dtype):
static-type compatible.
"""
# LOCAL
from .physical import is_physicaltypelike
from ._typing import HAS_ANNOTATED, Annotated

# process whether [unit] or [unit, shape, ptype]
if isinstance(unit_shape_dtype, tuple): # unit, shape, dtype
Expand All @@ -396,19 +382,27 @@ def __class_getitem__(cls, unit_shape_dtype):
shape_dtype = ()

# Allowed unit/physical types. Errors if neither.
unit = x if (x := is_unitlike(target, True)) else is_physicaltypelike(target)
if not unit:
raise TypeError("target is not a Unit or PhysicalType")
try:
unit = Unit(target)
except (TypeError, ValueError):
from astropy.units.physical import get_physical_type

try:
unit = get_physical_type(target)
except (TypeError, ValueError, KeyError): # KeyError for Enum
raise TypeError("unit annotation is not a Unit or PhysicalType") from None

# Allow to sort of work for python 3.8- / no typing_extensions
# instead of bailing out, return the unit for `quantity_input`
if not HAS_ANNOTATED:
warnings.warn("Quantity annotations are valid static type annotations only"
" if Python is v3.9+ or `typing_extensions` is installed.")
return unit

# Quantity does not (yet) properly extend the NumPy generics types,
# introduce in numpy v1.22+, instead just including the unit info as
# introduced in numpy v1.22+, instead just including the unit info as
# metadata using Annotated.
if minversion('numpy', '1.22'):
if not NUMPY_LT_1_22:
cls = super().__class_getitem__((cls, *shape_dtype))
return Annotated.__class_getitem__((cls, unit))

Expand Down
9 changes: 0 additions & 9 deletions astropy/units/tests/test_physical.py
Original file line number Diff line number Diff line change
Expand Up @@ -546,12 +546,3 @@ def test_physical_types_module_access():
# a failed access
with pytest.raises(AttributeError, match="has no attribute"):
physical.not_a_valid_physical_type_name


def test_is_physicaltypelike():
"""Test `~astropy.units.physical.is_physicaltypelike`"""
# is physical-type like
assert u.is_physicaltypelike("length") == u.m.physical_type

# is not like
assert u.is_physicaltypelike(TypeError) is False

0 comments on commit e6d147f

Please sign in to comment.