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,106 @@
1
+ """GitHub Actions workflow for building artifacts.
2
+
3
+ This module provides the BuildWorkflow class for creating
4
+ a workflow that builds artifacts across OS matrix after
5
+ successful health checks on main branch.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from pyrig.dev.configs.workflows.base.base import Workflow
11
+ from pyrig.dev.configs.workflows.health_check import HealthCheckWorkflow
12
+
13
+
14
+ class BuildWorkflow(Workflow):
15
+ """Workflow for building project artifacts.
16
+
17
+ Triggers after health check workflow completes on main branch.
18
+ Builds artifacts across OS matrix and uploads them for the
19
+ release workflow to use.
20
+ """
21
+
22
+ @classmethod
23
+ def get_workflow_triggers(cls) -> dict[str, Any]:
24
+ """Get the workflow triggers.
25
+
26
+ Returns:
27
+ Trigger for health check completion on main.
28
+ """
29
+ triggers = super().get_workflow_triggers()
30
+ triggers.update(
31
+ cls.on_workflow_run(
32
+ workflows=[HealthCheckWorkflow.get_workflow_name()],
33
+ branches=["main"],
34
+ )
35
+ )
36
+ return triggers
37
+
38
+ @classmethod
39
+ def get_jobs(cls) -> dict[str, Any]:
40
+ """Get the workflow jobs.
41
+
42
+ Returns:
43
+ Dict with build job.
44
+ """
45
+ jobs: dict[str, Any] = {}
46
+ jobs.update(cls.job_build_artifacts())
47
+ jobs.update(cls.job_build_container_image())
48
+ return jobs
49
+
50
+ @classmethod
51
+ def job_build_artifacts(cls) -> dict[str, Any]:
52
+ """Get the build job that runs across OS matrix.
53
+
54
+ Returns:
55
+ Job configuration for building artifacts.
56
+ """
57
+ return cls.get_job(
58
+ job_func=cls.job_build_artifacts,
59
+ if_condition=cls.if_workflow_run_is_success(),
60
+ strategy=cls.strategy_matrix_os(),
61
+ runs_on=cls.insert_matrix_os(),
62
+ steps=cls.steps_build_artifacts(),
63
+ )
64
+
65
+ @classmethod
66
+ def job_build_container_image(cls) -> dict[str, Any]:
67
+ """Get the build job that builds the container image.
68
+
69
+ Returns:
70
+ Job configuration for building container image.
71
+ """
72
+ return cls.get_job(
73
+ job_func=cls.job_build_container_image,
74
+ if_condition=cls.if_workflow_run_is_success(),
75
+ runs_on=cls.UBUNTU_LATEST,
76
+ steps=cls.steps_build_container_image(),
77
+ )
78
+
79
+ @classmethod
80
+ def steps_build_artifacts(cls) -> list[dict[str, Any]]:
81
+ """Get the steps for building artifacts.
82
+
83
+ Returns:
84
+ List of build steps, or placeholder if no builders defined.
85
+ """
86
+ return [
87
+ *cls.steps_core_matrix_setup(),
88
+ cls.step_build_artifacts(),
89
+ cls.step_upload_artifacts(),
90
+ ]
91
+
92
+ @classmethod
93
+ def steps_build_container_image(cls) -> list[dict[str, Any]]:
94
+ """Get the steps for building the container image.
95
+
96
+ Returns:
97
+ List of build steps.
98
+ """
99
+ return [
100
+ cls.step_checkout_repository(),
101
+ cls.step_install_container_engine(),
102
+ cls.step_build_container_image(),
103
+ cls.step_make_dist_folder(),
104
+ cls.step_save_container_image(),
105
+ cls.step_upload_artifacts(name="container-image"),
106
+ ]
@@ -0,0 +1,133 @@
1
+ """GitHub Actions workflow for health checks and CI.
2
+
3
+ This module provides the HealthCheckWorkflow class for creating
4
+ a workflow that runs on pull requests, pushes, and scheduled intervals
5
+ to verify code quality and run tests.
6
+ """
7
+
8
+ from datetime import UTC, datetime, timedelta
9
+ from typing import Any
10
+
11
+ import pyrig
12
+ from pyrig.dev.configs.workflows.base.base import Workflow
13
+ from pyrig.dev.utils.packages import get_src_package
14
+ from pyrig.src.modules.package import DependencyGraph
15
+
16
+
17
+ class HealthCheckWorkflow(Workflow):
18
+ """Workflow for continuous integration health checks.
19
+
20
+ Triggers on pull requests, pushes to main, and scheduled intervals.
21
+ Runs linting, type checking, security scanning, and tests across
22
+ a matrix of OS and Python versions.
23
+ """
24
+
25
+ BASE_CRON_HOUR = 0
26
+
27
+ @classmethod
28
+ def get_workflow_triggers(cls) -> dict[str, Any]:
29
+ """Get the workflow triggers.
30
+
31
+ Returns:
32
+ Triggers for pull requests, pushes, and scheduled runs.
33
+ """
34
+ triggers = super().get_workflow_triggers()
35
+ triggers.update(cls.on_pull_request())
36
+ triggers.update(cls.on_push())
37
+ triggers.update(cls.on_schedule(cron=cls.get_staggered_cron()))
38
+ return triggers
39
+
40
+ @classmethod
41
+ def get_staggered_cron(cls) -> str:
42
+ """Get a staggered cron schedule based on dependency depth.
43
+
44
+ Packages with more dependencies run later to avoid conflicts
45
+ when dependencies release right before dependent packages.
46
+
47
+ Returns:
48
+ Cron expression with hour offset based on dependency depth.
49
+ """
50
+ offset = cls.get_dependency_offset()
51
+ base_time = datetime.now(tz=UTC).replace(
52
+ hour=cls.BASE_CRON_HOUR, minute=0, second=0, microsecond=0
53
+ )
54
+ scheduled_time = base_time + timedelta(hours=offset)
55
+ return f"0 {scheduled_time.hour} * * *"
56
+
57
+ @classmethod
58
+ def get_dependency_offset(cls) -> int:
59
+ """Calculate hour offset based on dependency depth to pyrig.
60
+
61
+ Returns:
62
+ Number of hours to offset from base cron hour.
63
+ """
64
+ graph = DependencyGraph()
65
+ src_pkg = get_src_package()
66
+ return graph.shortest_path_length(src_pkg.__name__, pyrig.__name__)
67
+
68
+ @classmethod
69
+ def get_jobs(cls) -> dict[str, Any]:
70
+ """Get the workflow jobs.
71
+
72
+ Returns:
73
+ Dict with matrix and aggregation jobs.
74
+ """
75
+ jobs: dict[str, Any] = {}
76
+ jobs.update(cls.job_health_check_matrix())
77
+ jobs.update(cls.job_health_check())
78
+ return jobs
79
+
80
+ @classmethod
81
+ def job_health_check_matrix(cls) -> dict[str, Any]:
82
+ """Get the matrix job that runs across OS and Python versions.
83
+
84
+ Returns:
85
+ Job configuration for matrix testing.
86
+ """
87
+ return cls.get_job(
88
+ job_func=cls.job_health_check_matrix,
89
+ strategy=cls.strategy_matrix_os_and_python_version(),
90
+ runs_on=cls.insert_matrix_os(),
91
+ steps=cls.steps_health_check_matrix(),
92
+ )
93
+
94
+ @classmethod
95
+ def job_health_check(cls) -> dict[str, Any]:
96
+ """Get the aggregation job that depends on matrix completion.
97
+
98
+ Returns:
99
+ Job configuration for result aggregation.
100
+ """
101
+ return cls.get_job(
102
+ job_func=cls.job_health_check,
103
+ needs=[cls.make_id_from_func(cls.job_health_check_matrix)],
104
+ steps=cls.steps_aggregate_matrix_results(),
105
+ )
106
+
107
+ @classmethod
108
+ def steps_health_check_matrix(cls) -> list[dict[str, Any]]:
109
+ """Get the steps for the matrix health check job.
110
+
111
+ Returns:
112
+ List of steps for setup, linting, and testing.
113
+ """
114
+ return [
115
+ *cls.steps_core_matrix_setup(
116
+ python_version=cls.insert_matrix_python_version(),
117
+ ),
118
+ cls.step_protect_repository(),
119
+ cls.step_run_pre_commit_hooks(),
120
+ cls.step_run_tests(),
121
+ cls.step_upload_coverage_report(),
122
+ ]
123
+
124
+ @classmethod
125
+ def steps_aggregate_matrix_results(cls) -> list[dict[str, Any]]:
126
+ """Get the steps for aggregating matrix results.
127
+
128
+ Returns:
129
+ List with the aggregation step.
130
+ """
131
+ return [
132
+ cls.step_aggregate_matrix_results(),
133
+ ]
@@ -0,0 +1,68 @@
1
+ """GitHub Actions workflow for publishing to PyPI.
2
+
3
+ This module provides the PublishWorkflow class for creating
4
+ a workflow that publishes the package to PyPI after a successful release.
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ from pyrig.dev.configs.workflows.base.base import Workflow
10
+ from pyrig.dev.configs.workflows.release import ReleaseWorkflow
11
+
12
+
13
+ class PublishWorkflow(Workflow):
14
+ """Workflow for publishing packages to PyPI.
15
+
16
+ Triggers after the release workflow completes successfully.
17
+ Builds a wheel and publishes it to PyPI.
18
+ """
19
+
20
+ @classmethod
21
+ def get_workflow_triggers(cls) -> dict[str, Any]:
22
+ """Get the workflow triggers.
23
+
24
+ Returns:
25
+ Trigger for release workflow completion.
26
+ """
27
+ triggers = super().get_workflow_triggers()
28
+ triggers.update(
29
+ cls.on_workflow_run(workflows=[ReleaseWorkflow.get_workflow_name()])
30
+ )
31
+ return triggers
32
+
33
+ @classmethod
34
+ def get_jobs(cls) -> dict[str, Any]:
35
+ """Get the workflow jobs.
36
+
37
+ Returns:
38
+ Dict with the publish job.
39
+ """
40
+ jobs: dict[str, Any] = {}
41
+ jobs.update(cls.job_publish())
42
+ return jobs
43
+
44
+ @classmethod
45
+ def job_publish(cls) -> dict[str, Any]:
46
+ """Get the publish job configuration.
47
+
48
+ Returns:
49
+ Job that builds and publishes to PyPI.
50
+ """
51
+ return cls.get_job(
52
+ job_func=cls.job_publish,
53
+ steps=cls.steps_publish(),
54
+ if_condition=cls.if_workflow_run_is_success(),
55
+ )
56
+
57
+ @classmethod
58
+ def steps_publish(cls) -> list[dict[str, Any]]:
59
+ """Get the steps for publishing.
60
+
61
+ Returns:
62
+ List of steps for setup, build, and publish.
63
+ """
64
+ return [
65
+ *cls.steps_core_setup(),
66
+ cls.step_build_wheel(),
67
+ cls.step_publish_to_pypi(),
68
+ ]
@@ -0,0 +1,90 @@
1
+ """GitHub Actions workflow for creating releases.
2
+
3
+ This module provides the ReleaseWorkflow class for creating
4
+ a workflow that creates tags and publishes GitHub releases
5
+ after successful build workflow completion.
6
+ """
7
+
8
+ from typing import Any
9
+
10
+ from pyrig.dev.configs.workflows.base.base import Workflow
11
+ from pyrig.dev.configs.workflows.build import BuildWorkflow
12
+
13
+
14
+ class ReleaseWorkflow(Workflow):
15
+ """Workflow for creating GitHub releases.
16
+
17
+ Triggers after build workflow completes successfully.
18
+ Downloads artifacts from the build workflow, creates version tags,
19
+ generates changelogs, and publishes GitHub releases.
20
+ """
21
+
22
+ @classmethod
23
+ def get_workflow_triggers(cls) -> dict[str, Any]:
24
+ """Get the workflow triggers.
25
+
26
+ Returns:
27
+ Trigger for build workflow completion.
28
+ """
29
+ triggers = super().get_workflow_triggers()
30
+ triggers.update(
31
+ cls.on_workflow_run(
32
+ workflows=[BuildWorkflow.get_workflow_name()],
33
+ )
34
+ )
35
+ return triggers
36
+
37
+ @classmethod
38
+ def get_permissions(cls) -> dict[str, Any]:
39
+ """Get the workflow permissions.
40
+
41
+ Returns:
42
+ Permissions with write access for creating releases.
43
+ """
44
+ permissions = super().get_permissions()
45
+ permissions["contents"] = "write"
46
+ permissions["actions"] = "read"
47
+ return permissions
48
+
49
+ @classmethod
50
+ def get_jobs(cls) -> dict[str, Any]:
51
+ """Get the workflow jobs.
52
+
53
+ Returns:
54
+ Dict with release job.
55
+ """
56
+ jobs: dict[str, Any] = {}
57
+ jobs.update(cls.job_release())
58
+ return jobs
59
+
60
+ @classmethod
61
+ def job_release(cls) -> dict[str, Any]:
62
+ """Get the release job that creates the GitHub release.
63
+
64
+ Returns:
65
+ Job configuration for creating releases.
66
+ """
67
+ return cls.get_job(
68
+ job_func=cls.job_release,
69
+ if_condition=cls.if_workflow_run_is_success(),
70
+ steps=cls.steps_release(),
71
+ )
72
+
73
+ @classmethod
74
+ def steps_release(cls) -> list[dict[str, Any]]:
75
+ """Get the steps for creating the release.
76
+
77
+ Returns:
78
+ List of steps for tagging, changelog, and release creation.
79
+ """
80
+ return [
81
+ *cls.steps_core_installed_setup(repo_token=True),
82
+ cls.step_run_pre_commit_hooks(),
83
+ cls.step_commit_added_changes(),
84
+ cls.step_push_commits(),
85
+ cls.step_create_and_push_tag(),
86
+ cls.step_extract_version(),
87
+ cls.step_download_artifacts_from_workflow_run(),
88
+ cls.step_build_changelog(),
89
+ cls.step_create_release(),
90
+ ]
@@ -0,0 +1,5 @@
1
+ """Test infrastructure for pyrig-based projects.
2
+
3
+ This package provides pytest fixtures, utilities, and configuration
4
+ for automated testing of pyrig projects.
5
+ """
@@ -0,0 +1,40 @@
1
+ """Pytest configuration for pyrig tests.
2
+
3
+ This module automatically discovers and registers pytest plugins from all
4
+ packages that depend on pyrig. It finds fixtures modules across the dependency
5
+ graph and adds them to pytest_plugins for automatic fixture availability.
6
+
7
+ The discovery process:
8
+ 1. Finds all packages depending on pyrig
9
+ 2. Locates their fixtures modules
10
+ 3. Collects all Python files within those modules
11
+ 4. Registers them as pytest plugins
12
+ """
13
+
14
+ from pathlib import Path
15
+
16
+ import pyrig
17
+ from pyrig.dev.tests import fixtures
18
+ from pyrig.src.modules.module import (
19
+ get_same_modules_from_deps_depen_on_dep,
20
+ to_module_name,
21
+ to_path,
22
+ )
23
+
24
+ # find the fixtures module in all packages that depend on pyrig
25
+ # and add all paths to pytest_plugins
26
+ fixtures_pkgs = get_same_modules_from_deps_depen_on_dep(fixtures, pyrig)
27
+
28
+
29
+ pytest_plugin_paths: list[Path] = []
30
+ for pkg in fixtures_pkgs:
31
+ absolute_path = Path(pkg.__path__[0])
32
+ relative_path = to_path(pkg.__name__, is_package=True)
33
+
34
+ pkg_root = Path(absolute_path.as_posix().removesuffix(relative_path.as_posix()))
35
+
36
+ for path in absolute_path.rglob("*.py"):
37
+ rel_plugin_path = path.relative_to(pkg_root)
38
+ pytest_plugin_paths.append(rel_plugin_path)
39
+
40
+ pytest_plugins = [to_module_name(path) for path in pytest_plugin_paths]
@@ -0,0 +1 @@
1
+ """__init__ module."""
@@ -0,0 +1,147 @@
1
+ """Fixtures that assert some state or condition."""
2
+
3
+ import logging
4
+ import runpy
5
+ import sys
6
+ from collections.abc import Callable
7
+ from importlib import import_module
8
+ from types import ModuleType
9
+ from typing import Any
10
+
11
+ import pytest
12
+ from pytest_mock import MockerFixture
13
+
14
+ from pyrig import main
15
+ from pyrig.dev.cli.commands.create_tests import make_test_skeletons
16
+ from pyrig.dev.configs.pyproject import PyprojectConfigFile
17
+ from pyrig.dev.configs.python.main import MainConfigFile
18
+ from pyrig.dev.utils.testing import session_fixture
19
+ from pyrig.src.modules.module import (
20
+ get_module_content_as_str,
21
+ get_module_name_replacing_start_module,
22
+ get_objs_from_obj,
23
+ make_obj_importpath,
24
+ )
25
+ from pyrig.src.os.os import run_subprocess
26
+ from pyrig.src.project.mgt import PROJECT_MGT_RUN_ARGS
27
+ from pyrig.src.testing.assertions import assert_with_msg
28
+ from pyrig.src.testing.convention import (
29
+ get_obj_from_test_obj,
30
+ make_summary_error_msg,
31
+ make_test_obj_importpath_from_obj,
32
+ )
33
+
34
+ logger = logging.getLogger(__name__)
35
+
36
+
37
+ @session_fixture
38
+ def assert_no_untested_objs() -> Callable[
39
+ [ModuleType | type | Callable[..., Any]], None
40
+ ]:
41
+ """Fixture that asserts that all objects of an object have corresponding tests.
42
+
43
+ This fixture provides a function that can be called to assert that all objects
44
+ (functions, classes, or methods) in a given module, class, or function have
45
+ corresponding test objects in the test module, class, or function.
46
+
47
+ """
48
+
49
+ def _assert_no_untested_objs(
50
+ test_obj: ModuleType | type | Callable[..., Any],
51
+ ) -> None:
52
+ """Assert that all objects in the source have corresponding test objects.
53
+
54
+ This function verifies that every object (function, class, or method) in the
55
+ source module or class has a corresponding test object
56
+ in the test module or class.
57
+
58
+ Args:
59
+ test_obj: The test object (module, class, or function) to check
60
+
61
+ Raises:
62
+ AssertionError: If any object lacks a corresponding test object,
63
+ with a detailed error message listing the untested objects
64
+
65
+ """
66
+ test_objs = get_objs_from_obj(test_obj)
67
+ test_objs_paths = {make_obj_importpath(obj) for obj in test_objs}
68
+
69
+ try:
70
+ obj = get_obj_from_test_obj(test_obj)
71
+ except ImportError:
72
+ if isinstance(test_obj, ModuleType):
73
+ # we skip if module not found bc that means it has custom tests
74
+ # and is not part of the mirrored structure
75
+ logger.warning("No source module found for %s, skipping", test_obj)
76
+ return
77
+ raise
78
+ objs = get_objs_from_obj(obj)
79
+ test_obj_path_to_obj = {
80
+ make_test_obj_importpath_from_obj(obj): obj for obj in objs
81
+ }
82
+
83
+ missing_test_obj_path_to_obj = {
84
+ test_path: obj
85
+ for test_path, obj in test_obj_path_to_obj.items()
86
+ if test_path not in test_objs_paths
87
+ }
88
+
89
+ # get the modules of these obj
90
+ if missing_test_obj_path_to_obj:
91
+ make_test_skeletons()
92
+
93
+ msg = f"""Found missing tests. Tests skeletons were automatically created for:
94
+ {make_summary_error_msg(missing_test_obj_path_to_obj.keys())}
95
+ """
96
+
97
+ assert_with_msg(
98
+ not missing_test_obj_path_to_obj,
99
+ msg,
100
+ )
101
+
102
+ return _assert_no_untested_objs
103
+
104
+
105
+ @pytest.fixture
106
+ def main_test_fixture(mocker: MockerFixture) -> None:
107
+ """Fixture for testing main."""
108
+ project_name = PyprojectConfigFile.get_project_name()
109
+ src_package_name = PyprojectConfigFile.get_package_name()
110
+
111
+ cmds = [
112
+ [*PROJECT_MGT_RUN_ARGS, project_name, "--help"],
113
+ [*PROJECT_MGT_RUN_ARGS, project_name, main.main.__name__, "--help"],
114
+ ]
115
+ success = False
116
+ for cmd in cmds:
117
+ completed_process = run_subprocess(cmd, check=False)
118
+ if completed_process.returncode == 0:
119
+ success = True
120
+ break
121
+ else:
122
+ cmd_strs = [" ".join(cmd) for cmd in cmds]
123
+ assert_with_msg(
124
+ success,
125
+ f"Expected {main.main.__name__} to be callable by one of {cmd_strs}",
126
+ )
127
+
128
+ main_module_name = get_module_name_replacing_start_module(main, src_package_name)
129
+ main_module = import_module(main_module_name)
130
+ main_mock = mocker.patch.object(main_module, main.main.__name__)
131
+ main_module.main()
132
+ assert_with_msg(
133
+ main_mock.call_count == 1,
134
+ f"Expected main to be called, got {main_mock.call_count}",
135
+ )
136
+
137
+ # must run main module directly as __main__
138
+ # so that pytest-cov sees that it calls main
139
+ # remove module if already imported, so run_module reloads it
140
+ del sys.modules[main_module_name]
141
+ # run module as __main__, pytest-cov will see it
142
+ # run only if file content is the same as pyrig.main
143
+ main_module_content = get_module_content_as_str(main_module)
144
+ config_main_module_content = MainConfigFile.get_content_str()
145
+
146
+ if main_module_content == config_main_module_content:
147
+ runpy.run_module(main_module_name, run_name="__main__")
@@ -0,0 +1,5 @@
1
+ """Scope-specific pytest fixtures.
2
+
3
+ This package organizes fixtures by their pytest scope (function, class,
4
+ module, package, session) for automatic discovery and registration.
5
+ """
@@ -0,0 +1,42 @@
1
+ """Class-level test fixtures and utilities.
2
+
3
+ These fixtures in this module are automatically applied to all test classes
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_class_fixture from pyrig.src.testing.fixtures or with pytest's
7
+ autouse mechanism @pytest.fixture(scope="class", autouse=True).
8
+ """
9
+
10
+ from collections.abc import Callable
11
+ from types import ModuleType
12
+ from typing import Any
13
+
14
+ import pytest
15
+
16
+ from pyrig.dev.utils.testing import autouse_class_fixture
17
+
18
+
19
+ @autouse_class_fixture
20
+ def assert_all_methods_tested(
21
+ request: pytest.FixtureRequest,
22
+ assert_no_untested_objs: Callable[[ModuleType | type | Callable[..., Any]], None],
23
+ ) -> None:
24
+ """Verify that all methods in a class have corresponding tests.
25
+
26
+ This fixture runs automatically for each test class and checks that every
27
+ method defined in the corresponding source class has a test method defined
28
+ in the test class.
29
+
30
+ Args:
31
+ request: The pytest fixture request object containing the current class
32
+ assert_no_untested_objs: The assert_no_untested_objs fixture asserts
33
+ that all objects have corresponding tests
34
+
35
+ Raises:
36
+ AssertionError: If any method in the source class lacks a test
37
+
38
+ """
39
+ class_ = request.node.cls
40
+ if class_ is None:
41
+ return
42
+ assert_no_untested_objs(class_)
@@ -0,0 +1,40 @@
1
+ """Module-level test fixtures and utilities.
2
+
3
+ These fixtures in this module are automatically applied to all test modules
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_module_fixture from pyrig.src.testing.fixtures or with pytest's
7
+ autouse mechanism @pytest.fixture(scope="module", autouse=True).
8
+ """
9
+
10
+ from collections.abc import Callable
11
+ from types import ModuleType
12
+ from typing import Any
13
+
14
+ import pytest
15
+
16
+ from pyrig.dev.utils.testing import autouse_module_fixture
17
+
18
+
19
+ @autouse_module_fixture
20
+ def assert_all_funcs_and_classes_tested(
21
+ request: pytest.FixtureRequest,
22
+ assert_no_untested_objs: Callable[[ModuleType | type | Callable[..., Any]], None],
23
+ ) -> None:
24
+ """Verify that all functions and classes in a module have corresponding tests.
25
+
26
+ This fixture runs automatically for each test module and checks that every
27
+ function and class defined in the corresponding source module has a test
28
+ function or class defined in the test module.
29
+
30
+ Args:
31
+ request: The pytest fixture request object containing the current module
32
+ assert_no_untested_objs: The assert_no_untested_objs fixture asserts
33
+ that all objects have corresponding tests
34
+
35
+ Raises:
36
+ AssertionError: If any function or class in the source module lacks a test
37
+
38
+ """
39
+ module: ModuleType = request.module
40
+ assert_no_untested_objs(module)