Skip to content

Commit

Permalink
Drop Python 3.6 suport, it's EOL (#198)
Browse files Browse the repository at this point in the history
There are a few changes that need to happen together with this one,
namely we need to specify explicit 3.7+ PyPy3 versions and in order
to do that we have to upgrade the setup-python GH action.
  • Loading branch information
jstasiak committed Jun 14, 2022
1 parent 8122b89 commit 92212fe
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 114 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
python-version: [3.6, 3.7, 3.8, 3.9, "3.10", pypy3]
python-version: [3.7, 3.8, 3.9, "3.10", "pypy3.7", "pypy3.8", "pypy3.9"]
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ The core values of Injector are:
* Documentation: https://injector.readthedocs.org
* Change log: https://injector.readthedocs.io/en/latest/changelog.html

Injector works with CPython 3.6+ and PyPy 3 implementing Python 3.6+.
Injector works with CPython 3.7+ and PyPy 3 implementing Python 3.7+.

A Quick Example
---------------
Expand Down
143 changes: 62 additions & 81 deletions injector/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,36 +46,18 @@
except ImportError:
from typing_extensions import NoReturn

HAVE_ANNOTATED = sys.version_info >= (3, 7, 0)

# This is a messy, type-wise, because we not only have two potentially conflicting imports here
# but we also define our own versions in the else block in case we operate on Python 3.6
# which didn't get Annotated support in get_type_hints(). The easiest way to make mypy
# happy here is to tell it the versions from typing_extensions are canonical. Since this
# typing_extensions import is only for mypy it'll work even without typing_extensions actually
# installed so all's good.
# The easiest way to make mypy happy here is to tell it the versions from typing_extensions are
# canonical. Since this typing_extensions import is only for mypy it'll work even without
# typing_extensions actually installed so all's good.
if TYPE_CHECKING:
from typing_extensions import _AnnotatedAlias, Annotated, get_type_hints
elif HAVE_ANNOTATED:
else:
# Ignoring errors here as typing_extensions stub doesn't know about those things yet
try:
from typing import _AnnotatedAlias, Annotated, get_type_hints
except ImportError:
from typing_extensions import _AnnotatedAlias, Annotated, get_type_hints
else:

class Annotated:
pass

from typing import get_type_hints as _get_type_hints

def get_type_hints(
obj: Callable[..., Any],
globalns: Optional[Dict[str, Any]] = None,
localns: Optional[Dict[str, Any]] = None,
include_extras: bool = False,
) -> Dict[str, Any]:
return _get_type_hints(obj, globalns, localns)


__author__ = 'Alec Thomas <alec@swapoff.org>'
Expand Down Expand Up @@ -119,86 +101,85 @@ def wrapper(*args: Any, **kwargs: Any) -> Any:
_inject_marker = object()
_noinject_marker = object()

if HAVE_ANNOTATED:
InjectT = TypeVar('InjectT')
Inject = Annotated[InjectT, _inject_marker]
"""An experimental way to declare injectable dependencies utilizing a `PEP 593`_ implementation
in Python 3.9 and backported to Python 3.7+ in `typing_extensions`.
InjectT = TypeVar('InjectT')
Inject = Annotated[InjectT, _inject_marker]
"""An experimental way to declare injectable dependencies utilizing a `PEP 593`_ implementation
in Python 3.9 and backported to Python 3.7+ in `typing_extensions`.
Those two declarations are equivalent::
Those two declarations are equivalent::
@inject
def fun(t: SomeType) -> None:
pass
@inject
def fun(t: SomeType) -> None:
pass
def fun(t: Inject[SomeType]) -> None:
pass
def fun(t: Inject[SomeType]) -> None:
pass
The advantage over using :func:`inject` is that if you have some noninjectable parameters
it may be easier to spot what are they. Those two are equivalent::
The advantage over using :func:`inject` is that if you have some noninjectable parameters
it may be easier to spot what are they. Those two are equivalent::
@inject
@noninjectable('s')
def fun(t: SomeType, s: SomeOtherType) -> None:
pass
@inject
@noninjectable('s')
def fun(t: SomeType, s: SomeOtherType) -> None:
pass
def fun(t: Inject[SomeType], s: SomeOtherType) -> None:
pass
def fun(t: Inject[SomeType], s: SomeOtherType) -> None:
pass
.. seealso::
.. seealso::
Function :func:`get_bindings`
A way to inspect how various injection declarations interact with each other.
Function :func:`get_bindings`
A way to inspect how various injection declarations interact with each other.
.. versionadded:: 0.18.0
.. note:: Requires Python 3.7+.
.. note::
.. versionadded:: 0.18.0
.. note:: Requires Python 3.7+.
.. note::
If you're using mypy you need the version 0.750 or newer to fully type-check code using this
construct.
If you're using mypy you need the version 0.750 or newer to fully type-check code using this
construct.
.. _PEP 593: https://www.python.org/dev/peps/pep-0593/
.. _typing_extensions: https://pypi.org/project/typing-extensions/
"""
.. _PEP 593: https://www.python.org/dev/peps/pep-0593/
.. _typing_extensions: https://pypi.org/project/typing-extensions/
"""

