66from functools import wraps
77from importlib import import_module , invalidate_caches as invalidate_import_caches
88from inspect import (
9+ CO_ASYNC_GENERATOR ,
10+ CO_COROUTINE ,
911 Parameter ,
1012 getmembers ,
1113 isasyncgenfunction ,
@@ -127,6 +129,78 @@ def is_werkzeug_local_proxy(obj: Any) -> bool:
127129
128130 INSPECT_EXCLUSION_FILTERS .append (is_werkzeug_local_proxy )
129131
132+
133+ 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+ """
157+ return type (obj ).__name__ == "cython_function_or_method"
158+
159+
160+ 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+ """
167+ return isfunction (obj ) or _is_cyfunction (obj )
168+
169+
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+
130204from . import providers # noqa: E402
131205
132206__all__ = (
@@ -485,7 +559,7 @@ def wire( # noqa: C901
485559 warn_unresolved = warn_unresolved ,
486560 warn_unresolved_stacklevel = 1 ,
487561 )
488- elif isfunction (member ):
562+ elif _is_function_like (member ):
489563 _patch_fn (
490564 module ,
491565 member_name ,
@@ -548,10 +622,10 @@ def unwire( # noqa: C901
548622
549623 for module in modules :
550624 for name , member in getmembers (module ):
551- if isfunction (member ):
625+ if _is_function_like (member ):
552626 _unpatch (module , name , member )
553627 elif isclass (member ):
554- for method_name , method in getmembers (member , isfunction ):
628+ for method_name , method in getmembers (member , _is_function_like ):
555629 _unpatch (member , method_name , method )
556630
557631 for patched in _patched_registry .get_callables_from_module (module ):
@@ -803,7 +877,7 @@ def _fetch_modules(package):
803877
804878
805879def _is_method (member ) -> bool :
806- return ismethod (member ) or isfunction (member )
880+ return ismethod (member ) or _is_function_like (member )
807881
808882
809883def _is_marker (member ) -> bool :
@@ -821,9 +895,9 @@ def _get_patched(
821895 reference_closing = reference_closing ,
822896 )
823897
824- if iscoroutinefunction (fn ):
898+ if _iscoroutinefunction_compat (fn ):
825899 patched = _get_async_patched (fn , patched_object )
826- elif isasyncgenfunction (fn ):
900+ elif _isasyncgenfunction_compat (fn ):
827901 patched = _get_async_gen_patched (fn , patched_object )
828902 else :
829903 patched = _get_sync_patched (fn , patched_object )
0 commit comments