Skip to content

Commit ca3036c

Browse files
committed
refactor: simplify Cython wiring per review
Drop Cython <3 co_flags fallback helpers. inspect.iscoroutinefunction and inspect.isasyncgenfunction recognise Cython 3 cyfunctions natively via the CO_COROUTINE / CO_ASYNC_GENERATOR bits already on __code__. Replace manual cythonize + build_ext fixture harness with pyximport, which handles caching + cross-platform build. Parametrize the function-like predicate tests; drop the module-symbol-presence guard (build failure surfaces as ImportError).
1 parent 098b920 commit ca3036c

3 files changed

Lines changed: 39 additions & 351 deletions

File tree

src/dependency_injector/wiring.py

Lines changed: 4 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66
from functools import wraps
77
from importlib import import_module, invalidate_caches as invalidate_import_caches
88
from inspect import (
9-
CO_ASYNC_GENERATOR,
10-
CO_COROUTINE,
119
Parameter,
1210
getmembers,
1311
isasyncgenfunction,
@@ -131,76 +129,15 @@ def is_werkzeug_local_proxy(obj: Any) -> bool:
131129

132130

133131
def _is_cyfunction(obj: Any) -> bool:
134-
"""Return True for Cython-compiled functions/methods.
135-
136-
Cython-compiled callables (built with ``binding=True`` and
137-
``embedsignature=True``) are not recognised by :func:`inspect.isfunction`
138-
because they are instances of ``cython_function_or_method`` rather than
139-
``types.FunctionType``. They are nevertheless safe targets for wiring:
140-
dep-injector only *reads* ``inspect.signature`` (which works on
141-
cyfunctions with ``embedsignature=True``) and wraps the original via
142-
``functools.wraps``; no writes are performed on ``__code__``,
143-
``__defaults__`` or ``__globals__``.
144-
145-
Recognises ``cython_function_or_method`` only. Fused-function templates
146-
(``fused_cython_function``) dispatch per call and are intentionally
147-
excluded — wrapping the template would inject before type dispatch,
148-
which has not been validated. Fused support is potential follow-up
149-
work.
150-
151-
Cython >= 3.1.0 is the tested floor. Earlier versions may work but
152-
the ``co_flags`` fallbacks in :func:`_iscoroutinefunction_compat` and
153-
:func:`_isasyncgenfunction_compat` exist specifically because
154-
Cython < 3.0 did not surface coroutine / async-generator status via
155-
:mod:`inspect`.
156-
"""
132+
"""Return True for Cython-compiled functions/methods (non-fused)."""
157133
return type(obj).__name__ == "cython_function_or_method"
158134

159135

160136
def _is_function_like(obj: Any) -> bool:
161-
"""Return True for pure-Python functions and Cython-compiled functions.
162-
163-
Wiring's discovery pass must accept both so that codebases compiled to
164-
``.so`` extensions (e.g. for source-protected container images) can be
165-
wired transparently.
166-
"""
137+
"""Return True for pure-Python functions and Cython-compiled functions."""
167138
return isfunction(obj) or _is_cyfunction(obj)
168139

169140

170-
def _iscoroutinefunction_compat(fn: Any) -> bool:
171-
"""Coroutine-function check that also handles Cython-compiled ``async def``.
172-
173-
Cython 3.x exposes coroutine cyfunctions correctly via
174-
:func:`inspect.iscoroutinefunction`. Cython < 3.0 did not — for those
175-
versions the underlying ``__code__.co_flags`` still carries the
176-
``CO_COROUTINE`` bit, so fall back to that.
177-
"""
178-
if iscoroutinefunction(fn):
179-
return True
180-
code = getattr(fn, "__code__", None)
181-
if code is None:
182-
return False
183-
return bool(getattr(code, "co_flags", 0) & CO_COROUTINE)
184-
185-
186-
def _isasyncgenfunction_compat(fn: Any) -> bool:
187-
"""Async-generator check that also handles Cython-compiled ``async def`` w/ yield.
188-
189-
Symmetric to :func:`_iscoroutinefunction_compat`: Cython < 3.0
190-
async-generator cyfunctions are not recognised by
191-
:func:`inspect.isasyncgenfunction`, but the ``CO_ASYNC_GENERATOR`` bit
192-
is still present in ``__code__.co_flags``. Without this helper, async-
193-
gen cyfunctions would fall through to ``_get_sync_patched`` and break
194-
at first ``await`` / ``async for``.
195-
"""
196-
if isasyncgenfunction(fn):
197-
return True
198-
code = getattr(fn, "__code__", None)
199-
if code is None:
200-
return False
201-
return bool(getattr(code, "co_flags", 0) & CO_ASYNC_GENERATOR)
202-
203-
204141
from . import providers # noqa: E402
205142

206143
__all__ = (
@@ -895,9 +832,9 @@ def _get_patched(
895832
reference_closing=reference_closing,
896833
)
897834

898-
if _iscoroutinefunction_compat(fn):
835+
if iscoroutinefunction(fn):
899836
patched = _get_async_patched(fn, patched_object)
900-
elif _isasyncgenfunction_compat(fn):
837+
elif isasyncgenfunction(fn):
901838
patched = _get_async_gen_patched(fn, patched_object)
902839
else:
903840
patched = _get_sync_patched(fn, patched_object)

tests/unit/wiring/conftest.py

Lines changed: 3 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,5 @@
1-
"""Build Cython test fixtures at session start.
1+
"""Enable on-import compilation of the .pyx wiring fixture via pyximport."""
22

3-
The wiring test suite includes regression coverage for Cython-compiled user
4-
modules (see tests/unit/wiring/test_cython.py). This conftest compiles the
5-
.pyx fixture into a .so before test collection so the import succeeds.
3+
import pyximport
64

7-
If Cython or a C toolchain is unavailable, the build is skipped silently
8-
and the cython test module raises pytest.importorskip at collection.
9-
"""
10-
11-
import logging
12-
import sysconfig
13-
from pathlib import Path
14-
15-
_LOG = logging.getLogger(__name__)
16-
17-
_FIXTURE_DIR = (
18-
Path(__file__).resolve().parent.parent / "samples" / "wiringcython"
19-
)
20-
_PYX = _FIXTURE_DIR / "cythonmodule.pyx"
21-
22-
23-
def _ext_suffix() -> str:
24-
return sysconfig.get_config_var("EXT_SUFFIX") or ".so"
25-
26-
27-
def _fixture_so_path() -> Path:
28-
"""Exact .so path the build produces under the current interpreter.
29-
30-
Includes the ABI tag (e.g. ``.cpython-313-x86_64-linux-gnu.so``) so a
31-
stray .so built against a different Python / platform is treated as a
32-
miss instead of skipping the build silently on cross-ABI CI runs.
33-
"""
34-
return _FIXTURE_DIR / f"cythonmodule{_ext_suffix()}"
35-
36-
37-
def _fixture_already_built() -> bool:
38-
so_path = _fixture_so_path()
39-
if not so_path.exists():
40-
return False
41-
return so_path.stat().st_mtime >= _PYX.stat().st_mtime
42-
43-
44-
def _build_fixture() -> bool:
45-
if not _PYX.exists():
46-
return False
47-
if _fixture_already_built():
48-
return True
49-
try:
50-
from Cython.Build import cythonize
51-
from setuptools import Extension
52-
from setuptools.command.build_ext import build_ext
53-
from setuptools.dist import Distribution
54-
except ImportError as exc:
55-
_LOG.info("Cython fixture build skipped (missing dep): %s", exc)
56-
return False
57-
58-
# Bare module name (no dots) means setuptools writes the .so directly
59-
# to ``<build_lib>/cythonmodule<EXT_SUFFIX>`` — no package-layout
60-
# subdir is created. With ``build_lib`` pointed at the fixture dir,
61-
# the .so lands next to the .pyx where
62-
# ``from samples.wiringcython.cythonmodule import ...`` resolves via
63-
# the ``tests/unit/conftest.py`` ``sys.path`` insertion.
64-
#
65-
# ``inplace`` MUST be 0 here: ``inplace=1`` ignores ``build_lib`` and
66-
# writes next to the source tree relative to CWD, which on a clean
67-
# checkout drops the .so at the repo root.
68-
ext = Extension(
69-
"cythonmodule",
70-
sources=[str(_PYX)],
71-
)
72-
ext_modules = cythonize(
73-
[ext],
74-
compiler_directives={
75-
"language_level": 3,
76-
"binding": True,
77-
"embedsignature": True,
78-
"annotation_typing": False,
79-
},
80-
quiet=True,
81-
)
82-
dist = Distribution(
83-
{"name": "wiringcython_fixture", "ext_modules": ext_modules}
84-
)
85-
cmd = build_ext(dist)
86-
cmd.inplace = 0
87-
cmd.build_lib = str(_FIXTURE_DIR)
88-
cmd.build_temp = str(_FIXTURE_DIR / "_build")
89-
cmd.ensure_finalized()
90-
try:
91-
cmd.run()
92-
except Exception as exc: # noqa: BLE001 — surface every build failure mode
93-
_LOG.warning("Cython fixture build failed: %s", exc)
94-
return False
95-
96-
# Positive post-condition — the .so must be where the test importer
97-
# will look. If setuptools silently changed layout under us, fail
98-
# loud here instead of letting test_cython.py skip on importorskip.
99-
so_path = _fixture_so_path()
100-
if not so_path.exists():
101-
_LOG.warning(
102-
"Cython fixture build completed but expected .so missing at %s",
103-
so_path,
104-
)
105-
return False
106-
return True
107-
108-
109-
def pytest_configure(config):
110-
"""Compile cythonmodule.pyx so test_cython.py can import the .so."""
111-
_build_fixture()
5+
pyximport.install(language_level=3)

0 commit comments

Comments
 (0)