pyrig 2.2.6__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.
- pyrig/__init__.py +1 -0
- pyrig/dev/__init__.py +6 -0
- pyrig/dev/builders/__init__.py +1 -0
- pyrig/dev/builders/base/__init__.py +5 -0
- pyrig/dev/builders/base/base.py +256 -0
- pyrig/dev/builders/pyinstaller.py +229 -0
- pyrig/dev/cli/__init__.py +5 -0
- pyrig/dev/cli/cli.py +95 -0
- pyrig/dev/cli/commands/__init__.py +1 -0
- pyrig/dev/cli/commands/build_artifacts.py +16 -0
- pyrig/dev/cli/commands/create_root.py +25 -0
- pyrig/dev/cli/commands/create_tests.py +244 -0
- pyrig/dev/cli/commands/init_project.py +160 -0
- pyrig/dev/cli/commands/make_inits.py +27 -0
- pyrig/dev/cli/commands/protect_repo.py +145 -0
- pyrig/dev/cli/shared_subcommands.py +20 -0
- pyrig/dev/cli/subcommands.py +73 -0
- pyrig/dev/configs/__init__.py +1 -0
- pyrig/dev/configs/base/__init__.py +5 -0
- pyrig/dev/configs/base/base.py +826 -0
- pyrig/dev/configs/containers/__init__.py +1 -0
- pyrig/dev/configs/containers/container_file.py +111 -0
- pyrig/dev/configs/dot_env.py +95 -0
- pyrig/dev/configs/dot_python_version.py +88 -0
- pyrig/dev/configs/git/__init__.py +5 -0
- pyrig/dev/configs/git/gitignore.py +181 -0
- pyrig/dev/configs/git/pre_commit.py +170 -0
- pyrig/dev/configs/licence.py +112 -0
- pyrig/dev/configs/markdown/__init__.py +1 -0
- pyrig/dev/configs/markdown/docs/__init__.py +1 -0
- pyrig/dev/configs/markdown/docs/index.py +38 -0
- pyrig/dev/configs/markdown/readme.py +132 -0
- pyrig/dev/configs/py_typed.py +28 -0
- pyrig/dev/configs/pyproject.py +436 -0
- pyrig/dev/configs/python/__init__.py +5 -0
- pyrig/dev/configs/python/builders_init.py +27 -0
- pyrig/dev/configs/python/configs_init.py +28 -0
- pyrig/dev/configs/python/dot_experiment.py +46 -0
- pyrig/dev/configs/python/main.py +59 -0
- pyrig/dev/configs/python/resources_init.py +27 -0
- pyrig/dev/configs/python/shared_subcommands.py +29 -0
- pyrig/dev/configs/python/src_init.py +27 -0
- pyrig/dev/configs/python/subcommands.py +27 -0
- pyrig/dev/configs/testing/__init__.py +5 -0
- pyrig/dev/configs/testing/conftest.py +64 -0
- pyrig/dev/configs/testing/fixtures_init.py +27 -0
- pyrig/dev/configs/testing/main_test.py +74 -0
- pyrig/dev/configs/testing/zero_test.py +43 -0
- pyrig/dev/configs/workflows/__init__.py +5 -0
- pyrig/dev/configs/workflows/base/__init__.py +5 -0
- pyrig/dev/configs/workflows/base/base.py +1662 -0
- pyrig/dev/configs/workflows/build.py +106 -0
- pyrig/dev/configs/workflows/health_check.py +133 -0
- pyrig/dev/configs/workflows/publish.py +68 -0
- pyrig/dev/configs/workflows/release.py +90 -0
- pyrig/dev/tests/__init__.py +5 -0
- pyrig/dev/tests/conftest.py +40 -0
- pyrig/dev/tests/fixtures/__init__.py +1 -0
- pyrig/dev/tests/fixtures/assertions.py +147 -0
- pyrig/dev/tests/fixtures/autouse/__init__.py +5 -0
- pyrig/dev/tests/fixtures/autouse/class_.py +42 -0
- pyrig/dev/tests/fixtures/autouse/module.py +40 -0
- pyrig/dev/tests/fixtures/autouse/session.py +589 -0
- pyrig/dev/tests/fixtures/factories.py +118 -0
- pyrig/dev/utils/__init__.py +1 -0
- pyrig/dev/utils/cli.py +17 -0
- pyrig/dev/utils/git.py +312 -0
- pyrig/dev/utils/packages.py +93 -0
- pyrig/dev/utils/resources.py +77 -0
- pyrig/dev/utils/testing.py +66 -0
- pyrig/dev/utils/versions.py +268 -0
- pyrig/main.py +9 -0
- pyrig/py.typed +0 -0
- pyrig/resources/GITIGNORE +216 -0
- pyrig/resources/LATEST_PYTHON_VERSION +1 -0
- pyrig/resources/MIT_LICENSE_TEMPLATE +21 -0
- pyrig/resources/__init__.py +1 -0
- pyrig/src/__init__.py +1 -0
- pyrig/src/git/__init__.py +6 -0
- pyrig/src/git/git.py +146 -0
- pyrig/src/graph.py +255 -0
- pyrig/src/iterate.py +107 -0
- pyrig/src/modules/__init__.py +22 -0
- pyrig/src/modules/class_.py +369 -0
- pyrig/src/modules/function.py +189 -0
- pyrig/src/modules/inspection.py +148 -0
- pyrig/src/modules/module.py +658 -0
- pyrig/src/modules/package.py +452 -0
- pyrig/src/os/__init__.py +6 -0
- pyrig/src/os/os.py +121 -0
- pyrig/src/project/__init__.py +5 -0
- pyrig/src/project/mgt.py +83 -0
- pyrig/src/resource.py +58 -0
- pyrig/src/string.py +100 -0
- pyrig/src/testing/__init__.py +6 -0
- pyrig/src/testing/assertions.py +66 -0
- pyrig/src/testing/convention.py +203 -0
- pyrig-2.2.6.dist-info/METADATA +174 -0
- pyrig-2.2.6.dist-info/RECORD +102 -0
- pyrig-2.2.6.dist-info/WHEEL +4 -0
- pyrig-2.2.6.dist-info/entry_points.txt +3 -0
- pyrig-2.2.6.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
"""Session-level test fixtures and utilities.
|
|
2
|
+
|
|
3
|
+
These fixtures in this module are automatically applied to the test session
|
|
4
|
+
through pytest's autouse mechanism. Pyrig automatically adds this module to
|
|
5
|
+
pytest_plugins in conftest.py. However you still have decorate the fixture
|
|
6
|
+
with @autouse_session_fixture from pyrig.src.testing.fixtures or with pytest's
|
|
7
|
+
autouse mechanism @pytest.fixture(scope="session", autouse=True).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import shutil
|
|
14
|
+
from collections.abc import Generator
|
|
15
|
+
from contextlib import chdir
|
|
16
|
+
from importlib import import_module
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import TYPE_CHECKING
|
|
19
|
+
|
|
20
|
+
import pytest
|
|
21
|
+
|
|
22
|
+
import pyrig
|
|
23
|
+
from pyrig import dev, main, resources, src
|
|
24
|
+
from pyrig.dev.cli.commands.create_root import make_project_root
|
|
25
|
+
from pyrig.dev.cli.commands.create_tests import make_test_skeletons
|
|
26
|
+
from pyrig.dev.cli.commands.init_project import STANDARD_DEV_DEPS
|
|
27
|
+
from pyrig.dev.cli.commands.make_inits import get_namespace_packages, make_init_files
|
|
28
|
+
from pyrig.dev.configs.base.base import ConfigFile
|
|
29
|
+
from pyrig.dev.configs.git.gitignore import GitIgnoreConfigFile
|
|
30
|
+
from pyrig.dev.configs.git.pre_commit import PreCommitConfigConfigFile
|
|
31
|
+
from pyrig.dev.configs.pyproject import (
|
|
32
|
+
PyprojectConfigFile,
|
|
33
|
+
)
|
|
34
|
+
from pyrig.dev.configs.python.dot_experiment import DotExperimentConfigFile
|
|
35
|
+
from pyrig.dev.utils.packages import find_packages, get_src_package
|
|
36
|
+
from pyrig.dev.utils.testing import autouse_session_fixture
|
|
37
|
+
from pyrig.src.git.git import (
|
|
38
|
+
get_git_unstaged_changes,
|
|
39
|
+
running_in_github_actions,
|
|
40
|
+
)
|
|
41
|
+
from pyrig.src.modules.module import (
|
|
42
|
+
get_isolated_obj_name,
|
|
43
|
+
get_module_name_replacing_start_module,
|
|
44
|
+
import_module_with_default,
|
|
45
|
+
)
|
|
46
|
+
from pyrig.src.modules.package import (
|
|
47
|
+
DOCS_DIR_NAME,
|
|
48
|
+
DependencyGraph,
|
|
49
|
+
get_modules_and_packages_from_package,
|
|
50
|
+
get_pkg_name_from_project_name,
|
|
51
|
+
get_project_name_from_pkg_name,
|
|
52
|
+
walk_package,
|
|
53
|
+
)
|
|
54
|
+
from pyrig.src.os.os import run_subprocess
|
|
55
|
+
from pyrig.src.project.mgt import PROJECT_MGT_RUN_ARGS
|
|
56
|
+
from pyrig.src.testing.assertions import assert_with_msg
|
|
57
|
+
from pyrig.src.testing.convention import (
|
|
58
|
+
TESTS_PACKAGE_NAME,
|
|
59
|
+
make_summary_error_msg,
|
|
60
|
+
make_test_obj_importpath_from_obj,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if TYPE_CHECKING:
|
|
64
|
+
from types import ModuleType
|
|
65
|
+
|
|
66
|
+
logger = logging.getLogger(__name__)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@autouse_session_fixture
|
|
70
|
+
def assert_no_unstaged_changes() -> Generator[None, None, None]:
|
|
71
|
+
"""Verify that there are no unstaged changes.
|
|
72
|
+
|
|
73
|
+
Checks before and after the test session if there are unstaged changes.
|
|
74
|
+
If there are unstaged changes before the test session, it fails.
|
|
75
|
+
If there are unstaged changes after the test session, it fails.
|
|
76
|
+
|
|
77
|
+
Raises:
|
|
78
|
+
AssertionError: If there are unstaged changes
|
|
79
|
+
|
|
80
|
+
"""
|
|
81
|
+
in_github_actions = running_in_github_actions()
|
|
82
|
+
|
|
83
|
+
msg = (
|
|
84
|
+
"Found unstaged changes. Please commit or stash them. "
|
|
85
|
+
"Unstaged changes: {unstaged_changes}"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
if in_github_actions:
|
|
89
|
+
unstaged_changes = get_git_unstaged_changes()
|
|
90
|
+
assert_with_msg(
|
|
91
|
+
not unstaged_changes,
|
|
92
|
+
msg=msg.format(unstaged_changes=unstaged_changes),
|
|
93
|
+
)
|
|
94
|
+
yield
|
|
95
|
+
if in_github_actions:
|
|
96
|
+
unstaged_changes = get_git_unstaged_changes()
|
|
97
|
+
assert_with_msg(
|
|
98
|
+
not unstaged_changes,
|
|
99
|
+
msg=msg.format(unstaged_changes=unstaged_changes),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@autouse_session_fixture
|
|
104
|
+
def assert_root_is_correct() -> None:
|
|
105
|
+
"""Verify that the dev dependencies are installed.
|
|
106
|
+
|
|
107
|
+
This fixture runs once per test session and checks that the dev dependencies
|
|
108
|
+
are installed by trying to import them.
|
|
109
|
+
|
|
110
|
+
Raises:
|
|
111
|
+
ImportError: If a dev dependency is not installed
|
|
112
|
+
|
|
113
|
+
"""
|
|
114
|
+
# if we are in CI then we must create experiment.py if it doesn't exist
|
|
115
|
+
running_in_ci = running_in_github_actions()
|
|
116
|
+
if running_in_ci:
|
|
117
|
+
DotExperimentConfigFile()
|
|
118
|
+
|
|
119
|
+
subclasses = ConfigFile.get_all_subclasses()
|
|
120
|
+
incorrect_cfs = [cf for cf in subclasses if not cf.is_correct()]
|
|
121
|
+
|
|
122
|
+
if incorrect_cfs:
|
|
123
|
+
# init all per test run
|
|
124
|
+
make_project_root()
|
|
125
|
+
|
|
126
|
+
msg = f"""Found {len(incorrect_cfs)} incorrect ConfigFiles.
|
|
127
|
+
Attempted correcting them automatically.
|
|
128
|
+
Please verify the changes at the following paths:
|
|
129
|
+
"""
|
|
130
|
+
for cf in incorrect_cfs:
|
|
131
|
+
msg += f"""
|
|
132
|
+
- {cf.get_path()}
|
|
133
|
+
"""
|
|
134
|
+
assert_with_msg(not incorrect_cfs, msg)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@autouse_session_fixture
|
|
138
|
+
def assert_no_namespace_packages() -> None:
|
|
139
|
+
"""Verify that there are no namespace packages in the project.
|
|
140
|
+
|
|
141
|
+
This fixture runs once per test session and checks that all packages in the
|
|
142
|
+
project are regular packages with __init__.py files, not namespace packages.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
AssertionError: If any namespace packages are found
|
|
146
|
+
|
|
147
|
+
"""
|
|
148
|
+
any_namespace_packages = get_namespace_packages()
|
|
149
|
+
if any_namespace_packages:
|
|
150
|
+
make_init_files()
|
|
151
|
+
|
|
152
|
+
msg = f"""Found {len(any_namespace_packages)} namespace packages.
|
|
153
|
+
Created __init__.py files for them.
|
|
154
|
+
Please verify the changes at the following paths:
|
|
155
|
+
"""
|
|
156
|
+
for package in any_namespace_packages:
|
|
157
|
+
msg += f"""
|
|
158
|
+
- {package}
|
|
159
|
+
"""
|
|
160
|
+
assert_with_msg(not any_namespace_packages, msg)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@autouse_session_fixture
|
|
164
|
+
def assert_all_src_code_in_one_package() -> None:
|
|
165
|
+
"""Verify that all source code is in a single package.
|
|
166
|
+
|
|
167
|
+
This fixture runs once per test session and checks that there is only one
|
|
168
|
+
source package besides the tests package.
|
|
169
|
+
|
|
170
|
+
Raises:
|
|
171
|
+
AssertionError: If there are multiple source packages
|
|
172
|
+
|
|
173
|
+
"""
|
|
174
|
+
packages = find_packages(depth=0)
|
|
175
|
+
src_package = get_src_package()
|
|
176
|
+
src_package_name = src_package.__name__
|
|
177
|
+
expected_packages = {TESTS_PACKAGE_NAME, src_package_name, DOCS_DIR_NAME}
|
|
178
|
+
|
|
179
|
+
# pkgs must be subset of expected_packages
|
|
180
|
+
assert_with_msg(
|
|
181
|
+
set(packages).issubset(expected_packages),
|
|
182
|
+
f"Expected only packages {expected_packages}, but found {packages}",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# assert the src package's only submodules are main, src and dev
|
|
186
|
+
subpackages, submodules = get_modules_and_packages_from_package(src_package)
|
|
187
|
+
subpackage_names = {p.__name__.split(".")[-1] for p in subpackages}
|
|
188
|
+
submodule_names = {m.__name__.split(".")[-1] for m in submodules}
|
|
189
|
+
|
|
190
|
+
expected_subpackages = {
|
|
191
|
+
get_isolated_obj_name(sub_pkg)
|
|
192
|
+
for sub_pkg in [
|
|
193
|
+
dev,
|
|
194
|
+
src,
|
|
195
|
+
resources,
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
expected_submodules = {get_isolated_obj_name(main)}
|
|
199
|
+
assert_with_msg(
|
|
200
|
+
subpackage_names == expected_subpackages,
|
|
201
|
+
f"Expected subpackages {expected_subpackages}, but found {subpackage_names}",
|
|
202
|
+
)
|
|
203
|
+
assert_with_msg(
|
|
204
|
+
submodule_names == expected_submodules,
|
|
205
|
+
f"Expected submodules {expected_submodules}, but found {submodule_names}",
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@autouse_session_fixture
|
|
210
|
+
def assert_src_package_correctly_named() -> None:
|
|
211
|
+
"""Verify that the source package is correctly named.
|
|
212
|
+
|
|
213
|
+
This fixture runs once per test session and checks that the source package
|
|
214
|
+
is correctly named after the project.
|
|
215
|
+
|
|
216
|
+
Raises:
|
|
217
|
+
AssertionError: If the source package is not correctly named
|
|
218
|
+
|
|
219
|
+
"""
|
|
220
|
+
cwd_name = Path.cwd().name
|
|
221
|
+
project_name = PyprojectConfigFile.get_project_name()
|
|
222
|
+
assert_with_msg(
|
|
223
|
+
cwd_name == project_name,
|
|
224
|
+
f"Expected cwd name to be {project_name}, but it is {cwd_name}",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
src_package_name = get_src_package().__name__
|
|
228
|
+
src_package_name_from_cwd = get_pkg_name_from_project_name(cwd_name)
|
|
229
|
+
assert_with_msg(
|
|
230
|
+
src_package_name == src_package_name_from_cwd,
|
|
231
|
+
f"Expected source package to be named {src_package_name_from_cwd}, "
|
|
232
|
+
f"but it is named {src_package_name}",
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
src_package = get_src_package().__name__
|
|
236
|
+
expected_package = PyprojectConfigFile.get_package_name()
|
|
237
|
+
assert_with_msg(
|
|
238
|
+
src_package == expected_package,
|
|
239
|
+
f"Expected source package to be named {expected_package}, "
|
|
240
|
+
f"but it is named {src_package}",
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@autouse_session_fixture
|
|
245
|
+
def assert_all_modules_tested() -> None:
|
|
246
|
+
"""Verify that the project structure is mirrored in tests.
|
|
247
|
+
|
|
248
|
+
This fixture runs once per test session and checks that for every package and
|
|
249
|
+
module in the source package, there is a corresponding test package and module.
|
|
250
|
+
|
|
251
|
+
Raises:
|
|
252
|
+
AssertionError: If any package or module doesn't have a corresponding test
|
|
253
|
+
|
|
254
|
+
"""
|
|
255
|
+
src_package = get_src_package()
|
|
256
|
+
|
|
257
|
+
# we will now go through all the modules in the src package and check
|
|
258
|
+
# that there is a corresponding test module
|
|
259
|
+
missing_tests_to_module: dict[str, ModuleType] = {}
|
|
260
|
+
for package, modules in walk_package(src_package):
|
|
261
|
+
test_package_name = make_test_obj_importpath_from_obj(package)
|
|
262
|
+
test_package = import_module_with_default(test_package_name)
|
|
263
|
+
if test_package is None:
|
|
264
|
+
missing_tests_to_module[test_package_name] = package
|
|
265
|
+
|
|
266
|
+
for module in modules:
|
|
267
|
+
test_module_name = make_test_obj_importpath_from_obj(module)
|
|
268
|
+
test_module = import_module_with_default(test_module_name)
|
|
269
|
+
if test_module is None:
|
|
270
|
+
missing_tests_to_module[test_module_name] = module
|
|
271
|
+
|
|
272
|
+
if missing_tests_to_module:
|
|
273
|
+
make_test_skeletons()
|
|
274
|
+
|
|
275
|
+
msg = f"""Found missing tests. Tests skeletons were automatically created for:
|
|
276
|
+
{make_summary_error_msg(missing_tests_to_module.keys())}
|
|
277
|
+
"""
|
|
278
|
+
assert_with_msg(
|
|
279
|
+
not missing_tests_to_module,
|
|
280
|
+
msg,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@autouse_session_fixture
|
|
285
|
+
def assert_no_unit_test_package_usage() -> None:
|
|
286
|
+
"""Verify that the unit test package is not used in the project.
|
|
287
|
+
|
|
288
|
+
This fixture runs once per test session and checks that the unit test package
|
|
289
|
+
is not used in the project.
|
|
290
|
+
|
|
291
|
+
Raises:
|
|
292
|
+
AssertionError: If the unit test package is used
|
|
293
|
+
|
|
294
|
+
"""
|
|
295
|
+
for path in Path().rglob("*.py"):
|
|
296
|
+
if GitIgnoreConfigFile.path_is_in_gitignore(path):
|
|
297
|
+
continue
|
|
298
|
+
assert_with_msg(
|
|
299
|
+
"UnitTest".lower() not in path.read_text(encoding="utf-8"),
|
|
300
|
+
f"Found unit test package usage in {path}. Use pytest instead.",
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
@autouse_session_fixture
|
|
305
|
+
def assert_dependencies_are_up_to_date() -> None:
|
|
306
|
+
"""Verify that the dependencies are up to date.
|
|
307
|
+
|
|
308
|
+
This fixture runs once per test session
|
|
309
|
+
to make sure the dependencies are up to date.
|
|
310
|
+
"""
|
|
311
|
+
# update the dependencies
|
|
312
|
+
completed_process = PyprojectConfigFile.update_dependencies(check=False)
|
|
313
|
+
stderr = completed_process.stderr.decode("utf-8")
|
|
314
|
+
stdout = completed_process.stdout.decode("utf-8")
|
|
315
|
+
std_msg = stderr + stdout
|
|
316
|
+
|
|
317
|
+
not_expected = ["Updated"]
|
|
318
|
+
# if there were updates raise an error
|
|
319
|
+
update_occurred = any(exp in std_msg for exp in not_expected)
|
|
320
|
+
assert not update_occurred, f"Expected none of {not_expected}, got: {std_msg}"
|
|
321
|
+
|
|
322
|
+
# sync the dependencies
|
|
323
|
+
completed_process = PyprojectConfigFile.install_dependencies(check=True)
|
|
324
|
+
stderr = completed_process.stderr.decode("utf-8")
|
|
325
|
+
stdout = completed_process.stdout.decode("utf-8")
|
|
326
|
+
std_msg = stderr + stdout
|
|
327
|
+
expected = ["Resolved", "Audited"]
|
|
328
|
+
expected_in_err_or_out = any(exp in std_msg for exp in expected)
|
|
329
|
+
assert expected_in_err_or_out, f"Expected one of {expected}, got: {std_msg}"
|
|
330
|
+
|
|
331
|
+
not_expected = ["=="]
|
|
332
|
+
install_occurred = any(exp in std_msg for exp in not_expected)
|
|
333
|
+
assert not install_occurred, f"Expected none of {not_expected}, got: {std_msg}"
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
@autouse_session_fixture
|
|
337
|
+
def assert_pre_commit_is_installed() -> None:
|
|
338
|
+
"""Verify that pre-commit is installed.
|
|
339
|
+
|
|
340
|
+
This fixture runs once per test session and runs pre-commit install
|
|
341
|
+
to make sure pre-commit is installed.
|
|
342
|
+
"""
|
|
343
|
+
completed_process = PreCommitConfigConfigFile.install()
|
|
344
|
+
stdout = completed_process.stdout.decode("utf-8")
|
|
345
|
+
logger.info("Pre-commit install output: %s", stdout)
|
|
346
|
+
expected = "pre-commit installed at"
|
|
347
|
+
|
|
348
|
+
assert_with_msg(
|
|
349
|
+
expected in stdout,
|
|
350
|
+
f"Expected {expected} in pre-commit install output, got {stdout}",
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@autouse_session_fixture
|
|
355
|
+
def assert_src_runs_without_dev_deps(
|
|
356
|
+
tmp_path_factory: pytest.TempPathFactory,
|
|
357
|
+
) -> None:
|
|
358
|
+
"""Verify that the source code runs without dev dependencies.
|
|
359
|
+
|
|
360
|
+
This fixture runs once per test session and checks that the source code
|
|
361
|
+
runs without dev dependencies.
|
|
362
|
+
"""
|
|
363
|
+
tmp_path = tmp_path_factory.mktemp(assert_src_runs_without_dev_deps.__name__)
|
|
364
|
+
# copy the project folder to a temp directory
|
|
365
|
+
# run main.py from that directory
|
|
366
|
+
src_package = get_src_package()
|
|
367
|
+
src_package_file_str = src_package.__file__
|
|
368
|
+
if src_package_file_str is None:
|
|
369
|
+
msg = f"src_package.__file__ is None for {src_package}"
|
|
370
|
+
raise ValueError(msg)
|
|
371
|
+
|
|
372
|
+
project_path = Path(src_package_file_str).parent
|
|
373
|
+
|
|
374
|
+
project_name = get_project_name_from_pkg_name(src_package.__name__)
|
|
375
|
+
|
|
376
|
+
temp_project_path = tmp_path / src_package.__name__
|
|
377
|
+
|
|
378
|
+
# shutil copy the project to tmp_path
|
|
379
|
+
shutil.copytree(project_path, temp_project_path)
|
|
380
|
+
|
|
381
|
+
# copy pyproject.toml and uv.lock to tmp_path
|
|
382
|
+
configs = [
|
|
383
|
+
"pyproject.toml",
|
|
384
|
+
"README.md",
|
|
385
|
+
"LICENSE",
|
|
386
|
+
]
|
|
387
|
+
for config in configs:
|
|
388
|
+
shutil.copy(config, temp_project_path.parent)
|
|
389
|
+
|
|
390
|
+
env = os.environ.copy()
|
|
391
|
+
env.pop("VIRTUAL_ENV", None)
|
|
392
|
+
|
|
393
|
+
with chdir(tmp_path):
|
|
394
|
+
# install deps
|
|
395
|
+
completed_process = run_subprocess(
|
|
396
|
+
["uv", "sync", "--no-group", "dev"], env=env, check=False
|
|
397
|
+
)
|
|
398
|
+
stdout = completed_process.stdout.decode("utf-8")
|
|
399
|
+
stderr = completed_process.stderr.decode("utf-8")
|
|
400
|
+
std_msg = stderr + stdout
|
|
401
|
+
no_internet = "Temporary failure in name resolution" in std_msg
|
|
402
|
+
if no_internet:
|
|
403
|
+
logger.warning(
|
|
404
|
+
"No internet, skipping %s",
|
|
405
|
+
assert_src_runs_without_dev_deps.__name__,
|
|
406
|
+
)
|
|
407
|
+
return
|
|
408
|
+
|
|
409
|
+
# delete pyproject.toml and uv.lock and readme.md
|
|
410
|
+
for config in configs:
|
|
411
|
+
Path(config).unlink()
|
|
412
|
+
# python -m video_vault.main
|
|
413
|
+
|
|
414
|
+
# assert pytest is not installed
|
|
415
|
+
dev_dep = "pytest"
|
|
416
|
+
installed = run_subprocess(
|
|
417
|
+
[*PROJECT_MGT_RUN_ARGS, "pip", "show", dev_dep], check=False, env=env
|
|
418
|
+
)
|
|
419
|
+
stderr = installed.stderr.decode("utf-8")
|
|
420
|
+
dev_dep_not_installed = f"not found: {dev_dep}" in stderr
|
|
421
|
+
assert_with_msg(
|
|
422
|
+
dev_dep_not_installed,
|
|
423
|
+
f"Expected {dev_dep} not to be installed",
|
|
424
|
+
)
|
|
425
|
+
# check pytest is not importable
|
|
426
|
+
installed = run_subprocess(
|
|
427
|
+
[*PROJECT_MGT_RUN_ARGS, "python", "-c", "import pytest"],
|
|
428
|
+
check=False,
|
|
429
|
+
env=env,
|
|
430
|
+
)
|
|
431
|
+
stderr = installed.stderr.decode("utf-8")
|
|
432
|
+
assert_with_msg(
|
|
433
|
+
"ModuleNotFoundError" in stderr,
|
|
434
|
+
f"Expected ModuleNotFoundError in stderr, got {stderr}",
|
|
435
|
+
)
|
|
436
|
+
src_pkg_name = get_src_package().__name__
|
|
437
|
+
|
|
438
|
+
# run walk_package with src and import all modules to catch dev dep imports
|
|
439
|
+
cmd = [
|
|
440
|
+
"uv",
|
|
441
|
+
"run",
|
|
442
|
+
"--no-group",
|
|
443
|
+
"dev",
|
|
444
|
+
"python",
|
|
445
|
+
"-c",
|
|
446
|
+
(
|
|
447
|
+
"from importlib import import_module; "
|
|
448
|
+
"from pyrig import main; "
|
|
449
|
+
"from pyrig import src; "
|
|
450
|
+
"from pyrig.src.modules.module import get_module_name_replacing_start_module; " # noqa: E501
|
|
451
|
+
"from pyrig.src.modules.package import walk_package; "
|
|
452
|
+
"from pyrig.src.testing.assertions import assert_with_msg; "
|
|
453
|
+
f"import {src_pkg_name}; "
|
|
454
|
+
f"src_module=import_module(get_module_name_replacing_start_module(src, {src_pkg_name}.__name__)); " # noqa: E501
|
|
455
|
+
"pks=list(walk_package(src_module)); "
|
|
456
|
+
"assert_with_msg(isinstance(pks, list), 'Expected pks to be a list'); "
|
|
457
|
+
"assert_with_msg(len(pks) > 0, 'Expected pks to not be empty'); "
|
|
458
|
+
# also test that main can be called
|
|
459
|
+
f"main_module=import_module(get_module_name_replacing_start_module(main, {src_pkg_name}.__name__)); " # noqa: E501
|
|
460
|
+
# add a print statement to see the output
|
|
461
|
+
"print('Success')"
|
|
462
|
+
),
|
|
463
|
+
]
|
|
464
|
+
|
|
465
|
+
completed_process = run_subprocess(cmd, env=env, check=False)
|
|
466
|
+
stdout = completed_process.stdout.decode("utf-8")
|
|
467
|
+
stderr = completed_process.stderr.decode("utf-8")
|
|
468
|
+
assert_with_msg(
|
|
469
|
+
"Success" in stdout,
|
|
470
|
+
f"Expected Success in stdout, got {stdout} and {stderr}",
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
# run cli without dev deps
|
|
474
|
+
cmd = ["uv", "run", "--no-group", "dev", project_name, "--help"]
|
|
475
|
+
completed_process = run_subprocess(cmd, env=env, check=False)
|
|
476
|
+
stdout = completed_process.stdout.decode("utf-8")
|
|
477
|
+
stderr = completed_process.stderr.decode("utf-8")
|
|
478
|
+
assert "Usage:" in stdout, (
|
|
479
|
+
f"Expected Usage: in stdout, got {stdout} and {stderr}"
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
@autouse_session_fixture
|
|
484
|
+
def assert_src_does_not_use_dev() -> None:
|
|
485
|
+
"""Verify that the source code does not import any code from dev.
|
|
486
|
+
|
|
487
|
+
This tests that the src folder has no code that depends on dev code.
|
|
488
|
+
"""
|
|
489
|
+
src_package = get_src_package()
|
|
490
|
+
|
|
491
|
+
src_src_pkg_name = get_module_name_replacing_start_module(src, src_package.__name__)
|
|
492
|
+
|
|
493
|
+
src_src_pkg = import_module(src_src_pkg_name)
|
|
494
|
+
|
|
495
|
+
pkgs_depending_on_pyrig = DependencyGraph().get_all_depending_on(
|
|
496
|
+
pyrig, include_self=True
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
possible_dev_usages = [
|
|
500
|
+
get_module_name_replacing_start_module(dev, pkg.__name__)
|
|
501
|
+
for pkg in pkgs_depending_on_pyrig
|
|
502
|
+
]
|
|
503
|
+
|
|
504
|
+
possible_dev_usages_pattern = r"\b(" + "|".join(possible_dev_usages) + r")\b"
|
|
505
|
+
|
|
506
|
+
usages: list[str] = []
|
|
507
|
+
folder_path = Path(src_src_pkg.__path__[0])
|
|
508
|
+
for path in folder_path.rglob("*.py"):
|
|
509
|
+
content = path.read_text(encoding="utf-8")
|
|
510
|
+
|
|
511
|
+
is_dev_used = re.search(possible_dev_usages_pattern, content)
|
|
512
|
+
if is_dev_used:
|
|
513
|
+
usages.append(f"{path}: {is_dev_used.group()}")
|
|
514
|
+
|
|
515
|
+
msg = f"""Found dev usage in src:
|
|
516
|
+
{make_summary_error_msg(usages)}
|
|
517
|
+
"""
|
|
518
|
+
assert_with_msg(
|
|
519
|
+
not usages,
|
|
520
|
+
msg,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
@autouse_session_fixture
|
|
525
|
+
def assert_all_dev_deps_in_deps() -> None:
|
|
526
|
+
"""Checks that all of pyrigs dev deps are in toml."""
|
|
527
|
+
all_deps = set(PyprojectConfigFile.get_all_dependencies())
|
|
528
|
+
standard_dev_deps = set(STANDARD_DEV_DEPS)
|
|
529
|
+
|
|
530
|
+
stripped_deps = {
|
|
531
|
+
PyprojectConfigFile.remove_version_from_dep(dep) for dep in all_deps
|
|
532
|
+
}
|
|
533
|
+
stripped_standard_dev_deps = {
|
|
534
|
+
PyprojectConfigFile.remove_version_from_dep(dep) for dep in standard_dev_deps
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
assert stripped_standard_dev_deps.issubset(stripped_deps)
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
@autouse_session_fixture
|
|
541
|
+
def assert_project_mgt_is_up_to_date() -> None:
|
|
542
|
+
"""Verify that the project management tool is up to date."""
|
|
543
|
+
if not running_in_github_actions():
|
|
544
|
+
# update project mgt
|
|
545
|
+
completed_process = run_subprocess(["uv", "self", "update"], check=False)
|
|
546
|
+
stderr = completed_process.stderr.decode("utf-8")
|
|
547
|
+
stdout = completed_process.stdout.decode("utf-8")
|
|
548
|
+
std_msg = stderr + stdout
|
|
549
|
+
|
|
550
|
+
expected = [
|
|
551
|
+
"success: You're on the latest version of uv",
|
|
552
|
+
"GitHub API rate limit exceeded",
|
|
553
|
+
"Temporary failure in name resolution",
|
|
554
|
+
]
|
|
555
|
+
expected_in_err_or_out = any(exp in std_msg for exp in expected)
|
|
556
|
+
assert expected_in_err_or_out, f"Expected one of {expected}, got: {std_msg}"
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
@autouse_session_fixture
|
|
560
|
+
def assert_version_control_is_installed() -> None:
|
|
561
|
+
"""Verify that git is installed.
|
|
562
|
+
|
|
563
|
+
As pyrig needs and expects git to be installed.
|
|
564
|
+
"""
|
|
565
|
+
completed_process = run_subprocess(["git", "--version"], check=False)
|
|
566
|
+
stderr = completed_process.stderr.decode("utf-8")
|
|
567
|
+
stdout = completed_process.stdout.decode("utf-8")
|
|
568
|
+
std_msg = stderr + stdout
|
|
569
|
+
# use re expression to check if git version is in the output
|
|
570
|
+
git_is_installed = re.search(r"git version \d+\.\d+\.\d+", std_msg)
|
|
571
|
+
|
|
572
|
+
assert git_is_installed, f"Expected git to be installed, got: {std_msg}"
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
@autouse_session_fixture
|
|
576
|
+
def assert_container_engine_is_installed() -> None:
|
|
577
|
+
"""Verify that podman is installed.
|
|
578
|
+
|
|
579
|
+
As pyrig needs and expects podman to be installed.
|
|
580
|
+
"""
|
|
581
|
+
if not running_in_github_actions():
|
|
582
|
+
completed_process = run_subprocess(["podman", "--version"], check=False)
|
|
583
|
+
stderr = completed_process.stderr.decode("utf-8")
|
|
584
|
+
stdout = completed_process.stdout.decode("utf-8")
|
|
585
|
+
std_msg = stderr + stdout
|
|
586
|
+
# use re expression to check if podman version is in the output
|
|
587
|
+
podman_is_installed = re.search(r"podman version \d+\.\d+\.\d+", std_msg)
|
|
588
|
+
|
|
589
|
+
assert podman_is_installed, f"Expected podman to be installed, got: {std_msg}"
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Factory fixtures for testing pyrig components.
|
|
2
|
+
|
|
3
|
+
This module provides factory fixtures that wrap ConfigFile and Builder
|
|
4
|
+
classes to use temporary directories during testing. All fixtures defined
|
|
5
|
+
under the fixtures package are automatically registered via pytest_plugins.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
Using the config_file_factory::
|
|
9
|
+
|
|
10
|
+
def test_my_config(config_file_factory):
|
|
11
|
+
TestConfig = config_file_factory(MyConfigFile)
|
|
12
|
+
# TestConfig.get_path() now returns a path in tmp_path
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
|
|
20
|
+
from pyrig.dev.builders.base.base import Builder
|
|
21
|
+
from pyrig.dev.configs.base.base import ConfigFile
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def config_file_factory[T: ConfigFile](
|
|
26
|
+
tmp_path: Path,
|
|
27
|
+
) -> Callable[[type[T]], type[T]]:
|
|
28
|
+
"""Create a factory for ConfigFile subclasses using temporary paths.
|
|
29
|
+
|
|
30
|
+
This factory wraps any ConfigFile subclass to redirect get_path() to
|
|
31
|
+
tmp_path, enabling isolated testing without affecting real config files.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
tmp_path: Pytest's temporary directory fixture.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
A factory function that takes a ConfigFile subclass and returns
|
|
38
|
+
a wrapped version using tmp_path.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
TestConfig = config_file_factory(PyprojectConfigFile)
|
|
42
|
+
assert str(tmp_path) in str(TestConfig.get_path())
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def _make_test_config(
|
|
46
|
+
base_class: type[T],
|
|
47
|
+
) -> type[T]:
|
|
48
|
+
"""Create a test config class that uses tmp_path.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
base_class: The ConfigFile subclass to wrap.
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
A subclass with get_path() redirected to tmp_path.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
class TestConfigFile(base_class): # type: ignore [misc, valid-type]
|
|
58
|
+
"""Test config file with tmp_path override."""
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def get_path(cls) -> Path:
|
|
62
|
+
"""Get the path to the config file in tmp_path.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Path within tmp_path.
|
|
66
|
+
"""
|
|
67
|
+
path = super().get_path()
|
|
68
|
+
return Path(tmp_path / path)
|
|
69
|
+
|
|
70
|
+
return TestConfigFile # ty:ignore[invalid-return-type]
|
|
71
|
+
|
|
72
|
+
return _make_test_config
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.fixture
|
|
76
|
+
def builder_factory[T: Builder](tmp_path: Path) -> Callable[[type[T]], type[T]]:
|
|
77
|
+
"""Create a factory for Builder subclasses using temporary paths.
|
|
78
|
+
|
|
79
|
+
This factory wraps any Builder subclass to redirect get_artifacts_dir()
|
|
80
|
+
to tmp_path, enabling isolated testing of artifact generation.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
tmp_path: Pytest's temporary directory fixture.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
A factory function that takes a Builder subclass and returns
|
|
87
|
+
a wrapped version using tmp_path.
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
TestBuilder = builder_factory(MyBuilder)
|
|
91
|
+
assert str(tmp_path) in str(TestBuilder.get_artifacts_dir())
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def _make_test_builder(base_class: type[T]) -> type[T]:
|
|
95
|
+
"""Create a test builder class that uses tmp_path.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
base_class: The Builder subclass to wrap.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
A subclass with get_artifacts_dir() redirected to tmp_path.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
class TestBuilder(base_class): # type: ignore [misc, valid-type]
|
|
105
|
+
"""Test builder with tmp_path override."""
|
|
106
|
+
|
|
107
|
+
@classmethod
|
|
108
|
+
def get_artifacts_dir(cls) -> Path:
|
|
109
|
+
"""Get the artifacts directory in tmp_path.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Path within tmp_path.
|
|
113
|
+
"""
|
|
114
|
+
return Path(tmp_path / cls.ARTIFACTS_DIR_NAME)
|
|
115
|
+
|
|
116
|
+
return TestBuilder # ty:ignore[invalid-return-type]
|
|
117
|
+
|
|
118
|
+
return _make_test_builder
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""__init__ module."""
|