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 @@
|
|
|
1
|
+
"""__init__ module."""
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Configuration management for Containerfile files.
|
|
2
|
+
|
|
3
|
+
This module provides the ContainerfileConfigFile class for managing the
|
|
4
|
+
project's Containerfile file. It fetches GitHub's standard Python gitignore
|
|
5
|
+
and adds pyrig-specific patterns.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from pyrig.dev.configs.base.base import TextConfigFile
|
|
12
|
+
from pyrig.dev.configs.pyproject import PyprojectConfigFile
|
|
13
|
+
from pyrig.main import main
|
|
14
|
+
from pyrig.src.project.mgt import PROJECT_MGT_RUN_ARGS
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ContainerfileConfigFile(TextConfigFile):
|
|
18
|
+
"""Configuration file manager for Containerfile.
|
|
19
|
+
|
|
20
|
+
Creates a Containerfile file in the project root. It is based on the
|
|
21
|
+
GitHub Containerfile template.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def get_filename(cls) -> str:
|
|
26
|
+
"""Get the Containerfile filename.
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
The string "Containerfile".
|
|
30
|
+
"""
|
|
31
|
+
return "Containerfile"
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def get_parent_path(cls) -> Path:
|
|
35
|
+
"""Get the project root directory.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Path to the project root.
|
|
39
|
+
"""
|
|
40
|
+
return Path()
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def get_file_extension(cls) -> str:
|
|
44
|
+
"""Get the Containerfile file extension.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The string "Containerfile".
|
|
48
|
+
"""
|
|
49
|
+
return ""
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def get_extension_sep(cls) -> str:
|
|
53
|
+
"""Get the Containerfile extension separator.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The string "".
|
|
57
|
+
"""
|
|
58
|
+
return ""
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def get_content_str(cls) -> str:
|
|
62
|
+
"""Get the Containerfile content.
|
|
63
|
+
|
|
64
|
+
Builds a standard working Containerfile from scratch.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The Containerfile content.
|
|
68
|
+
"""
|
|
69
|
+
return "\n\n".join(cls.get_layers())
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def is_correct(cls) -> bool:
|
|
73
|
+
"""Check if the Containerfile is valid.
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
True if each step in the Containerfile is present.
|
|
77
|
+
"""
|
|
78
|
+
all_layers_in_file = all(
|
|
79
|
+
layer in cls.get_file_content() for layer in cls.get_layers()
|
|
80
|
+
)
|
|
81
|
+
return super().is_correct() or all_layers_in_file
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def get_layers(cls) -> list[str]:
|
|
85
|
+
"""Get the layers of the Containerfile.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
List of strings with each layer.
|
|
89
|
+
"""
|
|
90
|
+
latest_python_version = PyprojectConfigFile.get_latest_possible_python_version()
|
|
91
|
+
project_name = PyprojectConfigFile.get_project_name()
|
|
92
|
+
package_name = PyprojectConfigFile.get_package_name()
|
|
93
|
+
app_user_name = "appuser"
|
|
94
|
+
entrypoint_args = [*PROJECT_MGT_RUN_ARGS, package_name]
|
|
95
|
+
default_cmd_args = [main.__name__]
|
|
96
|
+
return [
|
|
97
|
+
f"FROM python:{latest_python_version}-slim",
|
|
98
|
+
f"WORKDIR /{project_name}",
|
|
99
|
+
"COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv",
|
|
100
|
+
"COPY README.md LICENSE pyproject.toml uv.lock ./",
|
|
101
|
+
f"RUN useradd -m -u 1000 {app_user_name}",
|
|
102
|
+
f"RUN chown -R {app_user_name}:{app_user_name} .",
|
|
103
|
+
f"USER {app_user_name}",
|
|
104
|
+
f"COPY --chown=appuser:appuser {package_name} {package_name}",
|
|
105
|
+
"RUN uv sync --no-group dev",
|
|
106
|
+
"RUN rm README.md LICENSE pyproject.toml uv.lock",
|
|
107
|
+
f"ENTRYPOINT {json.dumps(entrypoint_args)}",
|
|
108
|
+
# if the image is provided a different command, it will run that instead
|
|
109
|
+
# so adding a default is convenient without restricting usage
|
|
110
|
+
f"CMD {json.dumps(default_cmd_args)}",
|
|
111
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Configuration management for .env environment files.
|
|
2
|
+
|
|
3
|
+
This module provides the DotEnvConfigFile class for managing .env files
|
|
4
|
+
in pyrig projects. The .env file is used for local environment variables
|
|
5
|
+
and is not committed to version control.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from dotenv import dotenv_values
|
|
12
|
+
|
|
13
|
+
from pyrig.dev.configs.base.base import ConfigFile
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DotEnvConfigFile(ConfigFile):
|
|
17
|
+
"""Configuration file manager for .env environment files.
|
|
18
|
+
|
|
19
|
+
Creates an empty .env file if it doesn't exist. The file is used
|
|
20
|
+
for local environment variables and should not be committed to
|
|
21
|
+
version control (it's included in .gitignore by default).
|
|
22
|
+
|
|
23
|
+
Note:
|
|
24
|
+
This config file is read-only from pyrig's perspective.
|
|
25
|
+
Users manage the content manually.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def load(cls) -> dict[str, str | None]:
|
|
30
|
+
"""Load environment variables from the .env file.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Dict mapping variable names to their values.
|
|
34
|
+
"""
|
|
35
|
+
return dotenv_values(cls.get_path())
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def dump(cls, config: dict[str, Any] | list[Any]) -> None:
|
|
39
|
+
"""Prevent writing to .env files.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
config: Must be empty.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ValueError: If config is not empty.
|
|
46
|
+
"""
|
|
47
|
+
# is not supposed to be dumped to, so just raise error
|
|
48
|
+
if config:
|
|
49
|
+
msg = f"Cannot dump {config} to .env file."
|
|
50
|
+
raise ValueError(msg)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def get_file_extension(cls) -> str:
|
|
54
|
+
"""Get the env file extension.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The string "env".
|
|
58
|
+
"""
|
|
59
|
+
return "env"
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def get_filename(cls) -> str:
|
|
63
|
+
"""Get an empty filename to produce ".env".
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Empty string so the path becomes ".env".
|
|
67
|
+
"""
|
|
68
|
+
return "" # so it builds the path .env and not env.env
|
|
69
|
+
|
|
70
|
+
@classmethod
|
|
71
|
+
def get_parent_path(cls) -> Path:
|
|
72
|
+
"""Get the project root directory.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Path to the project root.
|
|
76
|
+
"""
|
|
77
|
+
return Path()
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
def get_configs(cls) -> dict[str, Any]:
|
|
81
|
+
"""Get the expected configuration (empty).
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
An empty dict.
|
|
85
|
+
"""
|
|
86
|
+
return {}
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def is_correct(cls) -> bool:
|
|
90
|
+
"""Check if the .env file exists.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
True if the file exists or parent validation passes.
|
|
94
|
+
"""
|
|
95
|
+
return super().is_correct() or cls.get_path().exists()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Configuration management for .python-version files.
|
|
2
|
+
|
|
3
|
+
This module provides the DotPythonVersionConfigFile class for managing
|
|
4
|
+
the .python-version file used by pyenv and other Python version managers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from pyrig.dev.configs.base.base import ConfigFile
|
|
11
|
+
from pyrig.dev.configs.pyproject import PyprojectConfigFile
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DotPythonVersionConfigFile(ConfigFile):
|
|
15
|
+
"""Configuration file manager for .python-version.
|
|
16
|
+
|
|
17
|
+
Creates and maintains the .python-version file used by pyenv and
|
|
18
|
+
similar tools to specify the Python version for the project.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
VERSION_KEY: Dictionary key for the version string.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
VERSION_KEY = "version"
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def get_filename(cls) -> str:
|
|
28
|
+
"""Get an empty filename to produce ".python-version".
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Empty string so the path becomes ".python-version".
|
|
32
|
+
"""
|
|
33
|
+
return "" # so it builds the path .python-version
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def get_file_extension(cls) -> str:
|
|
37
|
+
"""Get the python-version file extension.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
The string "python-version".
|
|
41
|
+
"""
|
|
42
|
+
return "python-version"
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def get_parent_path(cls) -> Path:
|
|
46
|
+
"""Get the project root directory.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Path to the project root.
|
|
50
|
+
"""
|
|
51
|
+
return Path()
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def get_configs(cls) -> dict[str, Any]:
|
|
55
|
+
"""Get the expected Python version from pyproject.toml.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Dict with the first supported Python version.
|
|
59
|
+
"""
|
|
60
|
+
return {
|
|
61
|
+
cls.VERSION_KEY: str(
|
|
62
|
+
PyprojectConfigFile.get_first_supported_python_version()
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def load(cls) -> dict[str, Any]:
|
|
68
|
+
"""Load the Python version from the file.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Dict with the version string.
|
|
72
|
+
"""
|
|
73
|
+
return {cls.VERSION_KEY: cls.get_path().read_text(encoding="utf-8")}
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def dump(cls, config: dict[str, Any] | list[Any]) -> None:
|
|
77
|
+
"""Write the Python version to the file.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
config: Dict containing the version under VERSION_KEY.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
TypeError: If config is not a dict.
|
|
84
|
+
"""
|
|
85
|
+
if not isinstance(config, dict):
|
|
86
|
+
msg = f"Cannot dump {config} to .python-version file."
|
|
87
|
+
raise TypeError(msg)
|
|
88
|
+
cls.get_path().write_text(config[cls.VERSION_KEY], encoding="utf-8")
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Configuration management for .gitignore files.
|
|
2
|
+
|
|
3
|
+
This module provides the GitIgnoreConfigFile class for managing the
|
|
4
|
+
project's .gitignore file. It fetches GitHub's standard Python gitignore
|
|
5
|
+
and adds pyrig-specific patterns.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import pathspec
|
|
13
|
+
import requests
|
|
14
|
+
|
|
15
|
+
import pyrig
|
|
16
|
+
from pyrig.dev.configs.base.base import ConfigFile
|
|
17
|
+
from pyrig.dev.configs.dot_env import DotEnvConfigFile
|
|
18
|
+
from pyrig.dev.configs.python.dot_experiment import DotExperimentConfigFile
|
|
19
|
+
from pyrig.dev.utils.resources import return_resource_content_on_fetch_error
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GitIgnoreConfigFile(ConfigFile):
|
|
23
|
+
"""Configuration file manager for .gitignore.
|
|
24
|
+
|
|
25
|
+
Creates a comprehensive .gitignore file by combining:
|
|
26
|
+
- GitHub's standard Python.gitignore
|
|
27
|
+
- VS Code workspace files
|
|
28
|
+
- pyrig-specific patterns
|
|
29
|
+
- Common cache directories
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def get_filename(cls) -> str:
|
|
34
|
+
"""Get an empty filename to produce ".gitignore".
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Empty string so the path becomes ".gitignore".
|
|
38
|
+
"""
|
|
39
|
+
return "" # so it builds the path .gitignore and not gitignore.gitignore
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def get_parent_path(cls) -> Path:
|
|
43
|
+
"""Get the project root directory.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Path to the project root.
|
|
47
|
+
"""
|
|
48
|
+
return Path()
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def get_file_extension(cls) -> str:
|
|
52
|
+
"""Get the gitignore file extension.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
The string "gitignore".
|
|
56
|
+
"""
|
|
57
|
+
return "gitignore"
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def load(cls) -> list[str]:
|
|
61
|
+
"""Load the .gitignore file as a list of patterns.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
List of gitignore patterns, one per line.
|
|
65
|
+
"""
|
|
66
|
+
return cls.get_path().read_text(encoding="utf-8").splitlines()
|
|
67
|
+
|
|
68
|
+
@classmethod
|
|
69
|
+
def dump(cls, config: list[str] | dict[str, Any]) -> None:
|
|
70
|
+
"""Write patterns to the .gitignore file.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
config: List of gitignore patterns.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
TypeError: If config is not a list.
|
|
77
|
+
"""
|
|
78
|
+
if not isinstance(config, list):
|
|
79
|
+
msg = f"Cannot dump {config} to .gitignore file."
|
|
80
|
+
raise TypeError(msg)
|
|
81
|
+
cls.get_path().write_text("\n".join(config), encoding="utf-8")
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def get_configs(cls) -> list[str]:
|
|
85
|
+
"""Get the expected .gitignore patterns.
|
|
86
|
+
|
|
87
|
+
Combines GitHub's Python gitignore with pyrig-specific patterns.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
List of gitignore patterns.
|
|
91
|
+
"""
|
|
92
|
+
# fetch the standard github gitignore via https://github.com/github/gitignore/blob/main/Python.gitignore
|
|
93
|
+
needed = [
|
|
94
|
+
*cls.get_github_python_gitignore_as_list(),
|
|
95
|
+
"# vscode stuff",
|
|
96
|
+
".vscode/",
|
|
97
|
+
"",
|
|
98
|
+
f"# {pyrig.__name__} stuff",
|
|
99
|
+
".git/",
|
|
100
|
+
DotExperimentConfigFile.get_path().as_posix(),
|
|
101
|
+
"# others",
|
|
102
|
+
DotEnvConfigFile.get_path().as_posix(),
|
|
103
|
+
".coverage",
|
|
104
|
+
"coverage.xml",
|
|
105
|
+
".mypy_cache/",
|
|
106
|
+
".pytest_cache/",
|
|
107
|
+
".ruff_cache/",
|
|
108
|
+
".venv/",
|
|
109
|
+
"dist/",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
dotenv_path = DotEnvConfigFile.get_path().as_posix()
|
|
113
|
+
if dotenv_path not in needed:
|
|
114
|
+
needed.extend(["# for secrets used locally", dotenv_path])
|
|
115
|
+
|
|
116
|
+
existing = cls.load()
|
|
117
|
+
needed = [p for p in needed if p not in set(existing)]
|
|
118
|
+
return existing + needed
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
@return_resource_content_on_fetch_error(resource_name="GITIGNORE")
|
|
122
|
+
def get_github_python_gitignore_as_str(cls) -> str:
|
|
123
|
+
"""Fetch GitHub's standard Python gitignore patterns.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
String of patterns from GitHub's Python.gitignore.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
RuntimeError: If fetch fails and no .gitignore exists.
|
|
130
|
+
"""
|
|
131
|
+
url = "https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore"
|
|
132
|
+
res = requests.get(url, timeout=10)
|
|
133
|
+
res.raise_for_status()
|
|
134
|
+
return res.text
|
|
135
|
+
|
|
136
|
+
@classmethod
|
|
137
|
+
def get_github_python_gitignore_as_list(cls) -> list[str]:
|
|
138
|
+
"""Fetch GitHub's standard Python gitignore patterns.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
List of patterns from GitHub's Python.gitignore.
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
RuntimeError: If fetch fails and no .gitignore exists.
|
|
145
|
+
"""
|
|
146
|
+
gitignore_str = cls.get_github_python_gitignore_as_str()
|
|
147
|
+
return gitignore_str.splitlines()
|
|
148
|
+
|
|
149
|
+
@classmethod
|
|
150
|
+
def path_is_in_gitignore(cls, relative_path: str | Path) -> bool:
|
|
151
|
+
"""Check if a path matches any pattern in .gitignore.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
relative_path: Path to check, relative to repository root.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
True if the path matches any gitignore pattern.
|
|
158
|
+
"""
|
|
159
|
+
gitignore_path = cls.get_path()
|
|
160
|
+
if not gitignore_path.exists():
|
|
161
|
+
return False
|
|
162
|
+
as_path = Path(relative_path)
|
|
163
|
+
if as_path.is_absolute():
|
|
164
|
+
as_path = as_path.relative_to(Path.cwd())
|
|
165
|
+
is_dir = (
|
|
166
|
+
bool(as_path.suffix == "")
|
|
167
|
+
or as_path.is_dir()
|
|
168
|
+
or str(as_path).endswith(os.sep)
|
|
169
|
+
)
|
|
170
|
+
is_dir = is_dir and not as_path.is_file()
|
|
171
|
+
|
|
172
|
+
as_posix = as_path.as_posix()
|
|
173
|
+
if is_dir and not as_posix.endswith("/"):
|
|
174
|
+
as_posix += "/"
|
|
175
|
+
|
|
176
|
+
spec = pathspec.PathSpec.from_lines(
|
|
177
|
+
"gitwildmatch",
|
|
178
|
+
cls.load(),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return spec.match_file(as_posix)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Configuration management for pre-commit hooks.
|
|
2
|
+
|
|
3
|
+
This module provides the PreCommitConfigConfigFile class for managing
|
|
4
|
+
the .pre-commit-config.yaml file. It configures local hooks for linting,
|
|
5
|
+
formatting, type checking, and security scanning.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from subprocess import CompletedProcess # nosec: B404
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pyrig.dev.configs.base.base import YamlConfigFile
|
|
14
|
+
from pyrig.src.git.git import git_add_file
|
|
15
|
+
from pyrig.src.os.os import run_subprocess
|
|
16
|
+
from pyrig.src.project.mgt import (
|
|
17
|
+
PROJECT_MGT_RUN_ARGS,
|
|
18
|
+
get_script_from_args,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PreCommitConfigConfigFile(YamlConfigFile):
|
|
25
|
+
"""Configuration file manager for .pre-commit-config.yaml.
|
|
26
|
+
|
|
27
|
+
Configures local pre-commit hooks for:
|
|
28
|
+
- ruff linting and formatting
|
|
29
|
+
- mypy type checking
|
|
30
|
+
- bandit security scanning
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def get_filename(cls) -> str:
|
|
35
|
+
"""Get the pre-commit config filename.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
The string ".pre-commit-config".
|
|
39
|
+
"""
|
|
40
|
+
filename = super().get_filename()
|
|
41
|
+
return f".{filename.replace('_', '-')}"
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def get_parent_path(cls) -> Path:
|
|
45
|
+
"""Get the project root directory.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Path to the project root.
|
|
49
|
+
"""
|
|
50
|
+
return Path()
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def get_hook(
|
|
54
|
+
cls,
|
|
55
|
+
name: str,
|
|
56
|
+
args: list[str],
|
|
57
|
+
*,
|
|
58
|
+
language: str = "system",
|
|
59
|
+
pass_filenames: bool = False,
|
|
60
|
+
always_run: bool = True,
|
|
61
|
+
**kwargs: Any,
|
|
62
|
+
) -> dict[str, Any]:
|
|
63
|
+
"""Create a pre-commit hook configuration.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
name: Hook identifier and display name.
|
|
67
|
+
args: Command arguments for the hook.
|
|
68
|
+
language: Hook language (default: "system").
|
|
69
|
+
pass_filenames: Whether to pass filenames to the hook.
|
|
70
|
+
always_run: Whether to run on every commit.
|
|
71
|
+
**kwargs: Additional hook configuration options.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Hook configuration dict.
|
|
75
|
+
"""
|
|
76
|
+
hook: dict[str, Any] = {
|
|
77
|
+
"id": name,
|
|
78
|
+
"name": name,
|
|
79
|
+
"entry": get_script_from_args(args),
|
|
80
|
+
"language": language,
|
|
81
|
+
"always_run": always_run,
|
|
82
|
+
"pass_filenames": pass_filenames,
|
|
83
|
+
**kwargs,
|
|
84
|
+
}
|
|
85
|
+
return hook
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def get_configs(cls) -> dict[str, Any]:
|
|
89
|
+
"""Get the expected pre-commit configuration.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Configuration dict with local hooks for linting,
|
|
93
|
+
formatting, type checking, and security scanning.
|
|
94
|
+
"""
|
|
95
|
+
hooks: list[dict[str, Any]] = [
|
|
96
|
+
cls.get_hook(
|
|
97
|
+
"lint-code",
|
|
98
|
+
["ruff", "check", "--fix"],
|
|
99
|
+
),
|
|
100
|
+
cls.get_hook(
|
|
101
|
+
"format-code",
|
|
102
|
+
["ruff", "format"],
|
|
103
|
+
),
|
|
104
|
+
cls.get_hook(
|
|
105
|
+
"check-types",
|
|
106
|
+
["ty", "check"],
|
|
107
|
+
),
|
|
108
|
+
cls.get_hook(
|
|
109
|
+
"check-static-types",
|
|
110
|
+
["mypy", "--exclude-gitignore"],
|
|
111
|
+
),
|
|
112
|
+
cls.get_hook(
|
|
113
|
+
"check-security",
|
|
114
|
+
["bandit", "-c", "pyproject.toml", "-r", "."],
|
|
115
|
+
),
|
|
116
|
+
]
|
|
117
|
+
return {
|
|
118
|
+
"repos": [
|
|
119
|
+
{
|
|
120
|
+
"repo": "local",
|
|
121
|
+
"hooks": hooks,
|
|
122
|
+
},
|
|
123
|
+
]
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
def __init__(self) -> None:
|
|
127
|
+
"""Initialize the pre-commit config file manager."""
|
|
128
|
+
super().__init__()
|
|
129
|
+
|
|
130
|
+
@classmethod
|
|
131
|
+
def install(cls) -> CompletedProcess[bytes]:
|
|
132
|
+
"""Install pre-commit hooks into the git repository.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
The completed process result.
|
|
136
|
+
"""
|
|
137
|
+
logger.info("Running pre-commit install")
|
|
138
|
+
return run_subprocess([*PROJECT_MGT_RUN_ARGS, "pre-commit", "install"])
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def run_hooks(
|
|
142
|
+
cls,
|
|
143
|
+
*,
|
|
144
|
+
with_install: bool = True,
|
|
145
|
+
all_files: bool = True,
|
|
146
|
+
add_before_commit: bool = False,
|
|
147
|
+
verbose: bool = True,
|
|
148
|
+
check: bool = True,
|
|
149
|
+
) -> None:
|
|
150
|
+
"""Run all pre-commit hooks.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
with_install: Whether to install hooks first.
|
|
154
|
+
all_files: Whether to run on all files.
|
|
155
|
+
add_before_commit: Whether to git add files first.
|
|
156
|
+
verbose: Whether to show verbose output.
|
|
157
|
+
check: Whether to raise on hook failure.
|
|
158
|
+
"""
|
|
159
|
+
if add_before_commit:
|
|
160
|
+
logger.info("Adding all files to git")
|
|
161
|
+
git_add_file(Path())
|
|
162
|
+
if with_install:
|
|
163
|
+
cls.install()
|
|
164
|
+
logger.info("Running pre-commit run")
|
|
165
|
+
args = ["pre-commit", "run"]
|
|
166
|
+
if all_files:
|
|
167
|
+
args.append("--all-files")
|
|
168
|
+
if verbose:
|
|
169
|
+
args.append("--verbose")
|
|
170
|
+
run_subprocess([*args], check=check)
|