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 @@
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,5 @@
1
+ """Git-related configuration file management.
2
+
3
+ This package provides ConfigFile subclasses for managing .gitignore
4
+ and .pre-commit-config.yaml files.
5
+ """
@@ -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)