Compare commits

..
4 Commits
Author SHA1 Message Date
CI Bot fef9552cbf chore: release v0.3.2 [skip ci] 2026-06-30 14:12:18 +00:00
brian e9b21895f4 Merge pull request '[patch] Fix optional dependency handling for Redis and MinIO adapters' (#19) from cursor/fix-optional-dependencies into main
Code Quality Pipeline / code-quality (push) Successful in 31s
Release on merge to main / release (push) Successful in 8s
Test Python Package / test (push) Successful in 37s
Reviewed-on: https://gitea.lille-vemmelund.dk/brian/python-repositories/pulls/19
2026-06-30 16:08:01 +02:00
Brian Bjarke JensenandCursor f5953906c1 Fix optional-deps import test for full-suite execution.
Code Quality Pipeline / code-quality (pull_request) Successful in 31s
PR Title Check / check-title (pull_request) Successful in 5s
Test Python Package / test (pull_request) Successful in 41s
Run the base-import assertion in a subprocess so integration tests do not pollute sys.modules.

Co-authored-by: Cursor <[email protected]>
2026-06-30 16:05:41 +02:00
Brian Bjarke JensenandCursor 1f02195c27 Delegate top-level adapter imports to adapters package.
Code Quality Pipeline / code-quality (pull_request) Successful in 47s
PR Title Check / check-title (pull_request) Successful in 6s
Test Python Package / test (pull_request) Failing after 1m1s
Keep lazy loading in adapters/__init__.py as the single place to register new adapters.

Co-authored-by: Cursor <[email protected]>
2026-06-30 15:57:52 +02:00
4 changed files with 44 additions and 24 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[project] [project]
name = "python-repositories" name = "python-repositories"
version = "0.3.1" version = "0.3.2"
description = "Various python repository interfaces exposed as a python package." description = "Various python repository interfaces exposed as a python package."
authors = [ authors = [
{ name = "Brian Bjarke Jensen", email = "[email protected]" } { name = "Brian Bjarke Jensen", email = "[email protected]" }
+8 -18
View File
@@ -2,10 +2,10 @@
from __future__ import annotations from __future__ import annotations
import importlib
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
# Interfaces are always available; they have no optional backend dependencies. # Interfaces are always available; they have no optional backend dependencies.
from . import adapters
from .interfaces import ( from .interfaces import (
ConnectionAwareInterface, ConnectionAwareInterface,
ContextAwareInterface, ContextAwareInterface,
@@ -13,34 +13,24 @@ from .interfaces import (
ObjectRepositoryInterface, ObjectRepositoryInterface,
) )
# Adapters are imported only for static type checkers; runtime loading is deferred below. # Adapters are imported only for static type checkers; runtime loading is delegated below.
if TYPE_CHECKING: if TYPE_CHECKING:
from .adapters.minio_adapter import MinioAdapter from .adapters.minio_adapter import MinioAdapter as MinioAdapter
from .adapters.redis_adapter import RedisAdapter from .adapters.redis_adapter import RedisAdapter as RedisAdapter
# Map public adapter names to their defining module and class.
# Each adapter module fails fast with an install hint if its extra is missing.
_LAZY_EXPORTS = {
"RedisAdapter": (".adapters.redis_adapter", "RedisAdapter"),
"MinioAdapter": (".adapters.minio_adapter", "MinioAdapter"),
}
__all__ = [ __all__ = [
"ConnectionAwareInterface", "ConnectionAwareInterface",
"ContextAwareInterface", "ContextAwareInterface",
"JsonRepositoryInterface", "JsonRepositoryInterface",
"ObjectRepositoryInterface", "ObjectRepositoryInterface",
"RedisAdapter", *adapters.__all__,
"MinioAdapter",
] ]
def __getattr__(name: str) -> object: def __getattr__(name: str) -> object:
"""Load adapters on first access so the base package installs without redis/minio.""" """Delegate adapter lookups to adapters; lazy loading is defined there."""
if name in _LAZY_EXPORTS: if name in adapters.__all__:
module_path, attr = _LAZY_EXPORTS[name] return getattr(adapters, name)
module = importlib.import_module(module_path, __package__)
return getattr(module, attr)
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
+6
View File
@@ -9,10 +9,14 @@ from __future__ import annotations
import importlib import importlib
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
# Adapters are imported only for static type checkers; runtime loading is deferred below.
if TYPE_CHECKING: if TYPE_CHECKING:
from .minio_adapter import MinioAdapter from .minio_adapter import MinioAdapter
from .redis_adapter import RedisAdapter from .redis_adapter import RedisAdapter
# Map public adapter names to their defining module and class.
# Each adapter module fails fast with an install hint if its extra is missing.
# When adding a new adapter, update this dict and __all__ only.
_LAZY_EXPORTS = { _LAZY_EXPORTS = {
"RedisAdapter": (".redis_adapter", "RedisAdapter"), "RedisAdapter": (".redis_adapter", "RedisAdapter"),
"MinioAdapter": (".minio_adapter", "MinioAdapter"), "MinioAdapter": (".minio_adapter", "MinioAdapter"),
@@ -25,6 +29,7 @@ __all__ = [
def __getattr__(name: str) -> object: def __getattr__(name: str) -> object:
"""Load an adapter on first access so the base package installs without backend clients."""
if name in _LAZY_EXPORTS: if name in _LAZY_EXPORTS:
module_path, attr = _LAZY_EXPORTS[name] module_path, attr = _LAZY_EXPORTS[name]
module = importlib.import_module(module_path, __package__) module = importlib.import_module(module_path, __package__)
@@ -33,4 +38,5 @@ def __getattr__(name: str) -> object:
def __dir__() -> list[str]: def __dir__() -> list[str]:
"""Expose lazy adapter names in tab completion and dir()."""
return sorted(__all__) return sorted(__all__)
+26 -2
View File
@@ -4,15 +4,19 @@ from __future__ import annotations
import builtins import builtins
import importlib import importlib
import os
import subprocess
import sys import sys
from collections.abc import Callable, Mapping, Sequence from collections.abc import Callable, Mapping, Sequence
from pathlib import Path
from types import ModuleType from types import ModuleType
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
import python_repositories import python_repositories
from python_repositories import JsonRepositoryInterface
_ROOT = Path(__file__).resolve().parents[2]
def _block_backend_import(blocked_prefix: str) -> Callable[..., ModuleType]: def _block_backend_import(blocked_prefix: str) -> Callable[..., ModuleType]:
@@ -33,10 +37,23 @@ def _block_backend_import(blocked_prefix: str) -> Callable[..., ModuleType]:
def test_base_import_does_not_load_adapters() -> None: def test_base_import_does_not_load_adapters() -> None:
"""Interfaces are available without loading adapter modules.""" """Base package import does not eagerly load backend adapter modules."""
script = """
import sys
from python_repositories import JsonRepositoryInterface
assert JsonRepositoryInterface is not None assert JsonRepositoryInterface is not None
assert "python_repositories.adapters.redis_adapter" not in sys.modules assert "python_repositories.adapters.redis_adapter" not in sys.modules
assert "python_repositories.adapters.minio_adapter" not in sys.modules assert "python_repositories.adapters.minio_adapter" not in sys.modules
"""
result = subprocess.run(
[sys.executable, "-c", script],
cwd=_ROOT,
env={**os.environ, "PYTHONPATH": str(_ROOT)},
capture_output=True,
text=True,
)
assert result.returncode == 0, result.stderr or result.stdout
def test_lazy_adapter_load_succeeds_when_extra_present() -> None: def test_lazy_adapter_load_succeeds_when_extra_present() -> None:
@@ -93,3 +110,10 @@ def test_top_level_lazy_import_propagates_minio_import_error() -> None:
): ):
with pytest.raises(ImportError, match=r"python-repositories\[minio\]"): with pytest.raises(ImportError, match=r"python-repositories\[minio\]"):
_ = python_repositories.MinioAdapter _ = python_repositories.MinioAdapter
def test_adapters_subpackage_lazy_import_succeeds() -> None:
"""Adapter subpackage imports delegate to the same lazy loader."""
from python_repositories.adapters import RedisAdapter
assert RedisAdapter.__name__ == "RedisAdapter"