Skip to content

feat(di): rewrite DI engine — proper injector integration, instance-based routing, provider normalization#125

Merged
ItayTheDar merged 18 commits into
mainfrom
auspicious-dart
May 9, 2026
Merged

feat(di): rewrite DI engine — proper injector integration, instance-based routing, provider normalization#125
ItayTheDar merged 18 commits into
mainfrom
auspicious-dart

Conversation

@ItayTheDar
Copy link
Copy Markdown
Contributor

Summary

  • Rewrites the DI engine from scratch to properly leverage the injector library instead of using it as syntactic sugar. Dependencies are now resolved at container build time and injected into actual instances — no more setattr(class, dep, value) mutating class-level attributes.
  • Non-singleton container: PyNestContainer is now a plain factory-created class (no _instance = None), making it safe to run multiple apps in the same process and fully testable.
  • Instance-based routing: RoutesResolver gets a real controller instance from the container and registers bound methods as FastAPI endpoints, instead of class-level monkey-patching via class_based_view.
  • Provider normalization: New ProviderDescriptor / normalize_provider() pipeline supports useClass, useValue, useFactory, useExisting, and InjectionToken keys — the full NestJS provider surface.
  • Compile-time cycle detection: DependencyGraph validates the provider graph with DFS before any instance is built.
  • Exception filters integrated: merged @Catch, @UseFilters, use_global_filters() from main; filter wrapping now lives in RoutesResolver rather than the deleted class_based_view.py.

Key files changed

File Change
nest/common/provider.py New — InjectionToken, Scope, ProviderDescriptor, normalize_provider
nest/core/dependency_graph.py New — DAG with cycle detection + topo sort
nest/core/injector_module.py New — PyNestInjectorModule, build_injector()
nest/core/pynest_container.py Rewritten — non-singleton, build() / get() API
nest/common/route_resolver.py Rewritten — instance-based routing + filter wrapping
nest/core/decorators/injectable.py Rewritten — pure metadata, no class mutation
nest/core/decorators/controller.py Rewritten — pure metadata, no CBV wrapping
nest/core/pynest_factory.py Updated — calls container.build()
nest/core/decorators/class_based_view.py Deleted — replaced by RoutesResolver

Test plan

  • All 137 unit tests pass locally (uv run --group test pytest)
  • Integration tests pass in CI
  • Exception filter tests (global, controller-level, route-level) pass
  • Existing example apps still boot correctly

🤖 Generated with Claude Code

ItayTheDar and others added 18 commits May 7, 2026 21:13
…ovider

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ormalize providers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…derDescriptors to injector bindings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…I, instance-based injection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ss mutation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…no class mutation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…methods

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…engine end-to-end

- PyNestFactory.create() now calls container.build() before creating PyNestApp
- PyNestApp rewritten: no longer inherits PyNestApplicationContext, no select_context_module/register_routes methods; RoutesResolver called inline in __init__
- test_pynest_factory.py replaced with 6 focused TDD tests covering e2e routes, isolation, and DI correctness
- test_pynest_application.py updated to match new PyNestApp API

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ort InjectionToken + Scope

- Drop parse_dependencies, get_instance_variables, get_non_dependencies_params from utils.py
- Remove dead imports from cli_decorators.py (only parse_params remains)
- Delete class_based_view.py (replaced by instance-based routing)
- Richer docstrings on all three exception classes in exceptions.py
- Export InjectionToken and Scope from nest.core.__init__

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolved conflicts:
- nest/core/pynest_application.py: kept new DI structure, added use_global_filters() + _register_global_handler() from main
- nest/core/decorators/class_based_view.py: deleted (replaced by RoutesResolver); ported _wrap_route_with_filters() logic into route_resolver._wrap_with_filters()

Also fixed nest/common/module.py forward-ref NameError by adding `from __future__ import annotations`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The new ModuleRef stores controllers as compiled.controllers (list),
not module.controllers (dict). Fixes integration test boot failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CLIAppFactory now calls container.build() then manually resolves
each CLI controller's constructor deps from the injector, instead of
passing the class as 'self' (which relied on the old class-mutation DI).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pynest generate module -n <name> now automatically adds the import and
registers the new module in src/app_module.py, matching the behavior of
'generate resource' and NestJS CLI convention.

Also fixes generate_empty_module_file to scaffold with proper
`@Module(imports=[], controllers=[], providers=[])` instead of `@Module()`.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hardcoded Path.cwd() / 'src' doubled the path when running from inside
src/. Now uses the same find_target_folder() logic as generate resource,
which walks up/down the directory tree to find src/ regardless of cwd.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two issues:
1. AppController scaffold was missing tag="app" — its routes appeared in
   Swagger's unnamed "default" bucket instead of an "app" section.
   Fixed by adding tag="app" to app_controller_file() template.

2. 'generate module' created a silent empty skeleton — no output, no hint,
   leaving the user wondering why nothing appeared in /docs.
   Now prints CREATE/UPDATE messages and a hint pointing to
   'generate resource' for a full CRUD scaffold.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. db_request_handler: change 'return HTTPException(...)' to
   'raise HTTPException(...)'. Returning an exception object lets it
   propagate silently as a truthy value, poisoning any multi-hop
   service call chain with confusing TypeErrors instead of HTTP 500s.
   Also removed session lifecycle from the decorator — session
   management belongs in each service method, not in a cross-cutting
   decorator.

2. OrmProvider.get_db(): the try/finally block was closing the session
   immediately after returning it, so the caller always received an
   already-closed session. Removed the finally; added get_session()
   context manager (rollback on exception, always close) as the
   canonical way to obtain a per-call session.

3. Service template: replaced 'self.session = self.config.get_db()' in
   __init__ (one shared session for the entire singleton lifetime) with
   'with self.config.get_session() as session:' inside each method,
   giving every request its own isolated session and preventing
   concurrent-request data corruption.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously every provider from every module landed in one flat injector
pool — a service in module A could inject a service from module B even
when A never imported B and B never exported the service. Encapsulation
was a documentation-only contract, not a check.

Now PyNestContainer.build() runs a validation pass after cycle detection:

- Each module gets a 'visible' set:
    own providers + own controllers
    + transitively-resolved exports of its imports (re-exports supported)
    + every provider from any @module(is_global=True)
- Every consumer's __init__ annotations are walked; class-typed deps that
  are registered providers but not in the consumer's visible set raise
  ProviderNotExportedException listing every violation with a concrete
  fix ('add imports=[X] to A and exports=[Y] to X, or move Y into A').

Updated test_imported_module_providers_are_resolvable to declare the
export it was implicitly relying on. Added 8 new tests covering:
same-module deps, proper import+export, global modules, module re-exports,
missing import, missing export, controller violations, and unrelated
sibling modules.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@ItayTheDar ItayTheDar merged commit c8107d9 into main May 9, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant