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
12 changes: 11 additions & 1 deletion docs/guides/imports.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ The predicate function receives the full module name (e.g., `"src.adapters.depen

### Factory methods

`PythonModuleLoader` provides three convenient factory methods for common filtering patterns:
`PythonModuleLoader` provides four convenient factory methods for common filtering patterns:

**Filter by prefix:**
```python
Expand All @@ -59,6 +59,16 @@ PythonModuleLoader.startswith("impl_").load(adapters, services)
PythonModuleLoader.endswith("_impl").load(adapters, services)
```

`startswith` and `endswith` only check the last part of the module name (e.g., `"dependency_impl"` for `"src.adapters.dependency_impl"`).

**Filter by pattern:**
```python
# Import only modules inside an "impl" subpackage
PythonModuleLoader.fnmatch("*.impl.*").load(adapters, services)
```

Unlike `startswith` and `endswith`, the patterns are matched against the full module name, using the [`fnmatch`](https://docs.python.org/3/library/fnmatch.html) syntax (`*`, `?`, `[seq]`). Matching is case-sensitive, and `*` also matches dots: `"src.*"` matches `"src.adapters.dependency_impl"`.

**Filter by keyword marker:**
```python
# Import only modules containing a specific comment
Expand Down
8 changes: 8 additions & 0 deletions injection/loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from abc import abstractmethod
from collections.abc import Callable, Iterator, Mapping, Sequence
from dataclasses import dataclass, field
from fnmatch import fnmatchcase
from importlib import import_module
from importlib.util import find_spec
from os.path import isfile
Expand Down Expand Up @@ -99,6 +100,13 @@ def __iter_modules_from(
module = import_module(name) if self.predicate(name) else None
yield name, module

@classmethod
def fnmatch(cls, *patterns: str) -> Self:
def predicate(module_name: str) -> bool:
return any(fnmatchcase(module_name, pattern) for pattern in patterns)

return cls(predicate)

@classmethod
def from_keywords(cls, *keywords: str) -> Self:
"""
Expand Down
7 changes: 7 additions & 0 deletions tests/test_loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ def test_load_with_loaded_module_returns_loader(self) -> None:

assert modules.keys() == {"tests.packages.package1.module1"}

def test_fnmatch_with_pattern_returns_loader(self) -> None:
loader = PythonModuleLoader.fnmatch("*.sub_package.*")

modules = loader.load(package1).modules

assert modules.keys() == {"tests.packages.package1.sub_package.module2"}

def test_from_keywords_with_keyword_returns_loader(self) -> None:
loader = PythonModuleLoader.from_keywords("@injectable")

Expand Down
Loading
Loading