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
pyrig/src/resource.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Resource file access utilities for development and PyInstaller builds.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for accessing static resource files (images,
|
|
4
|
+
configuration files, data files, etc.) in a way that works both during
|
|
5
|
+
development and when the application is bundled with PyInstaller.
|
|
6
|
+
|
|
7
|
+
When running from source, resources are accessed from the filesystem. When
|
|
8
|
+
running from a PyInstaller bundle, resources are extracted from the frozen
|
|
9
|
+
executable's temporary directory (MEIPASS). Using `importlib.resources`
|
|
10
|
+
handles both cases transparently.
|
|
11
|
+
|
|
12
|
+
Resources should be placed in `pkg/dev/artifacts/resources/` directories
|
|
13
|
+
and accessed via the `get_resource_path` function.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
>>> import my_project.dev.artifacts.resources as resources
|
|
17
|
+
>>> from pyrig.src.resource import get_resource_path
|
|
18
|
+
>>> config_path = get_resource_path("config.json", resources)
|
|
19
|
+
>>> data = config_path.read_text()
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from importlib.resources import as_file, files
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from types import ModuleType
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_resource_path(name: str, package: ModuleType) -> Path:
|
|
28
|
+
"""Get the filesystem path to a resource file.
|
|
29
|
+
|
|
30
|
+
Resolves the path to a resource file within a package, handling both
|
|
31
|
+
development (source) and production (PyInstaller bundle) environments.
|
|
32
|
+
Uses `importlib.resources` to ensure compatibility with frozen executables.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
name: The filename of the resource including extension
|
|
36
|
+
(e.g., "icon.png", "config.json").
|
|
37
|
+
package: The package module containing the resource. This should be
|
|
38
|
+
the `resources` package itself, not a parent package.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A Path object pointing to the resource file. In development, this
|
|
42
|
+
is the actual file path. In a PyInstaller bundle, this is a path
|
|
43
|
+
to the extracted file in the temporary directory.
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
>>> import my_app.dev.artifacts.resources as resources
|
|
47
|
+
>>> icon_path = get_resource_path("icon.png", resources)
|
|
48
|
+
>>> print(icon_path)
|
|
49
|
+
/path/to/my_app/dev/artifacts/resources/icon.png
|
|
50
|
+
|
|
51
|
+
Note:
|
|
52
|
+
The returned path is only valid within the context of the current
|
|
53
|
+
process. For PyInstaller bundles, the file is extracted to a
|
|
54
|
+
temporary directory that is cleaned up when the process exits.
|
|
55
|
+
"""
|
|
56
|
+
resource_path = files(package) / name
|
|
57
|
+
with as_file(resource_path) as path:
|
|
58
|
+
return path
|
pyrig/src/string.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""String manipulation and naming convention utilities.
|
|
2
|
+
|
|
3
|
+
This module provides utility functions for string transformations commonly needed
|
|
4
|
+
when working with Python naming conventions. It handles conversions between
|
|
5
|
+
different case styles (snake_case, PascalCase, kebab-case) and creates
|
|
6
|
+
human-readable names from Python objects.
|
|
7
|
+
|
|
8
|
+
These utilities are particularly useful for:
|
|
9
|
+
- Generating CLI command names from function names
|
|
10
|
+
- Creating display names for classes and modules
|
|
11
|
+
- Parsing and transforming identifiers
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
>>> from pyrig.src.string import split_on_uppercase, make_name_from_obj
|
|
15
|
+
>>> split_on_uppercase("MyClassName")
|
|
16
|
+
['My', 'Class', 'Name']
|
|
17
|
+
>>> make_name_from_obj("my_function_name")
|
|
18
|
+
'My-Function-Name'
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
from types import ModuleType
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def split_on_uppercase(string: str) -> list[str]:
|
|
28
|
+
"""Split a string at uppercase letter boundaries.
|
|
29
|
+
|
|
30
|
+
Useful for parsing PascalCase or camelCase identifiers into their
|
|
31
|
+
component words. Empty strings between consecutive uppercase letters
|
|
32
|
+
are filtered out.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
string: The string to split, typically in PascalCase or camelCase.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
A list of substrings, each starting with an uppercase letter
|
|
39
|
+
(except possibly the first if the original string started lowercase).
|
|
40
|
+
|
|
41
|
+
Example:
|
|
42
|
+
>>> split_on_uppercase("HelloWorld")
|
|
43
|
+
['Hello', 'World']
|
|
44
|
+
>>> split_on_uppercase("XMLParser")
|
|
45
|
+
['X', 'M', 'L', 'Parser']
|
|
46
|
+
>>> split_on_uppercase("lowercase")
|
|
47
|
+
['lowercase']
|
|
48
|
+
"""
|
|
49
|
+
return [s for s in re.split(r"(?=[A-Z])", string) if s]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def make_name_from_obj(
|
|
53
|
+
obj: ModuleType | Callable[..., Any] | type | str,
|
|
54
|
+
split_on: str = "_",
|
|
55
|
+
join_on: str = "-",
|
|
56
|
+
*,
|
|
57
|
+
capitalize: bool = True,
|
|
58
|
+
) -> str:
|
|
59
|
+
"""Create a human-readable name from a Python object or string.
|
|
60
|
+
|
|
61
|
+
Transforms Python identifiers (typically in snake_case) into formatted
|
|
62
|
+
display names. Commonly used to generate CLI command names, display
|
|
63
|
+
labels, or documentation titles from function/class/module names.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
obj: The object to extract a name from. Can be a module, callable,
|
|
67
|
+
class, or string. For non-string objects, uses the last component
|
|
68
|
+
of `__name__` (e.g., "my_module" from "package.my_module").
|
|
69
|
+
split_on: Character(s) to split the name on. Defaults to underscore
|
|
70
|
+
for snake_case input.
|
|
71
|
+
join_on: Character(s) to join the parts with. Defaults to hyphen
|
|
72
|
+
for kebab-case output.
|
|
73
|
+
capitalize: Whether to capitalize each word. When True, produces
|
|
74
|
+
Title-Case output.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
A formatted string with parts split and rejoined according to the
|
|
78
|
+
specified separators.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> make_name_from_obj("my_function_name")
|
|
82
|
+
'My-Function-Name'
|
|
83
|
+
>>> make_name_from_obj("my_function", join_on=" ", capitalize=True)
|
|
84
|
+
'My Function'
|
|
85
|
+
>>> import os
|
|
86
|
+
>>> make_name_from_obj(os.path)
|
|
87
|
+
'Path'
|
|
88
|
+
"""
|
|
89
|
+
if not isinstance(obj, str):
|
|
90
|
+
name = getattr(obj, "__name__", "")
|
|
91
|
+
if not name:
|
|
92
|
+
msg = f"Cannot extract name from {obj}"
|
|
93
|
+
raise ValueError(msg)
|
|
94
|
+
obj_name: str = name.split(".")[-1]
|
|
95
|
+
else:
|
|
96
|
+
obj_name = obj
|
|
97
|
+
parts = obj_name.split(split_on)
|
|
98
|
+
if capitalize:
|
|
99
|
+
parts = [part.capitalize() for part in parts]
|
|
100
|
+
return join_on.join(parts)
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Testing utilities and test generation infrastructure.
|
|
2
|
+
|
|
3
|
+
This package provides utilities for test creation, validation, and
|
|
4
|
+
conventions. It includes automatic test skeleton generation, assertion
|
|
5
|
+
helpers, and utilities for mapping between source and test objects.
|
|
6
|
+
"""
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Testing assertion utilities for enhanced test validation.
|
|
2
|
+
|
|
3
|
+
This module provides custom assertion functions that extend Python's built-in
|
|
4
|
+
assert statement with additional features like improved error messages and
|
|
5
|
+
specialized validation logic for common testing scenarios.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pyrig.src.modules.function import is_abstractmethod
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def assert_with_msg(expr: bool, msg: str) -> None: # noqa: FBT001
|
|
14
|
+
"""Assert that an expression is true with a custom error message.
|
|
15
|
+
|
|
16
|
+
A thin wrapper around Python's built-in assert statement that makes it
|
|
17
|
+
easier to provide meaningful error messages when assertions fail.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
expr: The expression to evaluate for truthiness
|
|
21
|
+
msg: The error message to display if the assertion fails
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
AssertionError: If the expression evaluates to False
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
assert expr, msg # noqa: S101 # nosec: B101
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def assert_with_info(expr: bool, expected: Any, actual: Any, msg: str = "") -> None: # noqa: FBT001
|
|
31
|
+
"""Assert that an expression is true with a custom error message.
|
|
32
|
+
|
|
33
|
+
wraps around assert with msg and adds the expected and actual values to the message.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
expr: The expression to evaluate for truthiness
|
|
37
|
+
expected: The expected value
|
|
38
|
+
actual: The actual value
|
|
39
|
+
msg: The error message to display if the assertion fails
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
AssertionError: If the expression evaluates to False
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
msg = f"""
|
|
46
|
+
Expected: {expected}
|
|
47
|
+
Actual: {actual}
|
|
48
|
+
{msg}
|
|
49
|
+
"""
|
|
50
|
+
assert_with_msg(expr, msg)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def assert_isabstrct_method(method: Any) -> None:
|
|
54
|
+
"""Assert that a method is an abstract method.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
method: The method to check
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
AssertionError: If the method is not an abstract method
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
assert_with_msg(
|
|
64
|
+
is_abstractmethod(method),
|
|
65
|
+
f"Expected {method} to be abstract method",
|
|
66
|
+
)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""Test naming conventions and object mapping utilities.
|
|
2
|
+
|
|
3
|
+
This module defines pyrig's test naming conventions and provides bidirectional
|
|
4
|
+
mapping between source objects and their corresponding test objects. The
|
|
5
|
+
conventions follow pytest standards:
|
|
6
|
+
|
|
7
|
+
- Test functions: ``test_<function_name>``
|
|
8
|
+
- Test classes: ``Test<ClassName>``
|
|
9
|
+
- Test modules: ``test_<module_name>.py``
|
|
10
|
+
- Test package: ``tests/``
|
|
11
|
+
|
|
12
|
+
Key functions:
|
|
13
|
+
- ``make_test_obj_name``: Generate test name from source object
|
|
14
|
+
- ``make_test_obj_importpath_from_obj``: Generate test import path
|
|
15
|
+
- ``get_test_obj_from_obj``: Get test object for a source object
|
|
16
|
+
- ``get_obj_from_test_obj``: Get source object for a test object
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
TEST_FUNCTION_PREFIX: Prefix for test functions ("test_").
|
|
20
|
+
TEST_CLASS_PREFIX: Prefix for test classes ("Test").
|
|
21
|
+
TEST_MODULE_PREFIX: Prefix for test modules ("test_").
|
|
22
|
+
TESTS_PACKAGE_NAME: Name of the tests package ("tests").
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from collections.abc import Callable, Iterable
|
|
26
|
+
from types import ModuleType
|
|
27
|
+
from typing import Any, overload
|
|
28
|
+
|
|
29
|
+
from pyrig.src.modules.module import (
|
|
30
|
+
get_isolated_obj_name,
|
|
31
|
+
import_obj_from_importpath,
|
|
32
|
+
make_obj_importpath,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
COVERAGE_THRESHOLD = 90
|
|
36
|
+
|
|
37
|
+
TEST_FUNCTION_PREFIX = "test_"
|
|
38
|
+
|
|
39
|
+
TEST_CLASS_PREFIX = "Test"
|
|
40
|
+
|
|
41
|
+
TEST_MODULE_PREFIX = TEST_FUNCTION_PREFIX
|
|
42
|
+
|
|
43
|
+
TEST_PREFIXES = [TEST_FUNCTION_PREFIX, TEST_CLASS_PREFIX, TEST_MODULE_PREFIX]
|
|
44
|
+
|
|
45
|
+
TESTS_PACKAGE_NAME = "tests"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_right_test_prefix(obj: Callable[..., Any] | type | ModuleType) -> str:
|
|
49
|
+
"""Get the appropriate test prefix for an object based on its type.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
obj: The object to get the test prefix for (function, class, or module)
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The appropriate test prefix string for the given object type
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
if isinstance(obj, ModuleType):
|
|
59
|
+
return TEST_MODULE_PREFIX
|
|
60
|
+
if isinstance(obj, type):
|
|
61
|
+
return TEST_CLASS_PREFIX
|
|
62
|
+
return TEST_FUNCTION_PREFIX
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def make_test_obj_name(obj: Callable[..., Any] | type | ModuleType) -> str:
|
|
66
|
+
"""Create a test name for an object by adding the appropriate prefix.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
obj: The object to create a test name for
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The test name with the appropriate prefix
|
|
73
|
+
|
|
74
|
+
"""
|
|
75
|
+
prefix = get_right_test_prefix(obj)
|
|
76
|
+
name = get_isolated_obj_name(obj)
|
|
77
|
+
return prefix + name
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def reverse_make_test_obj_name(test_name: str) -> str:
|
|
81
|
+
"""Extract the original object name from a test name by removing the prefix.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
test_name: The test name to extract the original name from
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
The original object name without the test prefix
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ValueError: If the test name doesn't start with any of the expected prefixes
|
|
91
|
+
|
|
92
|
+
"""
|
|
93
|
+
for prefix in TEST_PREFIXES:
|
|
94
|
+
if test_name.startswith(prefix):
|
|
95
|
+
return test_name.removeprefix(prefix)
|
|
96
|
+
msg = f"{test_name=} is expected to start with one of {TEST_PREFIXES=}"
|
|
97
|
+
raise ValueError(msg)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def make_test_obj_importpath_from_obj(
|
|
101
|
+
obj: Callable[..., Any] | type | ModuleType,
|
|
102
|
+
) -> str:
|
|
103
|
+
"""Create an import path for a test object based on the original object.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
obj: The original object to create a test import path for
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The import path for the corresponding test object
|
|
110
|
+
|
|
111
|
+
"""
|
|
112
|
+
parts = make_obj_importpath(obj).split(".")
|
|
113
|
+
test_name = make_test_obj_name(obj)
|
|
114
|
+
test_parts = [
|
|
115
|
+
(TEST_MODULE_PREFIX if part[0].islower() else TEST_CLASS_PREFIX) + part
|
|
116
|
+
for part in parts
|
|
117
|
+
]
|
|
118
|
+
test_parts[-1] = test_name
|
|
119
|
+
test_parts.insert(0, TESTS_PACKAGE_NAME)
|
|
120
|
+
return ".".join(test_parts)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def make_obj_importpath_from_test_obj(
|
|
124
|
+
test_obj: Callable[..., Any] | type | ModuleType,
|
|
125
|
+
) -> str:
|
|
126
|
+
"""Create an import path for an original object based on its test object.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
test_obj: The test object to create an original import path for
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The import path for the corresponding original object
|
|
133
|
+
|
|
134
|
+
"""
|
|
135
|
+
test_parts = make_obj_importpath(test_obj).split(".")
|
|
136
|
+
test_parts = test_parts[1:]
|
|
137
|
+
parts = [reverse_make_test_obj_name(part) for part in test_parts]
|
|
138
|
+
return ".".join(parts)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@overload
|
|
142
|
+
def get_test_obj_from_obj(obj: type) -> type: ...
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@overload
|
|
146
|
+
def get_test_obj_from_obj(obj: Callable[..., Any]) -> Callable[..., Any]: ...
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@overload
|
|
150
|
+
def get_test_obj_from_obj(obj: ModuleType) -> ModuleType: ...
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def get_test_obj_from_obj(
|
|
154
|
+
obj: Callable[..., Any] | type | ModuleType,
|
|
155
|
+
) -> Callable[..., Any] | type | ModuleType:
|
|
156
|
+
"""Get the test object corresponding to an original object.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
obj: The original object to get the test object for
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
The corresponding test object
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
test_obj_path = make_test_obj_importpath_from_obj(obj)
|
|
166
|
+
return import_obj_from_importpath(test_obj_path)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def get_obj_from_test_obj(
|
|
170
|
+
test_obj: Callable[..., Any] | type | ModuleType,
|
|
171
|
+
) -> Callable[..., Any] | type | ModuleType:
|
|
172
|
+
"""Get the original object corresponding to a test object.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
test_obj: The test object to get the original object for
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
The corresponding original object
|
|
179
|
+
|
|
180
|
+
"""
|
|
181
|
+
obj_importpath = make_obj_importpath_from_test_obj(test_obj)
|
|
182
|
+
return import_obj_from_importpath(obj_importpath)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def make_summary_error_msg(
|
|
186
|
+
errors_locations: Iterable[str],
|
|
187
|
+
) -> str:
|
|
188
|
+
"""Create an error message summarizing multiple error locations.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
errors_locations: Collection of error locations
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
A formatted error message listing all error locations
|
|
195
|
+
"""
|
|
196
|
+
msg = """
|
|
197
|
+
Found errors at:
|
|
198
|
+
"""
|
|
199
|
+
for error_location in errors_locations:
|
|
200
|
+
msg += f"""
|
|
201
|
+
- {error_location}
|
|
202
|
+
"""
|
|
203
|
+
return msg
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyrig
|
|
3
|
+
Version: 2.2.6
|
|
4
|
+
Summary: A Python toolkit that standardizes and automates project setup, configuration and development.
|
|
5
|
+
Author: Winipedia
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
10
|
+
Requires-Dist: typer>=0.20.0
|
|
11
|
+
Requires-Python: >=3.12
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# pyrig
|
|
15
|
+
|
|
16
|
+
<!-- tooling -->
|
|
17
|
+
[](https://github.com/Winipedia/pyrig)
|
|
18
|
+
[](https://github.com/astral-sh/uv)
|
|
19
|
+
[](https://podman.io/)
|
|
20
|
+
[](https://pre-commit.com/)
|
|
21
|
+
<!-- code-quality -->
|
|
22
|
+
[](https://github.com/astral-sh/ruff)
|
|
23
|
+
[](https://github.com/astral-sh/ty)[](https://mypy-lang.org/)
|
|
24
|
+
[](https://github.com/PyCQA/bandit)
|
|
25
|
+
[](https://pytest.org/)
|
|
26
|
+
[](https://codecov.io/gh/Winipedia/pyrig)
|
|
27
|
+
<!-- package-info -->
|
|
28
|
+
[](https://pypi.org/project/pyrig/)
|
|
29
|
+
[](https://www.python.org/)
|
|
30
|
+
[](https://github.com/Winipedia/pyrig/blob/main/LICENSE)
|
|
31
|
+
<!-- ci/cd -->
|
|
32
|
+
[](https://github.com/Winipedia/pyrig/actions/workflows/health_check.yaml)
|
|
33
|
+
[](https://github.com/Winipedia/pyrig/actions/workflows/release.yaml)
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
> A Python toolkit that standardizes and automates project setup, configuration and development.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## What is pyrig?
|
|
42
|
+
|
|
43
|
+
**pyrig** is an opinionated Python project framework that enforces best practices and keeps your projects up-to-date automatically. Unlike traditional project templates, pyrig is a living system that manages your entire development lifecycle. Pyrig makes project development seemless and keeps you focused on your code. It allows even in bigger project to not lose the overview. It opinionated and best practices approach allows you to always know what belongs where and where to find things.
|
|
44
|
+
|
|
45
|
+
### Key Features
|
|
46
|
+
|
|
47
|
+
- **Automated Setup** - Initialize production-ready projects in seconds with `pyrig init`
|
|
48
|
+
- **Living Configuration** - Configs stay synchronized automatically, no manual maintenance
|
|
49
|
+
- **Enforced Quality** - Strict linting, type checking, testing, and security scanning out of the box
|
|
50
|
+
- **Always Current** - Automatic dependency updates and latest tool versions via CI/CD
|
|
51
|
+
- **Multi-Package Support** - Build package ecosystems with cross-package discovery of ConfigFiles, Builders, fixtures, and CLI commands
|
|
52
|
+
- **Extensible Architecture** - Plugin system for custom ConfigFiles and Builders
|
|
53
|
+
|
|
54
|
+
### Philosophy
|
|
55
|
+
|
|
56
|
+
pyrig is designed for **serious, long-term Python projects** where code quality and maintainability matter. It makes opinionated choices about tooling and enforces best practices, so you can focus on building features instead of configuring tools and wondering whta is the best way to do something.
|
|
57
|
+
|
|
58
|
+
## Quick Start
|
|
59
|
+
|
|
60
|
+
### Prerequisites
|
|
61
|
+
|
|
62
|
+
- **Git** with username matching your GitHub username
|
|
63
|
+
- **uv** package manager
|
|
64
|
+
- **Podman** (for containerization)
|
|
65
|
+
|
|
66
|
+
### Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Create a new GitHub repository (don't initialize with README)
|
|
70
|
+
# Clone it locally
|
|
71
|
+
git clone https://github.com/YourUsername/your-project.git
|
|
72
|
+
cd your-project
|
|
73
|
+
|
|
74
|
+
# Initialize with uv
|
|
75
|
+
uv init
|
|
76
|
+
|
|
77
|
+
# Add pyrig
|
|
78
|
+
uv add pyrig
|
|
79
|
+
|
|
80
|
+
# Initialize your project (this does everything!)
|
|
81
|
+
uv run pyrig init
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
That's it! You now have a fully configured project with:
|
|
85
|
+
- Automated testing with pytest (90% coverage requirement)
|
|
86
|
+
- Linting and formatting with ruff (ALL rules enabled)
|
|
87
|
+
- Type checking with ty and mypy (strict mode)
|
|
88
|
+
- Security scanning with bandit
|
|
89
|
+
- Pre-commit hooks for quality enforcement
|
|
90
|
+
- GitHub Actions CI/CD workflows
|
|
91
|
+
- Branch protection and repository security
|
|
92
|
+
- Containerfile for deployment
|
|
93
|
+
- Custom CLI commands via `dev/cli/subcommands.py` and `dev/cli/shared_subcommands.py`
|
|
94
|
+
|
|
95
|
+
### Next Steps
|
|
96
|
+
|
|
97
|
+
After initialization, start coding in `<package>/src/` and run:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
uv run pyrig mktests # Generate test skeletons
|
|
101
|
+
uv run pytest # Run tests
|
|
102
|
+
git add .
|
|
103
|
+
git commit -m "Add feature" # Pre-commit hooks run automatically
|
|
104
|
+
git push
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Documentation
|
|
108
|
+
|
|
109
|
+
- **[Documentation Index](docs/index.md)** - Full documentation
|
|
110
|
+
- **[Getting Started Guide](docs/getting-started.md)** - Complete guide to creating your first pyrig project
|
|
111
|
+
- **[Multi-Package Architecture](docs/multi-package-architecture.md)** - Build package ecosystems with cross-package discovery
|
|
112
|
+
- **[Configuration Files Reference](docs/config-files/index.md)** - Detailed documentation for every config file
|
|
113
|
+
|
|
114
|
+
## Technology Stack
|
|
115
|
+
|
|
116
|
+
pyrig uses cutting-edge Python tooling:
|
|
117
|
+
|
|
118
|
+
- **[uv](https://github.com/astral-sh/uv)** - Fast Python package manager
|
|
119
|
+
- **[ruff](https://github.com/astral-sh/ruff)** - Extremely fast linter and formatter
|
|
120
|
+
- **[ty](https://github.com/astral-sh/ty)** - Fast type checker
|
|
121
|
+
- **[mypy](https://mypy-lang.org/)** - Static type checker
|
|
122
|
+
- **[pytest](https://pytest.org/)** - Testing framework
|
|
123
|
+
- **[bandit](https://github.com/PyCQA/bandit)** - Security scanner
|
|
124
|
+
- **[pre-commit](https://pre-commit.com/)** - Git hook framework
|
|
125
|
+
- **[Podman](https://podman.io/)** - Daemonless container runtime
|
|
126
|
+
- **[GitHub Actions](https://github.com/features/actions)** - CI/CD platform
|
|
127
|
+
|
|
128
|
+
## What Makes pyrig Different?
|
|
129
|
+
|
|
130
|
+
Unlike cookiecutter, copier, or other project templates:
|
|
131
|
+
|
|
132
|
+
- **Living** - Configs stay synchronized automatically, not just at creation time
|
|
133
|
+
- **Opinionated** - Best practices enforced, not suggested
|
|
134
|
+
- **Comprehensive** - Handles everything from init to deployment
|
|
135
|
+
- **Current** - Automatically updates to latest tools and standards
|
|
136
|
+
- **Extensible** - Plugin architecture for custom functionality
|
|
137
|
+
|
|
138
|
+
## Project Structure
|
|
139
|
+
|
|
140
|
+
pyrig creates a clean, organized structure:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
your-project/
|
|
144
|
+
├── <package>/
|
|
145
|
+
│ ├── dev/ # Development tools (not in production)
|
|
146
|
+
│ ├── src/ # Your application code
|
|
147
|
+
│ └── resources/ # Static files
|
|
148
|
+
├── tests/ # Test suite (mirrors src/)
|
|
149
|
+
├── docs/ # Documentation
|
|
150
|
+
├── .github/ # CI/CD workflows
|
|
151
|
+
└── ... # Config files
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Requirements
|
|
155
|
+
|
|
156
|
+
- Git (any recent version)
|
|
157
|
+
- uv package manager
|
|
158
|
+
- GitHub account (for CI/CD and repository protection)
|
|
159
|
+
- Podman (optional, for containerization)
|
|
160
|
+
|
|
161
|
+
## Contributing
|
|
162
|
+
|
|
163
|
+
Contributions are welcome! pyrig is built with pyrig itself, so you can explore the codebase to see how it works.
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT License - see [LICENSE](LICENSE) for details.
|
|
168
|
+
|
|
169
|
+
## Links
|
|
170
|
+
|
|
171
|
+
- **PyPI**: [pypi.org/project/pyrig](https://pypi.org/project/pyrig/)
|
|
172
|
+
- **Documentation**: [docs/](docs/)
|
|
173
|
+
- **Issues**: [github.com/Winipedia/pyrig/issues](https://github.com/Winipedia/pyrig/issues)
|
|
174
|
+
- **Source**: [github.com/Winipedia/pyrig](https://github.com/Winipedia/pyrig)
|