Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
4e7e9a7
feat(di): add InjectionToken, Scope, ProviderDescriptor, normalize_pr…
ItayTheDar May 7, 2026
c22a98f
feat(di): add DependencyGraph with cycle detection and topological sort
ItayTheDar May 7, 2026
8719963
feat(di): add CompiledModule dataclass and update ModuleCompiler to n…
ItayTheDar May 7, 2026
37f5fd2
feat(di): add PyNestInjectorModule and build_injector — bridges Provi…
ItayTheDar May 7, 2026
a916606
feat(di): rewrite PyNestContainer — non-singleton, build() + get() AP…
ItayTheDar May 7, 2026
11e0a28
feat(di): rewrite @Injectable — proper @inject, Scope support, no cla…
ItayTheDar May 7, 2026
6000f47
feat(di): rewrite @Controller — metadata-only, no __init__ deletion, …
ItayTheDar May 7, 2026
89cc71d
feat(di): rewrite RoutesResolver — instance-based routing with bound …
ItayTheDar May 7, 2026
27b596f
feat(di): update PyNestFactory to call container.build() — wires new …
ItayTheDar May 8, 2026
b641783
chore(di): remove dead code (parse_dependencies, ClassBasedView), exp…
ItayTheDar May 8, 2026
1a5f05c
Merge origin/main: uv tooling, exception filters, v0.4.1
ItayTheDar May 8, 2026
38b4af0
fix(cli): update CLIAppFactory to use module.compiled.controllers
ItayTheDar May 8, 2026
9a8620a
fix(cli): properly resolve CLI controller instances via DI
ItayTheDar May 8, 2026
76d5c60
fix(cli): auto-register module in AppModule after 'generate module'
ItayTheDar May 8, 2026
02bd18f
fix(cli): use find_target_folder to locate src/ in generate module
ItayTheDar May 8, 2026
c9b3a45
fix(cli): fix Swagger grouping and generate module UX
ItayTheDar May 8, 2026
941e3d4
fix(orm): fix three session/exception bugs in the sync ORM layer
ItayTheDar May 9, 2026
38d6e8f
feat(di): enforce NestJS-style module encapsulation at build time
ItayTheDar May 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions nest/cli/src/generate/generate_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,38 @@ def generate_module(self, name: str, path: str = None):
"""
template = self.get_template(name)
if path is None:
path = Path.cwd() / "src"
with open(f"{path}/{name}_module.py", "w") as f:
src_path = template.find_target_folder(Path.cwd(), "src")
if src_path is None:
raise Exception("src folder not found")
path = Path(src_path)
else:
path = Path(path)
module_file = path / f"{name}_module.py"
with open(module_file, "w") as f:
f.write(template.generate_empty_module_file())
click.echo(
click.style(f"CREATE src/{name}_module.py", fg="green")
)
app_module_path = path / "app_module.py"
if app_module_path.exists():
template.append_module_to_app(
path_to_app_py=str(app_module_path),
module_import_path=f"src.{name}_module",
)
click.echo(
click.style(
f"UPDATE src/app_module.py (registered {template.class_name})",
fg="yellow",
)
)
click.echo(
click.style(
f"\nHint: {template.class_name} is an empty skeleton. "
f"Add controllers/providers manually, or use "
f"'pynest generate resource -n {name}' for a full CRUD scaffold.",
fg="cyan",
)
)

def generate_app(self, app_name: str, db_type: str, is_async: bool, is_cli: bool):
"""
Expand Down
15 changes: 9 additions & 6 deletions nest/cli/templates/base_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ def app_controller_file():
from .app_service import AppService


@Controller("/")
@Controller("/", tag="app")
class AppController:

