Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion ellar/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Ellar - Python ASGI web framework for building fast, efficient, and scalable RESTful APIs and server-side applications."""

__version__ = "0.9.2"
__version__ = "0.9.3"
21 changes: 12 additions & 9 deletions ellar/testing/dependency_analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

if t.TYPE_CHECKING: # pragma: no cover
from ellar.common import ControllerBase
from ellar.core import ForwardRefModule, ModuleBase
from ellar.core import ForwardRefModule, ModuleBase, ModuleSetup
from ellar.di import ModuleTreeManager


Expand Down Expand Up @@ -60,6 +60,13 @@ def __init__(self, application_module: t.Union[t.Type["ModuleBase"], str]):

self._module_tree = self._build_module_tree()

def get_application_module_providers(self) -> t.List[t.Type]:
"""Get all provider types from the ApplicationModule tree"""
mod_data = self._module_tree.get_app_module()
if mod_data:
return list(mod_data.providers.values())
return []

def _build_module_tree(self) -> "ModuleTreeManager":
"""Build complete module tree for ApplicationModule"""
from ellar.app import AppFactory
Expand Down Expand Up @@ -164,7 +171,7 @@ def collect_dependencies(mod: t.Type["ModuleBase"]) -> None:

def resolve_forward_ref(
self, forward_ref: "ForwardRefModule"
) -> t.Optional[t.Type["ModuleBase"]]:
) -> t.Optional["ModuleSetup"]:
"""
Resolve a ForwardRefModule to its actual module from ApplicationModule tree

Expand All @@ -181,7 +188,7 @@ def resolve_forward_ref(
filter_item=lambda data: True,
find_predicate=lambda data: data.name == forward_ref.module_name,
)
return t.cast(t.Type["ModuleBase"], result.value.module) if result else None
return t.cast("ModuleSetup", result.value) if result else None