NoInject = Annotated[InjectT, _noinject_marker]
"""An experimental way to declare noninjectable dependencies utilizing a `PEP 593`_ implementation
in Python 3.9 and backported to Python 3.7+ in `typing_extensions`.
NoInject = Annotated[InjectT, _noinject_marker]
"""An experimental way to declare noninjectable dependencies utilizing a `PEP 593`_ implementation
in Python 3.9 and backported to Python 3.7+ in `typing_extensions`.
Since :func:`inject` declares all function's parameters to be injectable there needs to be a way
to opt out of it. This has been provided by :func:`noninjectable` but `noninjectable` suffers from
two issues:
Since :func:`inject` declares all function's parameters to be injectable there needs to be a way
to opt out of it. This has been provided by :func:`noninjectable` but `noninjectable` suffers from
two issues:
* You need to repeat the parameter name
* The declaration may be relatively distance in space from the actual parameter declaration, thus
hindering readability
* You need to repeat the parameter name
* The declaration may be relatively distance in space from the actual parameter declaration, thus
hindering readability
`NoInject` solves both of those concerns, for example (those two declarations are equivalent)::
`NoInject` solves both of those concerns, for example (those two declarations are equivalent)::
@inject
@noninjectable('b')
def fun(a: TypeA, b: TypeB) -> None:
pass
@inject
@noninjectable('b')
def fun(a: TypeA, b: TypeB) -> None:
pass
@inject
def fun(a: TypeA, b: NoInject[TypeB]) -> None:
pass
@inject
def fun(a: TypeA, b: NoInject[TypeB]) -> None:
pass
.. seealso::
.. seealso::
Function :func:`get_bindings`
A way to inspect how various injection declarations interact with each other.
Function :func:`get_bindings`
A way to inspect how various injection declarations interact with each other.
.. versionadded:: 0.18.0
.. note:: Requires Python 3.7+.
.. note::
.. versionadded:: 0.18.0
.. note:: Requires Python 3.7+.
.. note::
If you're using mypy you need the version 0.750 or newer to fully type-check code using this
construct.
If you're using mypy you need the version 0.750 or newer to fully type-check code using this
construct.
.. _PEP 593: https://www.python.org/dev/peps/pep-0593/
.. _typing_extensions: https://pypi.org/project/typing-extensions/
"""
.. _PEP 593: https://www.python.org/dev/peps/pep-0593/
.. _typing_extensions: https://pypi.org/project/typing-extensions/
"""


def reraise(original: Exception, exception: Exception, maximum_frames: int = 1) -> NoReturn:
Expand Down Expand Up @@ -683,7 +664,7 @@ def _is_specialization(cls: type, generic_class: Any) -> bool:
# We need to special-case Annotated as its __origin__ behaves differently than
# other typing generic classes. See https://github.com/python/typing/pull/635
# for some details.
if HAVE_ANNOTATED and generic_class is Annotated and isinstance(cls, _AnnotatedAlias):
if generic_class is Annotated and isinstance(cls, _AnnotatedAlias):
return True

if not hasattr(cls, '__origin__'):
Expand Down
57 changes: 27 additions & 30 deletions injector_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
from injector import (
Binder,
CallError,
Inject,
Injector,
NoInject,
Scope,
InstanceProvider,
ClassProvider,
Expand All @@ -46,12 +48,8 @@
ClassAssistedBuilder,
Error,
UnknownArgument,
HAVE_ANNOTATED,
)

if HAVE_ANNOTATED:
from injector import Inject, NoInject


class EmptyClass:
pass
Expand Down Expand Up @@ -1449,38 +1447,37 @@ def function3b(a: int, b: str) -> None:

assert get_bindings(function3b) == {'a': int}

if HAVE_ANNOTATED:
# The simple case of no @inject but injection requested with Inject[...]
def function4(a: Inject[int], b: str) -> None:
pass
# The simple case of no @inject but injection requested with Inject[...]
def function4(a: Inject[int], b: str) -> None:
pass

assert get_bindings(function4) == {'a': int}
assert get_bindings(function4) == {'a': int}

# Using @inject with Inject is redundant but it should not break anything
@inject
def function5(a: Inject[int], b: str) -> None:
pass
# Using @inject with Inject is redundant but it should not break anything
@inject
def function5(a: Inject[int], b: str) -> None:
pass

assert get_bindings(function5) == {'a': int, 'b': str}
assert get_bindings(function5) == {'a': int, 'b': str}

# We need to be able to exclude a parameter from injection with NoInject
@inject
def function6(a: int, b: NoInject[str]) -> None:
pass
# We need to be able to exclude a parameter from injection with NoInject
@inject
def function6(a: int, b: NoInject[str]) -> None:
pass

assert get_bindings(function6) == {'a': int}
assert get_bindings(function6) == {'a': int}

# The presence of NoInject should not trigger anything on its own
def function7(a: int, b: NoInject[str]) -> None:
pass
# The presence of NoInject should not trigger anything on its own
def function7(a: int, b: NoInject[str]) -> None:
pass

assert get_bindings(function7) == {}
assert get_bindings(function7) == {}

# There was a bug where in case of multiple NoInject-decorated parameters only the first one was
# actually made noninjectable and we tried to inject something we couldn't possibly provide
# into the second one.
@inject
def function8(a: NoInject[int], b: NoInject[int]) -> None:
pass
# There was a bug where in case of multiple NoInject-decorated parameters only the first one was
# actually made noninjectable and we tried to inject something we couldn't possibly provide
# into the second one.
@inject
def function8(a: NoInject[int], b: NoInject[int]) -> None:
pass

assert get_bindings(function8) == {}
assert get_bindings(function8) == {}

0 comments on commit 92212fe

Please sign in to comment.