python-neva 0.2.1__tar.gz → 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_neva-0.2.1 → python_neva-0.3.0}/PKG-INFO +3 -1
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/arch/app.py +3 -3
- python_neva-0.3.0/neva/testing/__init__.py +5 -0
- python_neva-0.3.0/neva/testing/fixtures.py +41 -0
- python_neva-0.3.0/neva/testing/http.py +21 -0
- python_neva-0.3.0/neva/testing/test_case.py +20 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/pyproject.toml +9 -2
- {python_neva-0.2.1 → python_neva-0.3.0}/ruff.toml +3 -0
- python_neva-0.3.0/specifications/future_ideas.md +5 -0
- python_neva-0.3.0/tests/__init__.py +1 -0
- python_neva-0.3.0/tests/conftest.py +5 -0
- python_neva-0.3.0/tests/test_example_usage.py +188 -0
- python_neva-0.3.0/tests/test_fixtures.py +135 -0
- python_neva-0.3.0/tests/test_test_case.py +110 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/uv.lock +14 -3
- python_neva-0.2.1/docs/README.md +0 -580
- {python_neva-0.2.1 → python_neva-0.3.0}/.envrc +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/.gitignore +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/.pre-commit-config.yaml +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/.python-version +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/README.md +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/__init__.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/arch/__init__.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/arch/application.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/arch/config.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/arch/facade.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/arch/service_provider.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/config/__init__.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/config/base_providers.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/config/loader.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/config/provider.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/config/repository.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/database/__init__.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/database/config.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/database/manager.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/database/provider.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/database/repository.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/events/__init__.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/events/dispatcher.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/events/event.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/events/event_registry.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/events/interface.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/events/listener.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/obs/__init__.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/obs/logging/__init__.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/obs/logging/manager.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/obs/logging/provider.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/obs/middleware/__init__.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/obs/middleware/correlation.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/obs/middleware/profiler.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/py.typed +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/__init__.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/accessors.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/facade/__init__.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/facade/app.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/facade/app.pyi +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/facade/config.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/facade/config.pyi +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/facade/log.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/facade/log.pyi +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/results.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/strconv.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/neva/support/time.py +0 -0
- {python_neva-0.2.1 → python_neva-0.3.0}/specifications/events.md +0 -0
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-neva
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Requires-Dist: dependency-injector>=4.48.3
|
|
7
7
|
Requires-Dist: dishka>=1.7.2
|
|
8
8
|
Requires-Dist: fastapi-cli>=0.0.16
|
|
9
9
|
Requires-Dist: fastapi[all]>=0.124.0
|
|
10
|
+
Requires-Dist: flexmock>=0.13.0
|
|
10
11
|
Requires-Dist: pyinstrument>=5.1.1
|
|
12
|
+
Requires-Dist: pytest>=9.0.2
|
|
11
13
|
Requires-Dist: structlog>=25.5.0
|
|
12
14
|
Requires-Dist: tortoise-orm[accel]>=0.25.3
|
|
@@ -13,7 +13,6 @@ from typing import Any, Callable
|
|
|
13
13
|
import fastapi
|
|
14
14
|
import dishka
|
|
15
15
|
from dishka.integrations.fastapi import setup_dishka
|
|
16
|
-
from starlette.middleware import Middleware
|
|
17
16
|
from starlette.routing import BaseRoute
|
|
18
17
|
from starlette.types import StatefulLifespan, StatelessLifespan
|
|
19
18
|
|
|
@@ -29,7 +28,7 @@ class App(fastapi.FastAPI):
|
|
|
29
28
|
self,
|
|
30
29
|
*,
|
|
31
30
|
routes: list[BaseRoute] | None = None,
|
|
32
|
-
middlewares: Sequence[
|
|
31
|
+
middlewares: Sequence[type] | None = None,
|
|
33
32
|
lifespan: StatelessLifespan["App"] | StatefulLifespan["App"] | None = None,
|
|
34
33
|
config_path: str | Path | None = None,
|
|
35
34
|
) -> None:
|
|
@@ -59,8 +58,9 @@ class App(fastapi.FastAPI):
|
|
|
59
58
|
docs_url=config.get("app.docs_url", default="/docs").unwrap(),
|
|
60
59
|
redoc_url=config.get("app.redoc_url", default="/redoc").unwrap(),
|
|
61
60
|
lifespan=self._create_lifespan(),
|
|
62
|
-
middleware=middlewares,
|
|
63
61
|
)
|
|
62
|
+
for middleware in middlewares or []:
|
|
63
|
+
self.add_middleware(middleware)
|
|
64
64
|
|
|
65
65
|
setup_dishka(self.application.container, app=self)
|
|
66
66
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Fixtures for testing."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from neva.arch import Application, App
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
def test_config(tmp_path: Path) -> Path:
|
|
12
|
+
"""Returns a test config directory."""
|
|
13
|
+
config_dir = tmp_path / "config"
|
|
14
|
+
config_dir.mkdir()
|
|
15
|
+
_ = (config_dir / "app.py").write_text(
|
|
16
|
+
"""config = { "name": "TestApp", "debug": True, "environment": "testing"}"""
|
|
17
|
+
)
|
|
18
|
+
_ = (config_dir / "providers.py").write_text("""config = { "providers": []}""")
|
|
19
|
+
return config_dir
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.fixture
|
|
23
|
+
async def application(test_config: Path) -> AsyncIterator[Application]:
|
|
24
|
+
"""Pytest fixture for app lifecycle.
|
|
25
|
+
|
|
26
|
+
Yields:
|
|
27
|
+
AsyncIterator[Application]: The application instance.
|
|
28
|
+
"""
|
|
29
|
+
app = Application(config_path=test_config)
|
|
30
|
+
async with app.lifespan():
|
|
31
|
+
yield app
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@pytest.fixture
|
|
35
|
+
def webapp(test_config: Path) -> App:
|
|
36
|
+
"""Pytest fixture for the HTTP Neva app.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
App: The Neva application instance.
|
|
40
|
+
"""
|
|
41
|
+
return App(config_path=test_config)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""HTTP Test helpers."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import AsyncIterator
|
|
4
|
+
from httpx import ASGITransport, AsyncClient
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from neva.arch import App
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@pytest.fixture
|
|
11
|
+
async def http_client(webapp: App) -> AsyncIterator[AsyncClient]:
|
|
12
|
+
"""An async httpx client to test the application.
|
|
13
|
+
|
|
14
|
+
Yields:
|
|
15
|
+
An async httpx client.
|
|
16
|
+
"""
|
|
17
|
+
async with AsyncClient(
|
|
18
|
+
transport=ASGITransport(webapp),
|
|
19
|
+
base_url="http://localhost:8000",
|
|
20
|
+
) as client:
|
|
21
|
+
yield client
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Base test case class."""
|
|
2
|
+
|
|
3
|
+
from typing import TypeVar
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from neva.arch import Application
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestCase:
|
|
13
|
+
"""Base test case with auto-injected app and helper methods."""
|
|
14
|
+
|
|
15
|
+
app: Application
|
|
16
|
+
|
|
17
|
+
@pytest.fixture(autouse=True)
|
|
18
|
+
def _inject_app(self, application: Application) -> None:
|
|
19
|
+
"""Auto-inject app fixture into self.app."""
|
|
20
|
+
self.app = application
|
|
@@ -7,7 +7,7 @@ packages = ["neva"]
|
|
|
7
7
|
|
|
8
8
|
[project]
|
|
9
9
|
name = "python-neva"
|
|
10
|
-
version = "0.
|
|
10
|
+
version = "0.3.0"
|
|
11
11
|
description = "Add your description here"
|
|
12
12
|
readme = "README.md"
|
|
13
13
|
requires-python = ">=3.12"
|
|
@@ -16,7 +16,9 @@ dependencies = [
|
|
|
16
16
|
"dishka>=1.7.2",
|
|
17
17
|
"fastapi-cli>=0.0.16",
|
|
18
18
|
"fastapi[all]>=0.124.0",
|
|
19
|
+
"flexmock>=0.13.0",
|
|
19
20
|
"pyinstrument>=5.1.1",
|
|
21
|
+
"pytest>=9.0.2",
|
|
20
22
|
"structlog>=25.5.0",
|
|
21
23
|
"tortoise-orm[accel]>=0.25.3",
|
|
22
24
|
]
|
|
@@ -27,7 +29,6 @@ dev = [
|
|
|
27
29
|
"poethepoet>=0.38.0",
|
|
28
30
|
"polyfactory>=3.1.0",
|
|
29
31
|
"pre-commit>=4.5.0",
|
|
30
|
-
"pytest>=9.0.2",
|
|
31
32
|
"pytest-asyncio>=0.25.3",
|
|
32
33
|
"pytest-benchmark>=5.2.3",
|
|
33
34
|
"pytest-cov>=7.0.0",
|
|
@@ -41,6 +42,12 @@ asyncio_default_fixture_loop_scope = "function"
|
|
|
41
42
|
testpaths = ["tests"]
|
|
42
43
|
|
|
43
44
|
[tool.poe.tasks]
|
|
45
|
+
# Code quality
|
|
44
46
|
lint = "ruff check"
|
|
45
47
|
fmt = "ruff format"
|
|
46
48
|
tc = "ty check"
|
|
49
|
+
|
|
50
|
+
# Tests
|
|
51
|
+
test = "pytest"
|
|
52
|
+
test-cov = "pytest --cov=neva --cov-report=term-missing"
|
|
53
|
+
test-full = "test-cov tests/"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Neva test suite."""
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from neva.arch import Application
|
|
6
|
+
from neva.config import ConfigRepository
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TestBasicUsage:
|
|
10
|
+
@pytest.mark.asyncio
|
|
11
|
+
async def test_simple_config_access(
|
|
12
|
+
self,
|
|
13
|
+
application: Application,
|
|
14
|
+
) -> None:
|
|
15
|
+
config = application.make(ConfigRepository).unwrap()
|
|
16
|
+
|
|
17
|
+
app_name = config.get("app.name").unwrap()
|
|
18
|
+
assert app_name == "TestApp"
|
|
19
|
+
|
|
20
|
+
is_debug = config.get("app.debug").unwrap()
|
|
21
|
+
assert is_debug is True
|
|
22
|
+
|
|
23
|
+
@pytest.mark.asyncio
|
|
24
|
+
async def test_config_with_default(
|
|
25
|
+
self,
|
|
26
|
+
application: Application,
|
|
27
|
+
) -> None:
|
|
28
|
+
config = application.make(ConfigRepository).unwrap()
|
|
29
|
+
|
|
30
|
+
missing = config.get("app.missing_key", "default_value").unwrap()
|
|
31
|
+
assert missing == "default_value"
|
|
32
|
+
|
|
33
|
+
@pytest.mark.asyncio
|
|
34
|
+
async def test_multiple_config_keys(
|
|
35
|
+
self,
|
|
36
|
+
application: Application,
|
|
37
|
+
) -> None:
|
|
38
|
+
config = application.make(ConfigRepository).unwrap()
|
|
39
|
+
|
|
40
|
+
assert config.get("app.name").unwrap() == "TestApp"
|
|
41
|
+
assert config.get("app.debug").unwrap() is True
|
|
42
|
+
assert config.get("app.environment").unwrap() == "testing"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestCustomConfig:
|
|
46
|
+
@pytest.fixture
|
|
47
|
+
def test_config(
|
|
48
|
+
self,
|
|
49
|
+
tmp_path: Path,
|
|
50
|
+
) -> Path:
|
|
51
|
+
config_dir = tmp_path / "config"
|
|
52
|
+
config_dir.mkdir()
|
|
53
|
+
|
|
54
|
+
_ = (config_dir / "app.py").write_text(
|
|
55
|
+
"""
|
|
56
|
+
config = {
|
|
57
|
+
"name": "CustomApp",
|
|
58
|
+
"debug": False,
|
|
59
|
+
"environment": "custom",
|
|
60
|
+
"custom_feature": True,
|
|
61
|
+
}
|
|
62
|
+
"""
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
_ = (config_dir / "providers.py").write_text("""config = {"providers": []}""")
|
|
66
|
+
|
|
67
|
+
return config_dir
|
|
68
|
+
|
|
69
|
+
@pytest.mark.asyncio
|
|
70
|
+
async def test_uses_custom_config(
|
|
71
|
+
self,
|
|
72
|
+
application: Application,
|
|
73
|
+
) -> None:
|
|
74
|
+
config = application.make(ConfigRepository).unwrap()
|
|
75
|
+
|
|
76
|
+
assert config.get("app.name").unwrap() == "CustomApp"
|
|
77
|
+
assert config.get("app.debug").unwrap() is False
|
|
78
|
+
assert config.get("app.environment").unwrap() == "custom"
|
|
79
|
+
assert config.get("app.custom_feature").unwrap() is True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class TestConfigManipulation:
|
|
83
|
+
@pytest.mark.asyncio
|
|
84
|
+
async def test_add_database_config(
|
|
85
|
+
self,
|
|
86
|
+
test_config: Path,
|
|
87
|
+
) -> None:
|
|
88
|
+
_ = (test_config / "database.py").write_text(
|
|
89
|
+
"""
|
|
90
|
+
config = {
|
|
91
|
+
"default": "sqlite",
|
|
92
|
+
"connections": {
|
|
93
|
+
"sqlite": {
|
|
94
|
+
"driver": "sqlite",
|
|
95
|
+
"database": ":memory:",
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
"""
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
app = Application(config_path=test_config)
|
|
103
|
+
|
|
104
|
+
async with app.lifespan():
|
|
105
|
+
config = app.make(ConfigRepository).unwrap()
|
|
106
|
+
|
|
107
|
+
# Verify database config is loaded
|
|
108
|
+
assert config.get("database.default").unwrap() == "sqlite"
|
|
109
|
+
assert config.get("database.connections.sqlite.driver").unwrap() == "sqlite"
|
|
110
|
+
assert (
|
|
111
|
+
config.get("database.connections.sqlite.database").unwrap()
|
|
112
|
+
== ":memory:"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@pytest.mark.asyncio
|
|
116
|
+
async def test_add_multiple_config_files(
|
|
117
|
+
self,
|
|
118
|
+
test_config: Path,
|
|
119
|
+
) -> None:
|
|
120
|
+
_ = (test_config / "cache.py").write_text(
|
|
121
|
+
"""config = {"driver": "memory", "ttl": 3600}"""
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
_ = (test_config / "logging.py").write_text(
|
|
125
|
+
"""config = {"level": "DEBUG", "format": "json"}"""
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
app = Application(config_path=test_config)
|
|
129
|
+
|
|
130
|
+
async with app.lifespan():
|
|
131
|
+
config = app.make(ConfigRepository).unwrap()
|
|
132
|
+
|
|
133
|
+
# Verify both configs are loaded
|
|
134
|
+
assert config.get("cache.driver").unwrap() == "memory"
|
|
135
|
+
assert config.get("cache.ttl").unwrap() == 3600
|
|
136
|
+
assert config.get("logging.level").unwrap() == "DEBUG"
|
|
137
|
+
assert config.get("logging.format").unwrap() == "json"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TestAppLifecycle:
|
|
141
|
+
@pytest.mark.asyncio
|
|
142
|
+
async def test_manual_app_creation(
|
|
143
|
+
self,
|
|
144
|
+
test_config: Path,
|
|
145
|
+
) -> None:
|
|
146
|
+
app = Application(config_path=test_config)
|
|
147
|
+
|
|
148
|
+
async with app.lifespan():
|
|
149
|
+
config = app.make(ConfigRepository).unwrap()
|
|
150
|
+
assert config.get("app.name").unwrap() == "TestApp"
|
|
151
|
+
|
|
152
|
+
@pytest.mark.asyncio
|
|
153
|
+
async def test_multiple_apps_in_one_test(
|
|
154
|
+
self,
|
|
155
|
+
test_config: Path,
|
|
156
|
+
) -> None:
|
|
157
|
+
app1 = Application(config_path=test_config)
|
|
158
|
+
async with app1.lifespan():
|
|
159
|
+
config1 = app1.make(ConfigRepository).unwrap()
|
|
160
|
+
assert config1.get("app.name").unwrap() == "TestApp"
|
|
161
|
+
|
|
162
|
+
app2 = Application(config_path=test_config)
|
|
163
|
+
async with app2.lifespan():
|
|
164
|
+
config2 = app2.make(ConfigRepository).unwrap()
|
|
165
|
+
assert config2.get("app.name").unwrap() == "TestApp"
|
|
166
|
+
|
|
167
|
+
assert app1 is not app2
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TestIsolation:
|
|
171
|
+
@pytest.mark.asyncio
|
|
172
|
+
async def test_isolation_test_one(
|
|
173
|
+
self,
|
|
174
|
+
application: Application,
|
|
175
|
+
) -> None:
|
|
176
|
+
config = application.make(ConfigRepository).unwrap()
|
|
177
|
+
|
|
178
|
+
original_name = config.get("app.name").unwrap()
|
|
179
|
+
assert original_name == "TestApp"
|
|
180
|
+
|
|
181
|
+
@pytest.mark.asyncio
|
|
182
|
+
async def test_isolation_test_two(
|
|
183
|
+
self,
|
|
184
|
+
application: Application,
|
|
185
|
+
) -> None:
|
|
186
|
+
config = application.make(ConfigRepository).unwrap()
|
|
187
|
+
|
|
188
|
+
assert config.get("app.name").unwrap() == "TestApp"
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Test the testing fixtures themselves."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from neva.arch import Application
|
|
8
|
+
from neva.config import ConfigRepository
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestTestConfig:
|
|
12
|
+
"""Test the test_config fixture."""
|
|
13
|
+
|
|
14
|
+
def test_creates_config_directory(
|
|
15
|
+
self,
|
|
16
|
+
test_config: Path,
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Test that test_config creates a config directory."""
|
|
19
|
+
assert test_config.exists()
|
|
20
|
+
assert test_config.is_dir()
|
|
21
|
+
assert test_config.name == "config"
|
|
22
|
+
|
|
23
|
+
def test_creates_app_config_file(
|
|
24
|
+
self,
|
|
25
|
+
test_config: Path,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Test that app.py config file is created."""
|
|
28
|
+
app_config = test_config / "app.py"
|
|
29
|
+
assert app_config.exists()
|
|
30
|
+
assert app_config.is_file()
|
|
31
|
+
|
|
32
|
+
content = app_config.read_text()
|
|
33
|
+
assert "TestApp" in content
|
|
34
|
+
assert "debug" in content
|
|
35
|
+
assert "testing" in content
|
|
36
|
+
|
|
37
|
+
def test_creates_providers_config_file(
|
|
38
|
+
self,
|
|
39
|
+
test_config: Path,
|
|
40
|
+
) -> None:
|
|
41
|
+
"""Test that providers.py config file is created."""
|
|
42
|
+
providers_config = test_config / "providers.py"
|
|
43
|
+
assert providers_config.exists()
|
|
44
|
+
assert providers_config.is_file()
|
|
45
|
+
|
|
46
|
+
content = providers_config.read_text()
|
|
47
|
+
assert "providers" in content
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestAppFixture:
|
|
51
|
+
@pytest.mark.asyncio
|
|
52
|
+
async def test_app_is_application_instance(
|
|
53
|
+
self,
|
|
54
|
+
application: Application,
|
|
55
|
+
) -> None:
|
|
56
|
+
assert isinstance(application, Application)
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
async def test_app_config_is_loaded(
|
|
60
|
+
self,
|
|
61
|
+
application: Application,
|
|
62
|
+
) -> None:
|
|
63
|
+
config_result = application.make(ConfigRepository)
|
|
64
|
+
assert config_result.is_ok
|
|
65
|
+
|
|
66
|
+
config = config_result.unwrap()
|
|
67
|
+
assert config is not None
|
|
68
|
+
|
|
69
|
+
assert config.get("app.name").unwrap() == "TestApp"
|
|
70
|
+
assert config.get("app.debug").unwrap() is True
|
|
71
|
+
assert config.get("app.environment").unwrap() == "testing"
|
|
72
|
+
|
|
73
|
+
@pytest.mark.asyncio
|
|
74
|
+
async def test_app_is_fresh_per_test_first(
|
|
75
|
+
self,
|
|
76
|
+
application: Application,
|
|
77
|
+
) -> None:
|
|
78
|
+
type(application).__test_marker = "first_test"
|
|
79
|
+
assert type(application).__test_marker == "first_test"
|
|
80
|
+
|
|
81
|
+
@pytest.mark.asyncio
|
|
82
|
+
async def test_app_is_fresh_per_test_second(
|
|
83
|
+
self,
|
|
84
|
+
application: Application,
|
|
85
|
+
) -> None:
|
|
86
|
+
assert not hasattr(type(application), "__test_marker")
|
|
87
|
+
|
|
88
|
+
@pytest.mark.asyncio
|
|
89
|
+
async def test_app_can_resolve_services(
|
|
90
|
+
self,
|
|
91
|
+
application: Application,
|
|
92
|
+
) -> None:
|
|
93
|
+
config_result = application.make(ConfigRepository)
|
|
94
|
+
assert config_result.is_ok
|
|
95
|
+
|
|
96
|
+
config = config_result.unwrap()
|
|
97
|
+
assert config is not None
|
|
98
|
+
assert isinstance(config, ConfigRepository)
|
|
99
|
+
|
|
100
|
+
@pytest.mark.asyncio
|
|
101
|
+
async def test_multiple_resolutions_return_same_instance(
|
|
102
|
+
self,
|
|
103
|
+
application: Application,
|
|
104
|
+
) -> None:
|
|
105
|
+
config1 = application.make(ConfigRepository).unwrap()
|
|
106
|
+
config2 = application.make(ConfigRepository).unwrap()
|
|
107
|
+
|
|
108
|
+
assert config1 is config2
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class TestFixtureIntegration:
|
|
112
|
+
@pytest.mark.asyncio
|
|
113
|
+
async def test_test_config_and_app_work_together(
|
|
114
|
+
self,
|
|
115
|
+
test_config: Path,
|
|
116
|
+
application: Application,
|
|
117
|
+
) -> None:
|
|
118
|
+
config = application.make(ConfigRepository).unwrap()
|
|
119
|
+
|
|
120
|
+
assert config.get("app.name").unwrap() == "TestApp"
|
|
121
|
+
|
|
122
|
+
app_config_file = test_config / "app.py"
|
|
123
|
+
assert app_config_file.exists()
|
|
124
|
+
|
|
125
|
+
@pytest.mark.asyncio
|
|
126
|
+
async def test_can_customize_config_in_test(self, test_config: Path) -> None:
|
|
127
|
+
_ = (test_config / "custom.py").write_text(
|
|
128
|
+
"""config = {"custom_key": "custom_value"}"""
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
app = Application(config_path=test_config)
|
|
132
|
+
|
|
133
|
+
async with app.lifespan():
|
|
134
|
+
config = app.make(ConfigRepository).unwrap()
|
|
135
|
+
assert config.get("custom.custom_key").unwrap() == "custom_value"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Test the TestCase base class."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from neva import UnwrapError
|
|
7
|
+
from neva.arch import Application
|
|
8
|
+
from neva.config import ConfigRepository
|
|
9
|
+
from neva.testing import TestCase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestTestCase(TestCase):
|
|
13
|
+
@pytest.mark.asyncio
|
|
14
|
+
async def test_app_is_injected(self) -> None:
|
|
15
|
+
assert hasattr(self, "app")
|
|
16
|
+
assert isinstance(self.app, Application)
|
|
17
|
+
|
|
18
|
+
@pytest.mark.asyncio
|
|
19
|
+
async def test_make_resolves_services(self) -> None:
|
|
20
|
+
config = self.app.make(ConfigRepository).unwrap()
|
|
21
|
+
|
|
22
|
+
assert config is not None
|
|
23
|
+
assert isinstance(config, ConfigRepository)
|
|
24
|
+
|
|
25
|
+
@pytest.mark.asyncio
|
|
26
|
+
async def test_make_unwraps_result(self) -> None:
|
|
27
|
+
config = self.app.make(ConfigRepository).unwrap()
|
|
28
|
+
|
|
29
|
+
name = config.get("app.name").unwrap()
|
|
30
|
+
assert name == "TestApp"
|
|
31
|
+
|
|
32
|
+
@pytest.mark.asyncio
|
|
33
|
+
async def test_make_raises_on_error(self) -> None:
|
|
34
|
+
class NonExistentService:
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
with pytest.raises(UnwrapError):
|
|
38
|
+
_ = self.app.make(NonExistentService).unwrap()
|
|
39
|
+
|
|
40
|
+
@pytest.mark.asyncio
|
|
41
|
+
async def test_app_isolation_between_tests_first(self) -> None:
|
|
42
|
+
self.app.__test_marker = "first" # type: ignore
|
|
43
|
+
assert self.app.__test_marker == "first" # type: ignore
|
|
44
|
+
|
|
45
|
+
@pytest.mark.asyncio
|
|
46
|
+
async def test_app_isolation_between_tests_second(self) -> None:
|
|
47
|
+
assert not hasattr(self.app, "__test_marker")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TestTestCaseWithCustomConfig(TestCase):
|
|
51
|
+
@pytest.fixture
|
|
52
|
+
def test_config(self, tmp_path: Path) -> Path:
|
|
53
|
+
config_dir = tmp_path / "config"
|
|
54
|
+
config_dir.mkdir()
|
|
55
|
+
|
|
56
|
+
_ = (config_dir / "app.py").write_text(
|
|
57
|
+
"""
|
|
58
|
+
config = {
|
|
59
|
+
"name": "CustomTestApp",
|
|
60
|
+
"custom_feature": True,
|
|
61
|
+
}
|
|
62
|
+
"""
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
_ = (config_dir / "providers.py").write_text('config = {"providers": []}')
|
|
66
|
+
|
|
67
|
+
return config_dir
|
|
68
|
+
|
|
69
|
+
@pytest.mark.asyncio
|
|
70
|
+
async def test_uses_custom_config(self) -> None:
|
|
71
|
+
config = self.app.make(ConfigRepository).unwrap()
|
|
72
|
+
|
|
73
|
+
assert config.get("app.name").unwrap() == "CustomTestApp"
|
|
74
|
+
assert config.get("app.custom_feature").unwrap() is True
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TestTestCaseInheritance:
|
|
78
|
+
class CustomTestCase(TestCase):
|
|
79
|
+
def get_app_name(self) -> str:
|
|
80
|
+
config = self.app.make(ConfigRepository).unwrap()
|
|
81
|
+
return config.get("app.name").unwrap()
|
|
82
|
+
|
|
83
|
+
def is_debug_mode(self) -> bool:
|
|
84
|
+
config = self.app.make(ConfigRepository).unwrap()
|
|
85
|
+
return config.get("app.debug").unwrap()
|
|
86
|
+
|
|
87
|
+
@pytest.mark.asyncio
|
|
88
|
+
async def test_custom_helpers_work(self) -> None:
|
|
89
|
+
test_case = self.CustomTestCase()
|
|
90
|
+
|
|
91
|
+
from neva.arch import Application
|
|
92
|
+
from pathlib import Path
|
|
93
|
+
from tempfile import TemporaryDirectory
|
|
94
|
+
|
|
95
|
+
with TemporaryDirectory() as tmp:
|
|
96
|
+
config_dir = Path(tmp) / "config"
|
|
97
|
+
config_dir.mkdir()
|
|
98
|
+
_ = (config_dir / "app.py").write_text(
|
|
99
|
+
"""config = {"name": "TestApp", "debug":"""
|
|
100
|
+
+ """ True, "environment": "testing"}"""
|
|
101
|
+
)
|
|
102
|
+
_ = (config_dir / "providers.py").write_text('config = {"providers": []}')
|
|
103
|
+
|
|
104
|
+
app = Application(config_path=config_dir)
|
|
105
|
+
async with app.lifespan():
|
|
106
|
+
test_case.app = app
|
|
107
|
+
|
|
108
|
+
# Use custom helpers
|
|
109
|
+
assert test_case.get_app_name() == "TestApp"
|
|
110
|
+
assert test_case.is_debug_mode() is True
|