Support wiring of Cython-compiled modules#965
Open
keyz182 wants to merge 1 commit into
Open
Conversation
The wiring discovery pass gated on inspect.isfunction / ismethod, which return False for cython_function_or_method, silently skipping handlers compiled to .so. Recognise cyfunctions in discovery and dispatch, fall back to __code__.co_flags for async-status detection on Cython < 3. Adds samples/wiringcython/ fixture (sync, async, async-gen, class __call__) + 15 regression tests. Cython added to [testenv] deps so the default tox invocation builds the fixture.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note:
I used Claude Code heavily here. I don't see any policy on your stance on AI use, so if you're against it, I can drop the PR.
That said, this works against our codebase when we run it through cythonize (previously we could only cythonize code that didn't have dependency injection).
Problem
Container.wire()silently skips functions and methods in user modules compiled to.sovia Cython.Depends(Provide[Container.x])markers in those modules fall through unresolved as the literal_Markersentinel, surfacing later asAttributeErrorfrom the handler body.Failure shape (real-world FastAPI handler):
The
uowparameter received the_Markersentinel because the wiring discovery pass never registered the cyfunction.Root cause
wiring.pygates oninspect.isfunction/inspect.ismethod, both of which returnFalseforcython_function_or_methodinstances. Affected call sites inwiring.py:wire()module-member dispatch —elif isfunction(member):(skips compiled module-level functions)_is_method()—ismethod(m) or isfunction(m)(skips compiled class methods, including__call__)unwire()traversal —if isfunction(member)+getmembers(member, isfunction)(no-op cleanup for compiled targets)_get_patched()sync/async dispatch —iscoroutinefunction/isasyncgenfunction(Cython < 3 wraps coroutine cyfunctions with the sync patcher)_cwiring.pyxis unaffected — it operates on the already-builtinjectionsdict and never inspects user functions. The blindness is entirely in the discovery pass.Safety analysis
Verified that dep-injector never mutates user functions:
_patch_fn/_patch_methodwrap viafunctools.wraps(writes go to the new pure-Python wrapper, not the cyfunction) and rebind the attribute on the parent module/class viasetattr._fetch_reference_injectionsonly readsinspect.signature(fn), which works on cyfunctions built withembedsignature=True.fn.__defaults__,fn.__code__,fn.__globals__, orfn.__signature__.Cython's read-only restrictions on those slots are therefore irrelevant.
Change
src/dependency_injector/wiring.py:_is_cyfunction,_is_function_like,_iscoroutinefunction_compat,_isasyncgenfunction_compat.isfunction(member)at the four discovery gates with_is_function_like(member).iscoroutinefunction(fn)/isasyncgenfunction(fn)in_get_patchedwith the compat helpers, which fall back to__code__.co_flags & CO_COROUTINE/CO_ASYNC_GENERATORso Cython < 3 async / async-gen cyfunctions are dispatched correctly.cython_function_or_methodonly.fused_cython_functiontemplates dispatch per call; wrapping the template would inject before type dispatch — intentionally excluded as potential follow-up work.Pure-Python wiring behaviour is unchanged.
_is_function_like(obj) = isfunction(obj) or _is_cyfunction(obj)only widens the accept set.Tests
tests/unit/wiring/test_cython.py— 15 tests covering:_iscoroutinefunction_compat/_isasyncgenfunction_compaton real compiled handlers AND via a synthetic__code__.co_flagscallable to exercise the Cython < 3 fallback branches.__call__shapes._patched_registryassertion thatunwire()empties the injection bindings while preservingreference_injections.HandlerClass.__call__so a future Cython release that changes the class-attribute representation surfaces as a test failure rather than silent wiring drop-out..sofails loud at collection time instead of producing a silent0 collectedCI run.tests/unit/wiring/conftest.py— session-start build of the.pyxfixture viacythonize+build_extwith bare module name +inplace=0+build_libpointed at the fixture dir (the .so lands where the test importer expects, not at the repo root that a dotted-or-undotted name withinplace=1would produce on a clean checkout). Freshness check includesEXT_SUFFIXso a stray .so from a different ABI/Python version is treated as a miss. Build skips silently if Cython / setuptools / a C toolchain is unavailable.tests/unit/samples/wiringcython/cythonmodule.pyx— fixture exercising the four wiring dispatch shapes withbinding=True,embedsignature=True,annotation_typing=False.tox.ini—cython>=3,<4added to[testenv]deps so the defaulttoxinvocation builds the fixture. Without it,test_cython.pysilently skips..gitignore— fixture build artefacts (*.c,*.so,_build/).docs/wiring.rst— new "Wiring of Cython-compiled modules" section documenting the requiredcythonizecompiler directives (with a callout thatannotation_typingdefaults toTruein Cython 3.x and must be flipped toFalsefor FastAPI handlers usingparam: str = Header(...)patterns).Validation
.sofiles, run against patched wiring.pyOut of scope (follow-up)
_patch_fndoessetattr(module, name, fn)with no read-back verification. Read-only slot / descriptor edge cases could silently no-op. Pre-existing behaviour unchanged by this PR; flagged for a separate hardening pass.fused_cython_functiontemplate support. Would require deciding whether to wrap before type dispatch or per-resolved-instance.Reviewer notes
_is_cyfunctionusestype(obj).__name__ == "cython_function_or_method"rather than anisinstancecheck against a Cython-imported type, to avoid making Cython a runtime import requirement of dep-injector itself. The name has been stable across Cython 0.29 → 3.x.co_flagsfallback branches are unreachable from real Cython 3.x cyfunctions (the project pins Cython ≥ 3.1.4). They exist as defensive coverage for downstream users on older Cython. Synthetic tests exercise them.