def __init__(self, service: AppService):
Expand Down Expand Up @@ -303,10 +303,12 @@ def append_import(

return tree

def append_module_to_app(self, path_to_app_py: str):
def append_module_to_app(self, path_to_app_py: str, module_import_path: str = None):
if module_import_path is None:
module_import_path = f"src.{self.module_name}.{self.module_name}_module"
tree = self.append_import(
file_path=path_to_app_py,
module_path=f"src.{self.module_name}.{self.module_name}_module",
module_path=module_import_path,
class_name=self.class_name,
import_exception="from nest.core import App",
)
Expand Down Expand Up @@ -383,7 +385,8 @@ class {self.capitalized_module_name}Service:
def generate_empty_module_file(self) -> str:
return f"""from nest.core import Module

@Module()

@Module(imports=[], controllers=[], providers=[])
class {self.capitalized_module_name}Module:
...
"""
pass
"""
19 changes: 10 additions & 9 deletions nest/cli/templates/orm_template.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,20 +103,21 @@ class {self.capitalized_module_name}Service:

def __init__(self):
self.config = config
self.session = self.config.get_db()


@db_request_handler
def add_{self.module_name}(self, {self.module_name}: {self.capitalized_module_name}):
new_{self.module_name} = {self.capitalized_module_name}Entity(
**{self.module_name}.dict()
)
self.session.add(new_{self.module_name})
self.session.commit()
return new_{self.module_name}.id
with self.config.get_session() as session:
new_{self.module_name} = {self.capitalized_module_name}Entity(
**{self.module_name}.dict()
)
session.add(new_{self.module_name})
session.commit()
return new_{self.module_name}.id

@db_request_handler
def get_{self.module_name}(self):
return self.session.query({self.capitalized_module_name}Entity).all()
with self.config.get_session() as session:
return session.query({self.capitalized_module_name}Entity).all()

"""

Expand Down
17 changes: 15 additions & 2 deletions nest/common/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,29 @@


class CircularDependencyException(Exception):
def __init__(self, message="Circular dependency detected"):
"""Raised when a circular dependency is detected in the provider graph at build time."""

def __init__(self, message: str = "Circular dependency detected"):
super().__init__(message)


class UnknownModuleException(Exception):
"""Raised when a module cannot be found in the container."""
pass


class NoneInjectableException(Exception):
def __init__(self, message="None Injectable Classe Detected"):
"""Raised when a class without @Injectable is listed as a provider."""

def __init__(self, message: str = "Non-injectable class detected"):
super().__init__(message)


class ProviderNotExportedException(Exception):
"""Raised when a class depends on a provider that lives in another module
which either isn't imported, or doesn't export that provider."""

def __init__(self, message: str = "Provider not visible across module boundary"):
super().__init__(message)


Expand Down
45 changes: 36 additions & 9 deletions nest/common/module.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
from __future__ import annotations

import hashlib
import random
import string
import uuid
from dataclasses import dataclass, field
from typing import Any, List, Type
from uuid import uuid4

from nest.core import Module
@dataclass
class CompiledModule:
"""The result of compiling a @Module-decorated class. Immutable snapshot used by the container."""
token: str
metatype: Type
imports: List[Type] = field(default_factory=list)
controllers: List[Type] = field(default_factory=list)
exports: List[Any] = field(default_factory=list)
provider_descriptors: List[Any] = field(default_factory=list)


class ModulesContainer(dict):
Expand Down Expand Up @@ -187,18 +198,34 @@ class ModuleCompiler:
def __init__(self, module_token_factory: ModuleTokenFactory = ModuleTokenFactory()):
self.module_token_factory = module_token_factory

def compile(self, metatype: Type[Any]):
metadata = self.extract_metadata(metatype)
module_type = metadata["type"]
dynamic_metadata = metadata["dynamic_metadata"]
token = self.module_token_factory.create(module_type, dynamic_metadata)
return ModuleFactory(module_type, token, dynamic_metadata)
def compile(self, metatype: Type[Any]) -> CompiledModule:
from nest.common.provider import normalize_provider # local import avoids circular

if not self.has_module_metadata(metatype):
raise Exception(f"{metatype.__name__} has no metadata found")

raw_providers = getattr(metatype, "providers", []) or []
controllers = getattr(metatype, "controllers", []) or []
imports = getattr(metatype, "imports", []) or []
exports = getattr(metatype, "exports", []) or []

provider_descriptors = [normalize_provider(p) for p in raw_providers]
token = self.module_token_factory.create(metatype)

return CompiledModule(
token=token,
metatype=metatype,
imports=list(imports),
controllers=list(controllers),
exports=list(exports),
provider_descriptors=provider_descriptors,
)

def extract_metadata(self, metatype) -> dict:
# Kept for backward compat with PyNestApplicationContext.select()
metadata = {"type": metatype, "dynamic_metadata": {}}

if not self.has_module_metadata(metatype):
raise Exception(f"{metatype.__name__} as no metadata found")
raise Exception(f"{metatype.__name__} has no metadata found")
for props in ["imports", "providers", "controllers", "exports"]:
metadata["dynamic_metadata"][props] = getattr(metatype, props, [])
return metadata
Expand Down
82 changes: 82 additions & 0 deletions nest/common/provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
from __future__ import annotations

from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Callable, List, Optional, Type, Union


class Scope(str, Enum):
SINGLETON = "singleton"
TRANSIENT = "transient"
REQUEST = "request"


class InjectionToken:
"""Named token for injecting non-class values (strings, primitives, configs)."""

def __init__(self, name: str, description: str = "") -> None:
self.name = name
self.description = description

def __repr__(self) -> str:
return f"InjectionToken({self.name!r})"

def __hash__(self) -> int:
return hash(self.name)

def __eq__(self, other: object) -> bool:
return isinstance(other, InjectionToken) and self.name == other.name


@dataclass
class ProviderDescriptor:
"""Normalized provider definition. Exactly one of use_class/use_value/use_factory/use_existing must be set."""

provide: Union[Type, InjectionToken, str]
use_class: Optional[Type] = None
use_value: Any = None
use_factory: Optional[Callable] = None
use_existing: Optional[Union[Type, InjectionToken]] = None
scope: Scope = Scope.SINGLETON
inject: List[Any] = field(default_factory=list)


def normalize_provider(
provider: Union[Type, dict, ProviderDescriptor],
) -> ProviderDescriptor:
"""Convert any provider form (class, dict, or ProviderDescriptor) to a ProviderDescriptor."""
if isinstance(provider, ProviderDescriptor):
return provider

if isinstance(provider, dict):
provide = provider["provide"]
scope = provider.get("scope", Scope.SINGLETON)
if "useValue" in provider:
return ProviderDescriptor(
provide=provide, use_value=provider["useValue"], scope=scope
)
if "useClass" in provider:
return ProviderDescriptor(
provide=provide, use_class=provider["useClass"], scope=scope
)
if "useFactory" in provider:
return ProviderDescriptor(
provide=provide,
use_factory=provider["useFactory"],
inject=provider.get("inject", []),
scope=scope,
)
if "useExisting" in provider:
return ProviderDescriptor(
provide=provide, use_existing=provider["useExisting"], scope=scope
)
raise ValueError(
f"Invalid provider descriptor: {provider!r}. "
"Must contain one of: useValue, useClass, useFactory, useExisting"
)

if not callable(provider):
raise ValueError(
f"Provider must be a class, dict, or ProviderDescriptor, got {provider!r}"
)
return ProviderDescriptor(provide=provider, use_class=provider, scope=Scope.SINGLETON)
Loading
Loading