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,826 @@
|
|
|
1
|
+
"""Abstract base classes for configuration file management.
|
|
2
|
+
|
|
3
|
+
This module provides the ConfigFile abstract base class and format-specific
|
|
4
|
+
subclasses for managing project configuration files. The system supports:
|
|
5
|
+
|
|
6
|
+
- Automatic discovery of ConfigFile subclasses across dependent packages
|
|
7
|
+
- Subset validation (configs can extend but not contradict base configs)
|
|
8
|
+
- Intelligent merging of missing configuration values
|
|
9
|
+
- Multiple file formats (YAML, TOML, Python, plain text)
|
|
10
|
+
|
|
11
|
+
The ConfigFile system is the heart of pyrig's automation. When you run
|
|
12
|
+
``pyrig init`` or ``pyrig create-root``, all ConfigFile subclasses are
|
|
13
|
+
discovered and initialized, creating the complete project configuration.
|
|
14
|
+
|
|
15
|
+
Subclasses must implement:
|
|
16
|
+
- ``get_parent_path``: Directory containing the config file
|
|
17
|
+
- ``get_file_extension``: File extension (yaml, toml, py, etc.)
|
|
18
|
+
- ``get_configs``: Return the expected configuration structure
|
|
19
|
+
- ``load``: Load configuration from disk
|
|
20
|
+
- ``dump``: Write configuration to disk
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
class MyConfigFile(YamlConfigFile):
|
|
24
|
+
@classmethod
|
|
25
|
+
def get_parent_path(cls) -> Path:
|
|
26
|
+
return Path(".")
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def get_configs(cls) -> dict[str, Any]:
|
|
30
|
+
return {"key": "value"}
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
import inspect
|
|
34
|
+
from abc import ABC, abstractmethod
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from types import ModuleType
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
import tomlkit
|
|
40
|
+
import yaml
|
|
41
|
+
|
|
42
|
+
import pyrig
|
|
43
|
+
from pyrig.dev import configs
|
|
44
|
+
from pyrig.src.iterate import nested_structure_is_subset
|
|
45
|
+
from pyrig.src.modules.class_ import (
|
|
46
|
+
get_all_nonabst_subcls_from_mod_in_all_deps_depen_on_dep,
|
|
47
|
+
)
|
|
48
|
+
from pyrig.src.modules.module import (
|
|
49
|
+
get_isolated_obj_name,
|
|
50
|
+
get_module_content_as_str,
|
|
51
|
+
get_module_name_replacing_start_module,
|
|
52
|
+
make_pkg_dir,
|
|
53
|
+
to_path,
|
|
54
|
+
)
|
|
55
|
+
from pyrig.src.string import split_on_uppercase
|
|
56
|
+
from pyrig.src.testing.convention import TESTS_PACKAGE_NAME
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class ConfigFile(ABC):
|
|
60
|
+
"""Abstract base class for configuration file management.
|
|
61
|
+
|
|
62
|
+
Provides automatic creation, validation, and updating of configuration
|
|
63
|
+
files. Subclasses define the file format, location, and expected content.
|
|
64
|
+
|
|
65
|
+
The initialization process:
|
|
66
|
+
1. Creates parent directories if needed
|
|
67
|
+
2. Creates the file with default content if it doesn't exist
|
|
68
|
+
3. Validates existing content against expected configuration
|
|
69
|
+
4. Adds any missing configuration values
|
|
70
|
+
|
|
71
|
+
Subclasses must implement:
|
|
72
|
+
- ``get_parent_path``: Return the directory for the config file
|
|
73
|
+
- ``get_file_extension``: Return the file extension
|
|
74
|
+
- ``get_configs``: Return the expected configuration structure
|
|
75
|
+
- ``load``: Load and parse the configuration file
|
|
76
|
+
- ``dump``: Write configuration to the file
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
@classmethod
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def get_parent_path(cls) -> Path:
|
|
82
|
+
"""Get the directory containing the config file.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Path to the parent directory.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
@abstractmethod
|
|
90
|
+
def load(cls) -> dict[str, Any] | list[Any]:
|
|
91
|
+
"""Load and parse the configuration file.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
The parsed configuration as a dict or list.
|
|
95
|
+
"""
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
@abstractmethod
|
|
99
|
+
def dump(cls, config: dict[str, Any] | list[Any]) -> None:
|
|
100
|
+
"""Write configuration to the file.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
config: The configuration to write.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
@classmethod
|
|
107
|
+
@abstractmethod
|
|
108
|
+
def get_file_extension(cls) -> str:
|
|
109
|
+
"""Get the file extension for this config file.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
The file extension without the leading dot.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def get_configs(cls) -> dict[str, Any] | list[Any]:
|
|
118
|
+
"""Get the expected configuration structure.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
The configuration that should be present in the file.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(self) -> None:
|
|
125
|
+
"""Initialize the config file, creating or updating as needed.
|
|
126
|
+
|
|
127
|
+
Raises:
|
|
128
|
+
ValueError: If the config file cannot be made correct.
|
|
129
|
+
"""
|
|
130
|
+
self.get_path().parent.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
if not self.get_path().exists():
|
|
132
|
+
self.get_path().touch()
|
|
133
|
+
self.dump(self.get_configs())
|
|
134
|
+
|
|
135
|
+
if not self.is_correct():
|
|
136
|
+
config = self.add_missing_configs()
|
|
137
|
+
self.dump(config)
|
|
138
|
+
|
|
139
|
+
if not self.is_correct():
|
|
140
|
+
msg = f"Config file {self.get_path()} is not correct."
|
|
141
|
+
raise ValueError(msg)
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def get_path(cls) -> Path:
|
|
145
|
+
"""Get the full path to the config file.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
Complete path including filename and extension.
|
|
149
|
+
"""
|
|
150
|
+
return cls.get_parent_path() / (
|
|
151
|
+
cls.get_filename() + cls.get_extension_sep() + cls.get_file_extension()
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@classmethod
|
|
155
|
+
def get_extension_sep(cls) -> str:
|
|
156
|
+
"""Get the extension separator.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
The string ".".
|
|
160
|
+
"""
|
|
161
|
+
return "."
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def get_filename(cls) -> str:
|
|
165
|
+
"""Derive the filename from the class name.
|
|
166
|
+
|
|
167
|
+
Removes abstract parent class suffixes and converts to snake_case.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
The filename without extension.
|
|
171
|
+
"""
|
|
172
|
+
name = cls.__name__
|
|
173
|
+
abstract_parents = [
|
|
174
|
+
parent.__name__ for parent in cls.__mro__ if inspect.isabstract(parent)
|
|
175
|
+
]
|
|
176
|
+
for parent in abstract_parents:
|
|
177
|
+
name = name.removesuffix(parent)
|
|
178
|
+
return "_".join(split_on_uppercase(name)).lower()
|
|
179
|
+
|
|
180
|
+
@classmethod
|
|
181
|
+
def add_missing_configs(cls) -> dict[str, Any] | list[Any]:
|
|
182
|
+
"""Merge expected configuration into the current file.
|
|
183
|
+
|
|
184
|
+
Adds any missing keys or values from the expected configuration
|
|
185
|
+
to the current configuration without overwriting existing values.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
The merged configuration.
|
|
189
|
+
"""
|
|
190
|
+
current_config = cls.load()
|
|
191
|
+
expected_config = cls.get_configs()
|
|
192
|
+
nested_structure_is_subset(
|
|
193
|
+
expected_config,
|
|
194
|
+
current_config,
|
|
195
|
+
cls.add_missing_dict_val,
|
|
196
|
+
cls.insert_missing_list_val,
|
|
197
|
+
)
|
|
198
|
+
return current_config
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def add_missing_dict_val(
|
|
202
|
+
expected_dict: dict[str, Any], actual_dict: dict[str, Any], key: str
|
|
203
|
+
) -> None:
|
|
204
|
+
"""Add a missing dictionary value during config merging.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
expected_dict: The expected configuration dictionary.
|
|
208
|
+
actual_dict: The actual configuration dictionary to update.
|
|
209
|
+
key: The key to add or update.
|
|
210
|
+
"""
|
|
211
|
+
expected_val = expected_dict[key]
|
|
212
|
+
actual_val = actual_dict.get(key)
|
|
213
|
+
actual_dict.setdefault(key, expected_val)
|
|
214
|
+
|
|
215
|
+
if isinstance(expected_val, dict) and isinstance(actual_val, dict):
|
|
216
|
+
actual_val.update(expected_val)
|
|
217
|
+
else:
|
|
218
|
+
actual_dict[key] = expected_val
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def insert_missing_list_val(
|
|
222
|
+
expected_list: list[Any], actual_list: list[Any], index: int
|
|
223
|
+
) -> None:
|
|
224
|
+
"""Insert a missing list value during config merging.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
expected_list: The expected list.
|
|
228
|
+
actual_list: The actual list to update.
|
|
229
|
+
index: The index at which to insert.
|
|
230
|
+
"""
|
|
231
|
+
actual_list.insert(index, expected_list[index])
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def is_correct(cls) -> bool:
|
|
235
|
+
"""Check if the configuration file is valid.
|
|
236
|
+
|
|
237
|
+
A file is considered correct if:
|
|
238
|
+
- It is empty (user opted out of this config)
|
|
239
|
+
- Its content is a superset of the expected configuration
|
|
240
|
+
|
|
241
|
+
Returns:
|
|
242
|
+
True if the configuration is valid.
|
|
243
|
+
"""
|
|
244
|
+
return cls.is_unwanted() or cls.is_correct_recursively(
|
|
245
|
+
cls.get_configs(), cls.load()
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
@classmethod
|
|
249
|
+
def is_unwanted(cls) -> bool:
|
|
250
|
+
"""Check if the user has opted out of this config file.
|
|
251
|
+
|
|
252
|
+
An empty file indicates the user doesn't want this configuration.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
True if the file exists and is empty.
|
|
256
|
+
"""
|
|
257
|
+
return (
|
|
258
|
+
cls.get_path().exists() and cls.get_path().read_text(encoding="utf-8") == ""
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
@staticmethod
|
|
262
|
+
def is_correct_recursively(
|
|
263
|
+
expected_config: dict[str, Any] | list[Any],
|
|
264
|
+
actual_config: dict[str, Any] | list[Any],
|
|
265
|
+
) -> bool:
|
|
266
|
+
"""Recursively check if expected config is a subset of actual.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
expected_config: The expected configuration structure.
|
|
270
|
+
actual_config: The actual configuration to validate.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
True if expected is a subset of actual.
|
|
274
|
+
"""
|
|
275
|
+
return nested_structure_is_subset(expected_config, actual_config)
|
|
276
|
+
|
|
277
|
+
@classmethod
|
|
278
|
+
def get_all_subclasses(cls) -> list[type["ConfigFile"]]:
|
|
279
|
+
"""Discover all non-abstract ConfigFile subclasses.
|
|
280
|
+
|
|
281
|
+
Searches all packages depending on pyrig for ConfigFile subclasses.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
List of ConfigFile subclass types.
|
|
285
|
+
"""
|
|
286
|
+
return get_all_nonabst_subcls_from_mod_in_all_deps_depen_on_dep(
|
|
287
|
+
cls,
|
|
288
|
+
pyrig,
|
|
289
|
+
configs,
|
|
290
|
+
discard_parents=True,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
@classmethod
|
|
294
|
+
def init_config_files(cls) -> None:
|
|
295
|
+
"""Initialize all discovered ConfigFile subclasses.
|
|
296
|
+
|
|
297
|
+
Initializes files in order: priority files first, then ordered
|
|
298
|
+
files, then all remaining files.
|
|
299
|
+
"""
|
|
300
|
+
cls.init_priority_config_files()
|
|
301
|
+
cls.init_ordered_config_files()
|
|
302
|
+
|
|
303
|
+
already_inited: set[type[ConfigFile]] = set(
|
|
304
|
+
cls.get_priority_config_files() + cls.get_ordered_config_files()
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
subclasses = cls.get_all_subclasses()
|
|
308
|
+
subclasses = [
|
|
309
|
+
subclass for subclass in subclasses if subclass not in already_inited
|
|
310
|
+
]
|
|
311
|
+
for subclass in subclasses:
|
|
312
|
+
subclass()
|
|
313
|
+
|
|
314
|
+
@classmethod
|
|
315
|
+
def get_ordered_config_files(cls) -> list[type["ConfigFile"]]:
|
|
316
|
+
"""Get config files that must be initialized in a specific order.
|
|
317
|
+
|
|
318
|
+
These files have dependencies on each other and must be
|
|
319
|
+
initialized after priority files but before general files.
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
List of ConfigFile types in initialization order.
|
|
323
|
+
"""
|
|
324
|
+
from pyrig.dev.configs.testing.conftest import ( # noqa: PLC0415
|
|
325
|
+
ConftestConfigFile,
|
|
326
|
+
)
|
|
327
|
+
from pyrig.dev.configs.testing.fixtures_init import ( # noqa: PLC0415
|
|
328
|
+
FixturesInitConfigFile,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
return [
|
|
332
|
+
FixturesInitConfigFile,
|
|
333
|
+
ConftestConfigFile,
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
@classmethod
|
|
337
|
+
def init_ordered_config_files(cls) -> None:
|
|
338
|
+
"""Initialize config files that require specific ordering."""
|
|
339
|
+
for subclass in cls.get_ordered_config_files():
|
|
340
|
+
subclass()
|
|
341
|
+
|
|
342
|
+
@classmethod
|
|
343
|
+
def init_priority_config_files(cls) -> None:
|
|
344
|
+
"""Initialize high-priority config files first."""
|
|
345
|
+
for subclass in cls.get_priority_config_files():
|
|
346
|
+
subclass()
|
|
347
|
+
|
|
348
|
+
@classmethod
|
|
349
|
+
def get_priority_config_files(cls) -> list[type["ConfigFile"]]:
|
|
350
|
+
"""Get config files that must be initialized first.
|
|
351
|
+
|
|
352
|
+
These files are required by other config files or the build
|
|
353
|
+
process and must exist before other initialization can proceed.
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
List of ConfigFile types in priority order.
|
|
357
|
+
"""
|
|
358
|
+
# Some must be first:
|
|
359
|
+
from pyrig.dev.configs.git.gitignore import ( # noqa: PLC0415
|
|
360
|
+
GitIgnoreConfigFile,
|
|
361
|
+
)
|
|
362
|
+
from pyrig.dev.configs.licence import ( # noqa: PLC0415
|
|
363
|
+
LicenceConfigFile,
|
|
364
|
+
)
|
|
365
|
+
from pyrig.dev.configs.pyproject import ( # noqa: PLC0415
|
|
366
|
+
PyprojectConfigFile,
|
|
367
|
+
)
|
|
368
|
+
from pyrig.dev.configs.python.builders_init import ( # noqa: PLC0415
|
|
369
|
+
BuildersInitConfigFile,
|
|
370
|
+
)
|
|
371
|
+
from pyrig.dev.configs.python.configs_init import ( # noqa: PLC0415
|
|
372
|
+
ConfigsInitConfigFile,
|
|
373
|
+
)
|
|
374
|
+
from pyrig.dev.configs.python.main import ( # noqa: PLC0415
|
|
375
|
+
MainConfigFile,
|
|
376
|
+
)
|
|
377
|
+
from pyrig.dev.configs.testing.zero_test import ( # noqa: PLC0415
|
|
378
|
+
ZeroTestConfigFile,
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
return [
|
|
382
|
+
GitIgnoreConfigFile,
|
|
383
|
+
PyprojectConfigFile,
|
|
384
|
+
LicenceConfigFile,
|
|
385
|
+
MainConfigFile,
|
|
386
|
+
ConfigsInitConfigFile,
|
|
387
|
+
BuildersInitConfigFile,
|
|
388
|
+
ZeroTestConfigFile,
|
|
389
|
+
]
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
class YamlConfigFile(ConfigFile):
|
|
393
|
+
"""Abstract base class for YAML configuration files.
|
|
394
|
+
|
|
395
|
+
Provides YAML-specific load and dump implementations using PyYAML.
|
|
396
|
+
"""
|
|
397
|
+
|
|
398
|
+
@classmethod
|
|
399
|
+
def load(cls) -> dict[str, Any] | list[Any]:
|
|
400
|
+
"""Load and parse the YAML configuration file.
|
|
401
|
+
|
|
402
|
+
Returns:
|
|
403
|
+
The parsed YAML content as a dict or list.
|
|
404
|
+
"""
|
|
405
|
+
return yaml.safe_load(cls.get_path().read_text(encoding="utf-8")) or {}
|
|
406
|
+
|
|
407
|
+
@classmethod
|
|
408
|
+
def dump(cls, config: dict[str, Any] | list[Any]) -> None:
|
|
409
|
+
"""Write configuration to the YAML file.
|
|
410
|
+
|
|
411
|
+
Args:
|
|
412
|
+
config: The configuration to write.
|
|
413
|
+
"""
|
|
414
|
+
with cls.get_path().open("w") as f:
|
|
415
|
+
yaml.safe_dump(config, f, sort_keys=False)
|
|
416
|
+
|
|
417
|
+
@classmethod
|
|
418
|
+
def get_file_extension(cls) -> str:
|
|
419
|
+
"""Get the YAML file extension.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
The string "yaml".
|
|
423
|
+
"""
|
|
424
|
+
return "yaml"
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
class TomlConfigFile(ConfigFile):
|
|
428
|
+
"""Abstract base class for TOML configuration files.
|
|
429
|
+
|
|
430
|
+
Provides TOML-specific load and dump implementations using tomlkit,
|
|
431
|
+
which preserves formatting and comments.
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
@classmethod
|
|
435
|
+
def load(cls) -> dict[str, Any]:
|
|
436
|
+
"""Load and parse the TOML configuration file.
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
The parsed TOML content as a dict.
|
|
440
|
+
"""
|
|
441
|
+
return tomlkit.parse(cls.get_path().read_text(encoding="utf-8"))
|
|
442
|
+
|
|
443
|
+
@classmethod
|
|
444
|
+
def dump(cls, config: dict[str, Any] | list[Any]) -> None:
|
|
445
|
+
"""Write configuration to the TOML file.
|
|
446
|
+
|
|
447
|
+
Args:
|
|
448
|
+
config: The configuration dict to write.
|
|
449
|
+
|
|
450
|
+
Raises:
|
|
451
|
+
TypeError: If config is not a dict.
|
|
452
|
+
"""
|
|
453
|
+
if not isinstance(config, dict):
|
|
454
|
+
msg = f"Cannot dump {config} to toml file."
|
|
455
|
+
raise TypeError(msg)
|
|
456
|
+
cls.pretty_dump(config)
|
|
457
|
+
|
|
458
|
+
@classmethod
|
|
459
|
+
def prettify_dict(cls, config: dict[str, Any]) -> dict[str, Any]:
|
|
460
|
+
"""Convert a dict to a tomlkit table with multiline arrays.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
config: The configuration dict to prettify.
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
A tomlkit table with formatted arrays.
|
|
467
|
+
"""
|
|
468
|
+
t = tomlkit.table()
|
|
469
|
+
|
|
470
|
+
for key, value in config.items():
|
|
471
|
+
if isinstance(value, list):
|
|
472
|
+
# Check if all items are dicts - use inline tables for those
|
|
473
|
+
if value and all(isinstance(item, dict) for item in value):
|
|
474
|
+
arr = tomlkit.array().multiline(multiline=True)
|
|
475
|
+
for item in value:
|
|
476
|
+
inline_table = tomlkit.inline_table()
|
|
477
|
+
inline_table.update(item)
|
|
478
|
+
arr.append(inline_table)
|
|
479
|
+
t.add(key, arr)
|
|
480
|
+
else:
|
|
481
|
+
# For non-dict items, use multiline arrays
|
|
482
|
+
arr = tomlkit.array().multiline(multiline=True)
|
|
483
|
+
for item in value:
|
|
484
|
+
arr.append(item)
|
|
485
|
+
t.add(key, arr)
|
|
486
|
+
|
|
487
|
+
elif isinstance(value, dict):
|
|
488
|
+
t.add(key, cls.prettify_dict(value))
|
|
489
|
+
|
|
490
|
+
else:
|
|
491
|
+
t.add(key, value)
|
|
492
|
+
|
|
493
|
+
return t
|
|
494
|
+
|
|
495
|
+
@classmethod
|
|
496
|
+
def pretty_dump(cls, config: dict[str, Any]) -> None:
|
|
497
|
+
"""Write configuration to TOML with pretty formatting.
|
|
498
|
+
|
|
499
|
+
Converts lists to multiline arrays for readability.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
config: The configuration dict to write.
|
|
503
|
+
"""
|
|
504
|
+
# trun all lists into multiline arrays
|
|
505
|
+
config = cls.prettify_dict(config)
|
|
506
|
+
with cls.get_path().open("w") as f:
|
|
507
|
+
tomlkit.dump(config, f, sort_keys=False)
|
|
508
|
+
|
|
509
|
+
@classmethod
|
|
510
|
+
def get_file_extension(cls) -> str:
|
|
511
|
+
"""Get the TOML file extension.
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
The string "toml".
|
|
515
|
+
"""
|
|
516
|
+
return "toml"
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
class TextConfigFile(ConfigFile):
|
|
520
|
+
"""Abstract base class for plain text configuration files.
|
|
521
|
+
|
|
522
|
+
Suitable for files that have a required starting content but can
|
|
523
|
+
be extended by the user (e.g., Python files, README.md).
|
|
524
|
+
|
|
525
|
+
Attributes:
|
|
526
|
+
CONTENT_KEY: Dictionary key used to store file content.
|
|
527
|
+
"""
|
|
528
|
+
|
|
529
|
+
CONTENT_KEY = "content"
|
|
530
|
+
|
|
531
|
+
@classmethod
|
|
532
|
+
@abstractmethod
|
|
533
|
+
def get_content_str(cls) -> str:
|
|
534
|
+
"""Get the required content for this file.
|
|
535
|
+
|
|
536
|
+
Returns:
|
|
537
|
+
The content string that must be present in the file.
|
|
538
|
+
"""
|
|
539
|
+
|
|
540
|
+
@classmethod
|
|
541
|
+
def load(cls) -> dict[str, str]:
|
|
542
|
+
"""Load the text file content.
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
Dict with the file content under CONTENT_KEY.
|
|
546
|
+
"""
|
|
547
|
+
return {cls.CONTENT_KEY: cls.get_path().read_text(encoding="utf-8")}
|
|
548
|
+
|
|
549
|
+
@classmethod
|
|
550
|
+
def dump(cls, config: dict[str, Any] | list[Any]) -> None:
|
|
551
|
+
"""Write content to the text file.
|
|
552
|
+
|
|
553
|
+
Appends existing file content to preserve user additions.
|
|
554
|
+
|
|
555
|
+
Args:
|
|
556
|
+
config: Dict containing the content to write.
|
|
557
|
+
|
|
558
|
+
Raises:
|
|
559
|
+
TypeError: If config is not a dict.
|
|
560
|
+
"""
|
|
561
|
+
if not isinstance(config, dict):
|
|
562
|
+
msg = f"Cannot dump {config} to text file."
|
|
563
|
+
raise TypeError(msg)
|
|
564
|
+
if cls.get_file_content().strip():
|
|
565
|
+
config[cls.CONTENT_KEY] = (
|
|
566
|
+
config[cls.CONTENT_KEY] + "\n" + cls.get_file_content()
|
|
567
|
+
)
|
|
568
|
+
cls.get_path().write_text(config[cls.CONTENT_KEY], encoding="utf-8")
|
|
569
|
+
|
|
570
|
+
@classmethod
|
|
571
|
+
def get_configs(cls) -> dict[str, Any]:
|
|
572
|
+
"""Get the expected configuration structure.
|
|
573
|
+
|
|
574
|
+
Returns:
|
|
575
|
+
Dict with the required content under CONTENT_KEY.
|
|
576
|
+
"""
|
|
577
|
+
return {cls.CONTENT_KEY: cls.get_content_str()}
|
|
578
|
+
|
|
579
|
+
@classmethod
|
|
580
|
+
def is_correct(cls) -> bool:
|
|
581
|
+
"""Check if the text file contains the required content.
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
True if the required content is present in the file.
|
|
585
|
+
"""
|
|
586
|
+
return (
|
|
587
|
+
super().is_correct()
|
|
588
|
+
or cls.get_content_str().strip() in cls.load()[cls.CONTENT_KEY]
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
@classmethod
|
|
592
|
+
def get_file_content(cls) -> str:
|
|
593
|
+
"""Get the current file content.
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
The full content of the file.
|
|
597
|
+
"""
|
|
598
|
+
return cls.load()[cls.CONTENT_KEY]
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
class MarkdownConfigFile(TextConfigFile):
|
|
602
|
+
"""Abstract base class for Markdown configuration files.
|
|
603
|
+
|
|
604
|
+
Attributes:
|
|
605
|
+
CONTENT_KEY: Dictionary key used to store file content.
|
|
606
|
+
"""
|
|
607
|
+
|
|
608
|
+
@classmethod
|
|
609
|
+
def get_file_extension(cls) -> str:
|
|
610
|
+
"""Get the Markdown file extension.
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
The string "md".
|
|
614
|
+
"""
|
|
615
|
+
return "md"
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
class PythonConfigFile(TextConfigFile):
|
|
619
|
+
"""Abstract base class for Python source file configuration.
|
|
620
|
+
|
|
621
|
+
Attributes:
|
|
622
|
+
CONTENT_KEY: Dictionary key used to store file content.
|
|
623
|
+
"""
|
|
624
|
+
|
|
625
|
+
CONTENT_KEY = "content"
|
|
626
|
+
|
|
627
|
+
@classmethod
|
|
628
|
+
def get_file_extension(cls) -> str:
|
|
629
|
+
"""Get the Python file extension.
|
|
630
|
+
|
|
631
|
+
Returns:
|
|
632
|
+
The string "py".
|
|
633
|
+
"""
|
|
634
|
+
return "py"
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
class PythonPackageConfigFile(PythonConfigFile):
|
|
638
|
+
"""Abstract base class for Python package configuration files.
|
|
639
|
+
|
|
640
|
+
Creates __init__.py files and ensures the parent directory is a
|
|
641
|
+
valid Python package.
|
|
642
|
+
"""
|
|
643
|
+
|
|
644
|
+
@classmethod
|
|
645
|
+
def dump(cls, config: dict[str, Any] | list[Any]) -> None:
|
|
646
|
+
"""Write the config file and ensure parent is a package.
|
|
647
|
+
|
|
648
|
+
Args:
|
|
649
|
+
config: The configuration to write.
|
|
650
|
+
"""
|
|
651
|
+
super().dump(config)
|
|
652
|
+
make_pkg_dir(cls.get_path().parent)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
class CopyModuleConfigFile(PythonPackageConfigFile):
|
|
656
|
+
"""Config file that copies content from an existing module.
|
|
657
|
+
|
|
658
|
+
Used to replicate pyrig's internal module structure in the target
|
|
659
|
+
project, allowing customization through subclassing.
|
|
660
|
+
"""
|
|
661
|
+
|
|
662
|
+
@classmethod
|
|
663
|
+
@abstractmethod
|
|
664
|
+
def get_src_module(cls) -> ModuleType:
|
|
665
|
+
"""Get the source module to copy.
|
|
666
|
+
|
|
667
|
+
Returns:
|
|
668
|
+
The module whose content will be copied.
|
|
669
|
+
"""
|
|
670
|
+
|
|
671
|
+
@classmethod
|
|
672
|
+
def get_parent_path(cls) -> Path:
|
|
673
|
+
"""Get the target directory for the copied module.
|
|
674
|
+
|
|
675
|
+
Transforms the source module path by replacing pyrig with
|
|
676
|
+
the target project's package name.
|
|
677
|
+
|
|
678
|
+
Returns:
|
|
679
|
+
Path to the target directory.
|
|
680
|
+
"""
|
|
681
|
+
from pyrig.dev.configs.pyproject import PyprojectConfigFile # noqa: PLC0415
|
|
682
|
+
|
|
683
|
+
src_module = cls.get_src_module()
|
|
684
|
+
new_module_name = get_module_name_replacing_start_module(
|
|
685
|
+
src_module, PyprojectConfigFile.get_package_name()
|
|
686
|
+
)
|
|
687
|
+
return to_path(new_module_name, is_package=True).parent
|
|
688
|
+
|
|
689
|
+
@classmethod
|
|
690
|
+
def get_content_str(cls) -> str:
|
|
691
|
+
"""Get the source module's content as a string.
|
|
692
|
+
|
|
693
|
+
Returns:
|
|
694
|
+
The full source code of the module.
|
|
695
|
+
"""
|
|
696
|
+
src_module = cls.get_src_module()
|
|
697
|
+
return get_module_content_as_str(src_module)
|
|
698
|
+
|
|
699
|
+
@classmethod
|
|
700
|
+
def get_filename(cls) -> str:
|
|
701
|
+
"""Get the filename from the source module name.
|
|
702
|
+
|
|
703
|
+
Returns:
|
|
704
|
+
The module's isolated name (without package prefix).
|
|
705
|
+
"""
|
|
706
|
+
src_module = cls.get_src_module()
|
|
707
|
+
return get_isolated_obj_name(src_module)
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
class CopyModuleOnlyDocstringConfigFile(CopyModuleConfigFile):
|
|
711
|
+
"""Config file that copies only the docstring from a module.
|
|
712
|
+
|
|
713
|
+
Useful for creating stub files that preserve documentation
|
|
714
|
+
but allow users to provide their own implementation.
|
|
715
|
+
"""
|
|
716
|
+
|
|
717
|
+
@classmethod
|
|
718
|
+
def get_content_str(cls) -> str:
|
|
719
|
+
"""Extract only the docstring from the source module.
|
|
720
|
+
|
|
721
|
+
Returns:
|
|
722
|
+
The module docstring wrapped in triple quotes.
|
|
723
|
+
"""
|
|
724
|
+
content = super().get_content_str()
|
|
725
|
+
parts = content.split('"""', 2)
|
|
726
|
+
return '"""' + parts[1] + '"""\n'
|
|
727
|
+
|
|
728
|
+
@classmethod
|
|
729
|
+
def is_correct(cls) -> bool:
|
|
730
|
+
"""Check if the file contains the source docstring.
|
|
731
|
+
|
|
732
|
+
Returns:
|
|
733
|
+
True if the docstring is present in the file.
|
|
734
|
+
"""
|
|
735
|
+
docstring = cls.get_content_str().strip()
|
|
736
|
+
# remove the triple quotes from the docstring
|
|
737
|
+
docstring = docstring[3:-3]
|
|
738
|
+
return docstring in cls.get_file_content() or super().is_correct()
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
class InitConfigFile(CopyModuleOnlyDocstringConfigFile):
|
|
742
|
+
"""Config file for creating __init__.py files.
|
|
743
|
+
|
|
744
|
+
Copies only the docstring from the source module's __init__.py.
|
|
745
|
+
"""
|
|
746
|
+
|
|
747
|
+
@classmethod
|
|
748
|
+
def get_filename(cls) -> str:
|
|
749
|
+
"""Get the __init__ filename.
|
|
750
|
+
|
|
751
|
+
Returns:
|
|
752
|
+
The string "__init__".
|
|
753
|
+
"""
|
|
754
|
+
return "__init__"
|
|
755
|
+
|
|
756
|
+
@classmethod
|
|
757
|
+
def get_parent_path(cls) -> Path:
|
|
758
|
+
"""Get the directory where __init__.py will be created.
|
|
759
|
+
|
|
760
|
+
Returns:
|
|
761
|
+
Path to the package directory.
|
|
762
|
+
"""
|
|
763
|
+
path = super().get_parent_path()
|
|
764
|
+
# this path will be parent of the init file
|
|
765
|
+
return path / get_isolated_obj_name(cls.get_src_module())
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
class TypedConfigFile(ConfigFile):
|
|
769
|
+
"""Config file for py.typed marker files.
|
|
770
|
+
|
|
771
|
+
Creates empty py.typed files to indicate PEP 561 compliance.
|
|
772
|
+
"""
|
|
773
|
+
|
|
774
|
+
@classmethod
|
|
775
|
+
def get_file_extension(cls) -> str:
|
|
776
|
+
"""Get the typed file extension.
|
|
777
|
+
|
|
778
|
+
Returns:
|
|
779
|
+
The string "typed".
|
|
780
|
+
"""
|
|
781
|
+
return "typed"
|
|
782
|
+
|
|
783
|
+
@classmethod
|
|
784
|
+
def load(cls) -> dict[str, Any] | list[Any]:
|
|
785
|
+
"""Load the py.typed file (always empty).
|
|
786
|
+
|
|
787
|
+
Returns:
|
|
788
|
+
An empty dict.
|
|
789
|
+
"""
|
|
790
|
+
return {}
|
|
791
|
+
|
|
792
|
+
@classmethod
|
|
793
|
+
def dump(cls, config: dict[str, Any] | list[Any]) -> None:
|
|
794
|
+
"""Validate that py.typed files remain empty.
|
|
795
|
+
|
|
796
|
+
Args:
|
|
797
|
+
config: Must be empty.
|
|
798
|
+
|
|
799
|
+
Raises:
|
|
800
|
+
ValueError: If config is not empty.
|
|
801
|
+
"""
|
|
802
|
+
if config:
|
|
803
|
+
msg = "Cannot dump to py.typed file."
|
|
804
|
+
raise ValueError(msg)
|
|
805
|
+
|
|
806
|
+
@classmethod
|
|
807
|
+
def get_configs(cls) -> dict[str, Any] | list[Any]:
|
|
808
|
+
"""Get the expected configuration (empty).
|
|
809
|
+
|
|
810
|
+
Returns:
|
|
811
|
+
An empty dict.
|
|
812
|
+
"""
|
|
813
|
+
return {}
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
class PythonTestsConfigFile(PythonConfigFile):
|
|
817
|
+
"""Abstract base class for Python files in the tests directory."""
|
|
818
|
+
|
|
819
|
+
@classmethod
|
|
820
|
+
def get_parent_path(cls) -> Path:
|
|
821
|
+
"""Get the tests directory path.
|
|
822
|
+
|
|
823
|
+
Returns:
|
|
824
|
+
Path to the tests package.
|
|
825
|
+
"""
|
|
826
|
+
return Path(TESTS_PACKAGE_NAME)
|