elif hasattr(forward_ref, "module") and forward_ref.module:
# Module can be a Type or a string import path
Expand All @@ -197,12 +204,8 @@ def resolve_forward_ref(

# Search for this module type in the tree
module_data = self._module_tree.get_module(module_cls)
return (
t.cast(t.Type["ModuleBase"], module_data.value.module)
if module_data
else None
)

if module_data:
return t.cast("ModuleSetup", module_data.value)
return None


Expand Down
41 changes: 28 additions & 13 deletions ellar/testing/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
constants,
)
from ellar.common.types import T
from ellar.core import ModuleBase
from ellar.core import ModuleBase, ModuleSetup
from ellar.core.routing import EllarControllerMount
from ellar.di import ProviderConfig
from ellar.reflect import reflect
Expand Down Expand Up @@ -166,16 +166,12 @@ def create_test_module(
app_analyzer = ApplicationModuleDependencyAnalyzer(application_module)
controller_analyzer = ControllerDependencyAnalyzer()

# 1. Resolve ForwardRefs in registered modules
resolved_modules = cls._resolve_forward_refs(modules_list, app_analyzer)
modules_list = resolved_modules

# 2. Analyze controllers and find required modules (with recursive dependencies)
required_modules = cls._analyze_and_resolve_controller_dependencies(
controllers, controller_analyzer, app_analyzer
)

# 3. Add required modules that aren't already registered
# 2. Add required modules that aren't already registered
# Use type comparison to avoid duplicates
existing_module_types = {
m if isinstance(m, type) else m.module if hasattr(m, "module") else m
Expand All @@ -186,6 +182,15 @@ def create_test_module(
modules_list.append(required_module)
existing_module_types.add(required_module)

# 4. Resolve ForwardRefs in registered modules
resolved_modules = cls._resolve_forward_refs(modules_list, app_analyzer)
modules_list.extend(resolved_modules)

providers = list(providers)
# 5. Add application module providers, since this is the root module
# and it will be used to resolve dependencies
providers.extend(app_analyzer.get_application_module_providers())

# Create the module with complete dependency list
module = Module(
modules=modules_list,
Expand Down Expand Up @@ -229,20 +234,30 @@ def _resolve_forward_refs(
modules: t.List[t.Any],
app_analyzer: "ApplicationModuleDependencyAnalyzer",
) -> t.List[t.Any]:
"""Resolve ForwardRefModule instances from ApplicationModule"""
"""Resolve ForwardRefModule instances from ApplicationModule recursively"""
from ellar.core import ForwardRefModule

resolved = []
for module in modules:
# Resolve current module if it's a ForwardRefModule
if isinstance(module, ForwardRefModule):
actual_module = app_analyzer.resolve_forward_ref(module)
if actual_module:
resolved.append(actual_module)
else:
# Keep original if can't resolve (might be test-specific)
resolved.append(module)
current_module = actual_module.module
resolved.append(actual_module)
elif isinstance(module, ModuleSetup):
current_module = module.module
else:
resolved.append(module)
current_module = module

# Recursively resolve forward refs in module's dependencies
registered_modules = (
reflect.get_metadata(constants.MODULE_METADATA.MODULES, current_module)
or []
)
if registered_modules:
resolved.extend(
cls._resolve_forward_refs(registered_modules, app_analyzer)
)

return resolved

Expand Down
124 changes: 121 additions & 3 deletions tests/test_testing_dependency_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,13 +276,39 @@ def test_application_module_analyzer_get_module_dependencies_none():
assert len(dependencies) == 0


def test_application_module_analyzer_get_application_module_providers():
"""Test getting providers from ApplicationModule"""
from ellar.di import ProviderConfig

@injectable
class AppLevelService:
pass

@Module(
name="TestAppModuleWithProviders",
modules=[AuthModule],
providers=[ProviderConfig(AppLevelService, use_class=AppLevelService)],
)
class TestAppModuleWithProviders(ModuleBase):
pass

analyzer = ApplicationModuleDependencyAnalyzer(TestAppModuleWithProviders)
providers = analyzer.get_application_module_providers()

# Should include AppLevelService
assert AppLevelService in providers or any(
hasattr(p, "get_type") and p.get_type() == AppLevelService for p in providers
)


# ============================================================================
# Unit Tests: ForwardRefModule Resolution
# ============================================================================


def test_forward_ref_resolution_by_type():
"""Test resolving ForwardRefModule by type"""
from ellar.core.modules import ModuleSetup

# Need to have DatabaseModule actually registered in the application tree
@Module(
Expand All @@ -298,7 +324,9 @@ class ForwardRefTestModule(ModuleBase):
forward_ref = ForwardRefModule(module=DatabaseModule)
resolved = analyzer.resolve_forward_ref(forward_ref)

assert resolved == DatabaseModule
# Should return a ModuleSetup instance
assert isinstance(resolved, ModuleSetup)
assert resolved.module == DatabaseModule


def test_forward_ref_resolution_by_name():
Expand All @@ -318,7 +346,8 @@ class ForwardRefTestModule2(ModuleBase):
forward_ref = ForwardRefModule(module_name="DatabaseModule")
resolved = analyzer.resolve_forward_ref(forward_ref)

assert resolved == DatabaseModule
# When resolving by name, it returns the module type directly
assert resolved.module == DatabaseModule


def test_forward_ref_resolution_not_found():
Expand All @@ -336,6 +365,52 @@ class ForwardRefTestModule3(ModuleBase):
assert resolved is None


def test_resolve_forward_refs_handles_module_setup():
"""Test that _resolve_forward_refs properly handles ModuleSetup instances"""
from ellar.testing.module import Test

@Module(name="TestModuleForSetup", modules=[AuthModule, DatabaseModule])
class TestModuleForSetup(ModuleBase):
pass

analyzer = ApplicationModuleDependencyAnalyzer(TestModuleForSetup)

# Pass ForwardRefModule instances that will be resolved
forward_ref_auth = ForwardRefModule(module=AuthModule)
forward_ref_db = ForwardRefModule(module=DatabaseModule)

modules = [forward_ref_auth, forward_ref_db]
resolved = Test._resolve_forward_refs(modules, analyzer)

# Should resolve both ForwardRefModules (and potentially their dependencies)
assert len(resolved) >= 2


def test_resolve_forward_refs_recursive_extension():
"""Test that _resolve_forward_refs recursively extends with nested modules"""
from ellar.testing.module import Test

# DatabaseModule has LoggingModule as dependency
@Module(
name="TestModuleForRecursive",
modules=[DatabaseModule, AuthModule],
)
class TestModuleForRecursive(ModuleBase):
pass

analyzer = ApplicationModuleDependencyAnalyzer(TestModuleForRecursive)

# Start with ForwardRefModule to DatabaseModule (which has LoggingModule as dependency)
forward_ref_db = ForwardRefModule(module=DatabaseModule)
modules = [forward_ref_db]
resolved = Test._resolve_forward_refs(modules, analyzer)

# Should return resolved DatabaseModule (and potentially nested dependencies)
# The exact count depends on whether DatabaseModule's LoggingModule dependency
# has any ForwardRefModules in its metadata
assert len(resolved) >= 1


# ============================================================================
# Integration Tests: Test.create_test_module()
# ============================================================================
Expand Down Expand Up @@ -469,7 +544,7 @@ class TestAppWithForwardRef(ModuleBase):

tm = Test.create_test_module(
controllers=[UserController],
modules=[ModuleWithForwardRef], # Contains ForwardRef to AuthModule
# Don't manually add ForwardRef module - let auto-resolution handle it
application_module=TestAppWithForwardRef,
)

Expand Down Expand Up @@ -622,6 +697,49 @@ def test_create_test_module_with_import_string_application_module(reflect_contex
assert isinstance(controller.auth_service, IAuthService)


def test_create_test_module_includes_application_module_providers(reflect_context):
"""Test that test module includes providers from ApplicationModule"""

@injectable
class AppLevelService:
def get_value(self):
return "app_level"

@Module(
name="AppModuleWithProviders",
modules=[AuthModule],
providers=[ProviderConfig(AppLevelService, use_class=AppLevelService)],
)
class AppModuleWithProviders(ModuleBase):
pass

@Controller()
class ControllerUsingAppService:
def __init__(self, app_service: AppLevelService):
self.app_service = app_service

@get("/test")
def test_endpoint(self):
return {"value": self.app_service.get_value()}

tm = Test.create_test_module(
controllers=[ControllerUsingAppService],
application_module=AppModuleWithProviders,
)

tm.create_application()

# Should be able to get the app-level service
app_service = tm.get(AppLevelService)
assert app_service is not None
assert app_service.get_value() == "app_level"

# Controller should also work
controller = tm.get(ControllerUsingAppService)
assert controller is not None
assert isinstance(controller.app_service, AppLevelService)


def test_application_module_analyzer_with_import_string():
"""Test that ApplicationModuleDependencyAnalyzer accepts import strings"""
import_string = "tests.test_testing_dependency_resolution:ApplicationModule"
Expand Down