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.
Files changed (102) hide show
  1. pyrig/__init__.py +1 -0
  2. pyrig/dev/__init__.py +6 -0
  3. pyrig/dev/builders/__init__.py +1 -0
  4. pyrig/dev/builders/base/__init__.py +5 -0
  5. pyrig/dev/builders/base/base.py +256 -0
  6. pyrig/dev/builders/pyinstaller.py +229 -0
  7. pyrig/dev/cli/__init__.py +5 -0
  8. pyrig/dev/cli/cli.py +95 -0
  9. pyrig/dev/cli/commands/__init__.py +1 -0
  10. pyrig/dev/cli/commands/build_artifacts.py +16 -0
  11. pyrig/dev/cli/commands/create_root.py +25 -0
  12. pyrig/dev/cli/commands/create_tests.py +244 -0
  13. pyrig/dev/cli/commands/init_project.py +160 -0
  14. pyrig/dev/cli/commands/make_inits.py +27 -0
  15. pyrig/dev/cli/commands/protect_repo.py +145 -0
  16. pyrig/dev/cli/shared_subcommands.py +20 -0
  17. pyrig/dev/cli/subcommands.py +73 -0
  18. pyrig/dev/configs/__init__.py +1 -0
  19. pyrig/dev/configs/base/__init__.py +5 -0
  20. pyrig/dev/configs/base/base.py +826 -0
  21. pyrig/dev/configs/containers/__init__.py +1 -0
  22. pyrig/dev/configs/containers/container_file.py +111 -0
  23. pyrig/dev/configs/dot_env.py +95 -0
  24. pyrig/dev/configs/dot_python_version.py +88 -0
  25. pyrig/dev/configs/git/__init__.py +5 -0
  26. pyrig/dev/configs/git/gitignore.py +181 -0
  27. pyrig/dev/configs/git/pre_commit.py +170 -0
  28. pyrig/dev/configs/licence.py +112 -0
  29. pyrig/dev/configs/markdown/__init__.py +1 -0
  30. pyrig/dev/configs/markdown/docs/__init__.py +1 -0
  31. pyrig/dev/configs/markdown/docs/index.py +38 -0
  32. pyrig/dev/configs/markdown/readme.py +132 -0
  33. pyrig/dev/configs/py_typed.py +28 -0
  34. pyrig/dev/configs/pyproject.py +436 -0
  35. pyrig/dev/configs/python/__init__.py +5 -0
  36. pyrig/dev/configs/python/builders_init.py +27 -0
  37. pyrig/dev/configs/python/configs_init.py +28 -0
  38. pyrig/dev/configs/python/dot_experiment.py +46 -0
  39. pyrig/dev/configs/python/main.py +59 -0
  40. pyrig/dev/configs/python/resources_init.py +27 -0
  41. pyrig/dev/configs/python/shared_subcommands.py +29 -0
  42. pyrig/dev/configs/python/src_init.py +27 -0
  43. pyrig/dev/configs/python/subcommands.py +27 -0
  44. pyrig/dev/configs/testing/__init__.py +5 -0
  45. pyrig/dev/configs/testing/conftest.py +64 -0
  46. pyrig/dev/configs/testing/fixtures_init.py +27 -0
  47. pyrig/dev/configs/testing/main_test.py +74 -0
  48. pyrig/dev/configs/testing/zero_test.py +43 -0
  49. pyrig/dev/configs/workflows/__init__.py +5 -0
  50. pyrig/dev/configs/workflows/base/__init__.py +5 -0
  51. pyrig/dev/configs/workflows/base/base.py +1662 -0
  52. pyrig/dev/configs/workflows/build.py +106 -0
  53. pyrig/dev/configs/workflows/health_check.py +133 -0
  54. pyrig/dev/configs/workflows/publish.py +68 -0
  55. pyrig/dev/configs/workflows/release.py +90 -0
  56. pyrig/dev/tests/__init__.py +5 -0
  57. pyrig/dev/tests/conftest.py +40 -0
  58. pyrig/dev/tests/fixtures/__init__.py +1 -0
  59. pyrig/dev/tests/fixtures/assertions.py +147 -0
  60. pyrig/dev/tests/fixtures/autouse/__init__.py +5 -0
  61. pyrig/dev/tests/fixtures/autouse/class_.py +42 -0
  62. pyrig/dev/tests/fixtures/autouse/module.py +40 -0
  63. pyrig/dev/tests/fixtures/autouse/session.py +589 -0
  64. pyrig/dev/tests/fixtures/factories.py +118 -0
  65. pyrig/dev/utils/__init__.py +1 -0
  66. pyrig/dev/utils/cli.py +17 -0
  67. pyrig/dev/utils/git.py +312 -0
  68. pyrig/dev/utils/packages.py +93 -0
  69. pyrig/dev/utils/resources.py +77 -0
  70. pyrig/dev/utils/testing.py +66 -0
  71. pyrig/dev/utils/versions.py +268 -0
  72. pyrig/main.py +9 -0
  73. pyrig/py.typed +0 -0
  74. pyrig/resources/GITIGNORE +216 -0
  75. pyrig/resources/LATEST_PYTHON_VERSION +1 -0
  76. pyrig/resources/MIT_LICENSE_TEMPLATE +21 -0
  77. pyrig/resources/__init__.py +1 -0
  78. pyrig/src/__init__.py +1 -0
  79. pyrig/src/git/__init__.py +6 -0
  80. pyrig/src/git/git.py +146 -0
  81. pyrig/src/graph.py +255 -0
  82. pyrig/src/iterate.py +107 -0
  83. pyrig/src/modules/__init__.py +22 -0
  84. pyrig/src/modules/class_.py +369 -0
  85. pyrig/src/modules/function.py +189 -0
  86. pyrig/src/modules/inspection.py +148 -0
  87. pyrig/src/modules/module.py +658 -0
  88. pyrig/src/modules/package.py +452 -0
  89. pyrig/src/os/__init__.py +6 -0
  90. pyrig/src/os/os.py +121 -0
  91. pyrig/src/project/__init__.py +5 -0
  92. pyrig/src/project/mgt.py +83 -0
  93. pyrig/src/resource.py +58 -0
  94. pyrig/src/string.py +100 -0
  95. pyrig/src/testing/__init__.py +6 -0
  96. pyrig/src/testing/assertions.py +66 -0
  97. pyrig/src/testing/convention.py +203 -0
  98. pyrig-2.2.6.dist-info/METADATA +174 -0
  99. pyrig-2.2.6.dist-info/RECORD +102 -0
  100. pyrig-2.2.6.dist-info/WHEEL +4 -0
  101. pyrig-2.2.6.dist-info/entry_points.txt +3 -0
  102. 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."""