pytest-aiohutils 0.22.4__py3-none-any.whl

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.
@@ -0,0 +1,228 @@
1
+ __version__ = '0.22.4'
2
+ import atexit
3
+ from collections.abc import Iterator, Mapping
4
+ from inspect import iscoroutinefunction
5
+ from itertools import cycle
6
+ from pathlib import Path
7
+ from typing import TypedDict
8
+ from unittest.mock import patch
9
+
10
+ from aiohutils.session import ClientSession, SessionManager
11
+ from pydantic import ConfigDict
12
+ from pydantic.type_adapter import TypeAdapter
13
+ from pytest import Config, Function, Parser, StashKey, fixture
14
+
15
+ # Define a Stash Key for storing calculated configuration (Pytest idiomatic way)
16
+ CONFIG_KEY = StashKey[dict]()
17
+
18
+
19
+ def pytest_addoption(parser: Parser):
20
+ """Registers the command line and ini-file options."""
21
+ group = parser.getgroup('testconfig')
22
+ group.addoption(
23
+ '--record',
24
+ action='store_true',
25
+ default=False,
26
+ dest='RECORD_MODE',
27
+ help='Enable record mode for tests (saves new responses).',
28
+ )
29
+ group.addoption(
30
+ '--offline',
31
+ action='store_true',
32
+ default=True,
33
+ dest='OFFLINE_MODE',
34
+ help='Enable offline mode (only uses saved data).',
35
+ )
36
+ group.addoption(
37
+ '--remove-unused-data',
38
+ action='store_true',
39
+ default=False,
40
+ dest='REMOVE_UNUSED_TESTDATA',
41
+ help='Remove test data files not used during the run.',
42
+ )
43
+
44
+
45
+ _remove_unused_testdata: bool
46
+ testdata: Path
47
+ tests_path: Path
48
+
49
+
50
+ def pytest_configure(config: Config):
51
+ """Called after command line options are parsed and configuration is loaded."""
52
+ global tests_path, _remove_unused_testdata, testdata
53
+
54
+ RECORD_MODE = config.getoption('RECORD_MODE')
55
+ # Note: If RECORD_MODE is true, we always disable OFFLINE_MODE
56
+ OFFLINE_MODE = config.getoption('OFFLINE_MODE') and not RECORD_MODE
57
+ REMOVE_UNUSED_TESTDATA = _remove_unused_testdata = (
58
+ config.getoption('REMOVE_UNUSED_TESTDATA') and OFFLINE_MODE
59
+ )
60
+
61
+ # Store calculated config in the Pytest config stash
62
+ config.stash[CONFIG_KEY] = {
63
+ 'RECORD_MODE': RECORD_MODE,
64
+ 'OFFLINE_MODE': OFFLINE_MODE,
65
+ 'REMOVE_UNUSED_TESTDATA': REMOVE_UNUSED_TESTDATA,
66
+ }
67
+
68
+ # Set the tests_path to the root of the test directory (e.g., /project_root/tests),
69
+ # using Path objects for clean, cross-platform path construction.
70
+ tests_path = Path(config.rootpath) / 'tests'
71
+ testdata = tests_path / 'testdata'
72
+
73
+ if _remove_unused_testdata:
74
+ atexit.register(remove_unused_testdata)
75
+
76
+
77
+ class TestConfig(TypedDict):
78
+ RECORD_MODE: bool
79
+ OFFLINE_MODE: bool
80
+ REMOVE_UNUSED_TESTDATA: bool
81
+ TESTS_PATH: Path
82
+
83
+
84
+ @fixture(scope='session')
85
+ def test_config(request) -> TestConfig:
86
+ """Provides test configuration variables to test functions."""
87
+ # Retrieve configuration from the Pytest Stash using the standard 'request' fixture
88
+ config_data = request.config.stash.get(CONFIG_KEY, {})
89
+
90
+ # Fallback in case fixture is called before pytest_configure for some reason
91
+ return {
92
+ 'RECORD_MODE': config_data.get('RECORD_MODE', False),
93
+ 'OFFLINE_MODE': config_data.get('OFFLINE_MODE', True),
94
+ 'REMOVE_UNUSED_TESTDATA': config_data.get(
95
+ 'REMOVE_UNUSED_TESTDATA', False
96
+ ),
97
+ 'TESTS_PATH': tests_path,
98
+ }
99
+
100
+
101
+ class EqualToEverything:
102
+ """A placeholder object that always evaluates as equal."""
103
+
104
+ def __eq__(self, other):
105
+ return True
106
+
107
+
108
+ class FakeResponse:
109
+ """A mock response object for offline mode."""
110
+
111
+ __slots__ = 'files'
112
+ files: Iterator
113
+ url = EqualToEverything()
114
+ history = ()
115
+
116
+ @property
117
+ def file(self) -> Path:
118
+ return next(self.files)
119
+
120
+ async def read(self) -> bytes:
121
+ return self.file.read_bytes()
122
+
123
+ def raise_for_status(self):
124
+ pass
125
+
126
+ async def text(self) -> str:
127
+ return (await self.read()).decode()
128
+
129
+
130
+ @fixture(scope='session')
131
+ # DEPENDENCY INJECTION: session now depends on test_config to get its mode
132
+ async def session(test_config: TestConfig):
133
+ """Pytest fixture to mock or record HTTP sessions."""
134
+ # Use injected config variables instead of globals
135
+ RECORD_MODE = test_config['RECORD_MODE']
136
+ OFFLINE_MODE = test_config['OFFLINE_MODE']
137
+
138
+ if OFFLINE_MODE:
139
+
140
+ class FakeSession:
141
+ @staticmethod
142
+ async def get(*_, **__):
143
+ return FakeResponse()
144
+
145
+ orig_session = SessionManager.session
146
+ SessionManager.session = FakeSession() # type: ignore
147
+ yield
148
+ SessionManager.session = orig_session # type: ignore
149
+ return
150
+
151
+ if RECORD_MODE:
152
+ original_get = ClientSession.get
153
+
154
+ async def recording_get(*args, **kwargs):
155
+ resp = await original_get(*args, **kwargs)
156
+ content = await resp.read()
157
+ FakeResponse().file.write_bytes(content)
158
+ return resp
159
+
160
+ ClientSession.get = recording_get # type: ignore
161
+
162
+ yield
163
+ ClientSession.get = original_get
164
+ return
165
+
166
+ yield
167
+ return
168
+
169
+
170
+ def pytest_collection_modifyitems(items: list[Function]):
171
+ """Automatically apply the 'session' fixture to all async tests."""
172
+ for item in items:
173
+ if iscoroutinefunction(item.obj):
174
+ item.fixturenames.append('session')
175
+
176
+
177
+ def remove_unused_testdata():
178
+ """Removes test data files that were not used during the test run."""
179
+ unused_testdata = {f.name for f in testdata.iterdir()} - USED_FILENAMES
180
+
181
+ if not unused_testdata:
182
+ print('REMOVE_UNUSED_TESTDATA: no action required')
183
+ return
184
+ for filename in unused_testdata:
185
+ (testdata / filename).unlink()
186
+ print(f'REMOVE_UNUSED_TESTDATA: removed {filename}')
187
+
188
+
189
+ USED_FILENAMES = set()
190
+ # atexit.register is now called conditionally in pytest_configure
191
+
192
+
193
+ def file(filename: str):
194
+ """Mocks the response files for a single file test case."""
195
+ # Checks if cleanup is active using the minimal global flag set in pytest_configure
196
+ if _remove_unused_testdata:
197
+ USED_FILENAMES.add(filename)
198
+
199
+ return patch.object(
200
+ FakeResponse,
201
+ 'files',
202
+ cycle([testdata / filename]),
203
+ )
204
+
205
+
206
+ def files(*filenames: str):
207
+ """Mocks the response files for sequential file test cases."""
208
+ # Checks if cleanup is active using the minimal global flag set in pytest_configure
209
+ if _remove_unused_testdata:
210
+ for filename in filenames:
211
+ USED_FILENAMES.add(filename)
212
+
213
+ return patch.object(
214
+ FakeResponse,
215
+ 'files',
216
+ (testdata / filename for filename in filenames),
217
+ )
218
+
219
+
220
+ strict_config = ConfigDict(strict=True)
221
+
222
+
223
+ def validate_dict(dct: Mapping, typed_dct: type[TypedDict]): # type: ignore
224
+ # A trick to disallow extra keys. See
225
+ # https://stackoverflow.com/questions/77165374/runtime-checking-for-extra-keys-in-typeddict
226
+ # https://docs.pydantic.dev/2.4/concepts/strict_mode/#dataclasses-and-typeddict
227
+ typed_dct.__pydantic_config__ = strict_config # type: ignore
228
+ TypeAdapter(typed_dct).validate_python(dct, strict=True)
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.3
2
+ Name: pytest-aiohutils
3
+ Version: 0.22.4
4
+ Summary: Pytest plugin providing fixtures and configuration for aiohutils projects (offline, record, cleanup modes).
5
+ Author: 5j9
6
+ Author-email: 5j9 <5j9@users.noreply.github.com>
7
+ License: GNU General Public License v3 (GPLv3)
8
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Requires-Dist: aiohutils
11
+ Requires-Dist: pytest
12
+ Requires-Dist: pytest-asyncio
13
+ Requires-Dist: pydantic
14
+ Requires-Python: >=3.12
15
+ Project-URL: Homepage, https://github.com/5j9/pytest-aiohutils
16
+ Description-Content-Type: text/x-rst
17
+
18
+ This library is not intended for direct public use.
19
+ It is used as development dependency for the following projects:
20
+
21
+ * https://github.com/5j9/tsetmc
22
+ * https://github.com/5j9/fipiran
23
+ * https://github.com/5j9/iranetf
24
+ * https://github.com/5j9/aggregator
@@ -0,0 +1,5 @@
1
+ pytest_aiohutils/__init__.py,sha256=iYmflF-nAUsnzrntLGbo16H_fLc8wnvVGxSoUPwC-po,7144
2
+ pytest_aiohutils-0.22.4.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
3
+ pytest_aiohutils-0.22.4.dist-info/entry_points.txt,sha256=sLtuk_C05CNj2GRRSV5M8DrAjMKBlBZohNtOQpPkxmE,48
4
+ pytest_aiohutils-0.22.4.dist-info/METADATA,sha256=Lw9YKuBD1KjHnBkmwYrR-jTAaYxgOnNrJfDgujeZPSc,899
5
+ pytest_aiohutils-0.22.4.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.8.24
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [pytest11]
2
+ aiohutils_plugin = pytest_aiohutils
3
+