Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make @particle_input compatible with from __future__ import annotations #2479

Merged
2 changes: 2 additions & 0 deletions changelog/2479.bugfix.rst
@@ -0,0 +1,2 @@
Fixed a bug so that |particle_input| now works when a module begins
with py:`from future import annotations`.
17 changes: 2 additions & 15 deletions plasmapy/particles/decorators.py
Expand Up @@ -9,7 +9,7 @@
from collections.abc import Callable, Iterable, MutableMapping
from inspect import BoundArguments
from numbers import Integral, Real
from typing import Any, Optional, TypedDict, Union
from typing import Any, Optional, TypedDict, Union, get_type_hints
Copy link
Member Author

@namurphy namurphy Feb 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChatGPT suggested using typing.get_type_hints because other methods returned the string representations of the annotations rather than the actual objects. I don't know how long this would have taken me to figure out otherwise!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ChatGPT 😱


import numpy as np
import wrapt
Expand Down Expand Up @@ -57,19 +57,6 @@ class _CallableDataDict(TypedDict, total=False):
)


def _get_annotations(callable_: Callable[..., Any]) -> dict[str, Any]:
"""
Access the annotations of a callable.

.. note::

For Python 3.10+, this should be replaced with
`inspect.get_annotations`.
"""
# Python 3.10: Replace this with inspect.get_annotations
return getattr(callable_, "__annotations__", {})


def _make_into_set_or_none(obj: Any) -> Optional[Iterable[str]]:
"""
Return `None` if ``obj`` is `None`, and otherwise convert ``obj``
Expand Down Expand Up @@ -221,7 +208,7 @@ def callable_(self) -> Callable[..., Any]:
@callable_.setter
def callable_(self, callable_: Callable[..., Any]) -> None:
self._data["callable_"] = callable_
self._data["annotations"] = _get_annotations(callable_)
self._data["annotations"] = get_type_hints(callable_)
self._data["parameters_to_process"] = self.find_parameters_to_process()
self._data["signature"] = inspect.signature(callable_)

Expand Down
44 changes: 44 additions & 0 deletions plasmapy/particles/tests/test_decorators_future_annotations.py
@@ -0,0 +1,44 @@
"""Tests for ``@particle_input`` with delayed evaluation of annotations."""

# These tests must be in their own file because the `from __future__`
# import must be at the top.
from __future__ import annotations

from plasmapy.particles.decorators import particle_input
from plasmapy.particles.particle_class import Particle, ParticleLike


@particle_input
def function_decorated_with_particle_input(particle: ParticleLike) -> Particle:
return particle # type: ignore[return-value]


class DecoratedClass:
@particle_input
def __init__(self, particle: ParticleLike) -> None:
self.particle = particle


class UndecoratedClass:
@particle_input
def decorated_method(self, particle: ParticleLike) -> Particle:
return particle # type: ignore[return-value]


def test_particle_input_from_future_import_annotations_function() -> None:
particle = function_decorated_with_particle_input("p+")
assert particle == Particle("p+")


def test_particle_input_from_future_import_annotations_instantiation() -> None:
instance = DecoratedClass("p+")

assert isinstance(instance.particle, Particle)
assert instance.particle == Particle("p+")


def test_particle_input_from_future_import_annotations_method() -> None:
instance = UndecoratedClass()
result = instance.decorated_method(particle="p+")
assert isinstance(result, Particle)
assert result == Particle("p+")