dycw-actions 0.3.2__py3-none-any.whl → 0.7.1__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 (83) hide show
  1. actions/__init__.py +1 -1
  2. actions/action_dicts/constants.py +8 -0
  3. actions/action_dicts/lib.py +186 -0
  4. actions/clean_dir/cli.py +21 -0
  5. actions/clean_dir/constants.py +7 -0
  6. actions/clean_dir/lib.py +59 -0
  7. actions/clean_dir/settings.py +18 -0
  8. actions/cli.py +95 -6
  9. actions/constants.py +10 -0
  10. actions/pre_commit/click.py +15 -0
  11. actions/pre_commit/conformalize_repo/__init__.py +1 -0
  12. actions/pre_commit/conformalize_repo/cli.py +64 -0
  13. actions/pre_commit/conformalize_repo/configs/gitignore +244 -0
  14. actions/pre_commit/conformalize_repo/constants.py +78 -0
  15. actions/pre_commit/conformalize_repo/lib.py +1293 -0
  16. actions/pre_commit/conformalize_repo/settings.py +119 -0
  17. actions/pre_commit/constants.py +8 -0
  18. actions/pre_commit/format_requirements/__init__.py +1 -0
  19. actions/pre_commit/format_requirements/cli.py +24 -0
  20. actions/pre_commit/format_requirements/constants.py +7 -0
  21. actions/pre_commit/format_requirements/lib.py +52 -0
  22. actions/pre_commit/replace_sequence_strs/__init__.py +1 -0
  23. actions/pre_commit/replace_sequence_strs/cli.py +24 -0
  24. actions/pre_commit/replace_sequence_strs/constants.py +7 -0
  25. actions/pre_commit/replace_sequence_strs/lib.py +73 -0
  26. actions/pre_commit/touch_empty_py/__init__.py +1 -0
  27. actions/pre_commit/touch_empty_py/cli.py +24 -0
  28. actions/pre_commit/touch_empty_py/constants.py +7 -0
  29. actions/pre_commit/touch_empty_py/lib.py +54 -0
  30. actions/pre_commit/touch_py_typed/__init__.py +1 -0
  31. actions/pre_commit/touch_py_typed/cli.py +24 -0
  32. actions/pre_commit/touch_py_typed/constants.py +7 -0
  33. actions/pre_commit/touch_py_typed/lib.py +64 -0
  34. actions/pre_commit/update_requirements/__init__.py +1 -0
  35. actions/pre_commit/update_requirements/classes.py +117 -0
  36. actions/pre_commit/update_requirements/cli.py +24 -0
  37. actions/pre_commit/update_requirements/constants.py +7 -0
  38. actions/pre_commit/update_requirements/lib.py +128 -0
  39. actions/pre_commit/utilities.py +386 -0
  40. actions/publish_package/__init__.py +1 -0
  41. actions/publish_package/cli.py +27 -0
  42. actions/publish_package/constants.py +7 -0
  43. actions/{publish → publish_package}/lib.py +18 -17
  44. actions/{publish → publish_package}/settings.py +7 -7
  45. actions/py.typed +0 -0
  46. actions/random_sleep/__init__.py +1 -0
  47. actions/random_sleep/cli.py +26 -0
  48. actions/random_sleep/constants.py +7 -0
  49. actions/{sleep → random_sleep}/lib.py +14 -13
  50. actions/{sleep → random_sleep}/settings.py +3 -3
  51. actions/run_hooks/__init__.py +1 -0
  52. actions/run_hooks/cli.py +21 -0
  53. actions/run_hooks/constants.py +7 -0
  54. actions/run_hooks/lib.py +97 -0
  55. actions/run_hooks/settings.py +24 -0
  56. actions/setup_cronjob/__init__.py +1 -0
  57. actions/setup_cronjob/cli.py +31 -0
  58. actions/setup_cronjob/configs/cron.tmpl +3 -0
  59. actions/setup_cronjob/configs/logrotate.tmpl +10 -0
  60. actions/setup_cronjob/constants.py +12 -0
  61. actions/setup_cronjob/lib.py +120 -0
  62. actions/setup_cronjob/settings.py +27 -0
  63. actions/tag_commit/__init__.py +1 -0
  64. actions/tag_commit/cli.py +27 -0
  65. actions/tag_commit/constants.py +7 -0
  66. actions/tag_commit/lib.py +63 -0
  67. actions/{tag → tag_commit}/settings.py +3 -3
  68. actions/types.py +14 -1
  69. actions/utilities.py +131 -17
  70. dycw_actions-0.7.1.dist-info/METADATA +22 -0
  71. dycw_actions-0.7.1.dist-info/RECORD +77 -0
  72. {dycw_actions-0.3.2.dist-info → dycw_actions-0.7.1.dist-info}/WHEEL +1 -1
  73. actions/publish/cli.py +0 -43
  74. actions/settings.py +0 -18
  75. actions/sleep/cli.py +0 -39
  76. actions/tag/cli.py +0 -43
  77. actions/tag/lib.py +0 -62
  78. dycw_actions-0.3.2.dist-info/METADATA +0 -14
  79. dycw_actions-0.3.2.dist-info/RECORD +0 -22
  80. /actions/{publish → action_dicts}/__init__.py +0 -0
  81. /actions/{sleep → clean_dir}/__init__.py +0 -0
  82. /actions/{tag → pre_commit}/__init__.py +0 -0
  83. {dycw_actions-0.3.2.dist-info → dycw_actions-0.7.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,1293 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from contextlib import contextmanager, suppress
5
+ from itertools import product
6
+ from pathlib import Path
7
+ from re import MULTILINE, escape, search, sub
8
+ from shlex import join
9
+ from string import Template
10
+ from subprocess import CalledProcessError
11
+ from typing import TYPE_CHECKING, Literal, assert_never
12
+
13
+ import tomlkit
14
+ from ruamel.yaml.scalarstring import LiteralScalarString
15
+ from tomlkit import TOMLDocument, table
16
+ from tomlkit.exceptions import NonExistentKey
17
+ from utilities.inflect import counted_noun
18
+ from utilities.pathlib import get_repo_root
19
+ from utilities.re import extract_groups
20
+ from utilities.subprocess import ripgrep
21
+ from utilities.text import repr_str, strip_and_dedent
22
+ from utilities.version import ParseVersionError, Version, parse_version
23
+ from utilities.whenever import HOUR, get_now
24
+ from whenever import ZonedDateTime
25
+ from xdg_base_dirs import xdg_cache_home
26
+
27
+ from actions import __version__
28
+ from actions.action_dicts.lib import (
29
+ run_action_pre_commit_dict,
30
+ run_action_publish_dict,
31
+ run_action_pyright_dict,
32
+ run_action_pytest_dict,
33
+ run_action_ruff_dict,
34
+ run_action_tag_dict,
35
+ )
36
+ from actions.constants import YAML_INSTANCE
37
+ from actions.logging import LOGGER
38
+ from actions.pre_commit.conformalize_repo.constants import (
39
+ ACTIONS_URL,
40
+ BUMPVERSION_TOML,
41
+ CONFORMALIZE_REPO_SUB_CMD,
42
+ COVERAGERC_TOML,
43
+ DOCKERFMT_URL,
44
+ ENVRC,
45
+ GITHUB_PULL_REQUEST_YAML,
46
+ GITHUB_PUSH_YAML,
47
+ GITIGNORE,
48
+ MAX_PYTHON_VERSION,
49
+ PATH_CONFIGS,
50
+ PRE_COMMIT_CONFIG_YAML,
51
+ PRE_COMMIT_HOOKS_URL,
52
+ PYPROJECT_TOML,
53
+ PYRIGHTCONFIG_JSON,
54
+ PYTEST_TOML,
55
+ README_MD,
56
+ RUFF_TOML,
57
+ RUFF_URL,
58
+ SHELLCHECK_URL,
59
+ SHFMT_URL,
60
+ TAPLO_URL,
61
+ UV_URL,
62
+ )
63
+ from actions.pre_commit.conformalize_repo.settings import SETTINGS
64
+ from actions.pre_commit.format_requirements.constants import FORMAT_REQUIREMENTS_SUB_CMD
65
+ from actions.pre_commit.replace_sequence_strs.constants import (
66
+ REPLACE_SEQUENCE_STRS_SUB_CMD,
67
+ )
68
+ from actions.pre_commit.touch_empty_py.constants import TOUCH_EMPTY_PY_SUB_CMD
69
+ from actions.pre_commit.touch_py_typed.constants import TOUCH_PY_TYPED_SUB_CMD
70
+ from actions.pre_commit.utilities import (
71
+ ensure_aot_contains,
72
+ ensure_contains,
73
+ ensure_contains_partial_dict,
74
+ ensure_contains_partial_str,
75
+ ensure_not_contains,
76
+ get_aot,
77
+ get_array,
78
+ get_dict,
79
+ get_list,
80
+ get_table,
81
+ yield_json_dict,
82
+ yield_text_file,
83
+ yield_toml_doc,
84
+ yield_yaml_dict,
85
+ )
86
+ from actions.utilities import logged_run, write_text
87
+
88
+ if TYPE_CHECKING:
89
+ from collections.abc import Iterator, MutableSet
90
+
91
+ from tomlkit.items import Table
92
+ from utilities.types import PathLike
93
+
94
+ from actions.types import StrDict
95
+
96
+
97
+ def conformalize_repo(
98
+ *,
99
+ coverage: bool = SETTINGS.coverage,
100
+ description: str | None = SETTINGS.description,
101
+ envrc: bool = SETTINGS.envrc,
102
+ envrc__uv: bool = SETTINGS.envrc__uv,
103
+ envrc__uv__native_tls: bool = SETTINGS.envrc__uv__native_tls,
104
+ github__pull_request__pre_commit: bool = SETTINGS.github__pull_request__pre_commit,
105
+ github__pull_request__pre_commit__gitea: bool = SETTINGS.github__pull_request__pre_commit__gitea,
106
+ github__pull_request__pyright: bool = SETTINGS.github__pull_request__pyright,
107
+ github__pull_request__pytest__macos: bool = SETTINGS.github__pull_request__pytest__macos,
108
+ github__pull_request__pytest__ubuntu: bool = SETTINGS.github__pull_request__pytest__ubuntu,
109
+ github__pull_request__pytest__windows: bool = SETTINGS.github__pull_request__pytest__windows,
110
+ github__pull_request__ruff: bool = SETTINGS.github__pull_request__ruff,
111
+ github__push__publish: bool = SETTINGS.github__push__publish,
112
+ github__push__tag: bool = SETTINGS.github__push__tag,
113
+ github__push__tag__major: bool = SETTINGS.github__push__tag__major,
114
+ github__push__tag__major_minor: bool = SETTINGS.github__push__tag__major_minor,
115
+ github__push__tag__latest: bool = SETTINGS.github__push__tag__latest,
116
+ gitignore: bool = SETTINGS.gitignore,
117
+ package_name: str | None = SETTINGS.package_name,
118
+ pre_commit__dockerfmt: bool = SETTINGS.pre_commit__dockerfmt,
119
+ pre_commit__prettier: bool = SETTINGS.pre_commit__prettier,
120
+ pre_commit__python: bool = SETTINGS.pre_commit__python,
121
+ pre_commit__ruff: bool = SETTINGS.pre_commit__ruff,
122
+ pre_commit__shell: bool = SETTINGS.pre_commit__shell,
123
+ pre_commit__taplo: bool = SETTINGS.pre_commit__taplo,
124
+ pre_commit__uv: bool = SETTINGS.pre_commit__uv,
125
+ pre_commit__uv__script: str | None = SETTINGS.pre_commit__uv__script,
126
+ pyproject: bool = SETTINGS.pyproject,
127
+ pyproject__project__optional_dependencies__scripts: bool = SETTINGS.pyproject__project__optional_dependencies__scripts,
128
+ pyproject__tool__uv__indexes: list[
129
+ tuple[str, str]
130
+ ] = SETTINGS.pyproject__tool__uv__indexes,
131
+ pyright: bool = SETTINGS.pyright,
132
+ pytest: bool = SETTINGS.pytest,
133
+ pytest__asyncio: bool = SETTINGS.pytest__asyncio,
134
+ pytest__ignore_warnings: bool = SETTINGS.pytest__ignore_warnings,
135
+ pytest__timeout: int | None = SETTINGS.pytest__timeout,
136
+ python_package_name: str | None = SETTINGS.python_package_name,
137
+ python_version: str = SETTINGS.python_version,
138
+ readme: bool = SETTINGS.readme,
139
+ repo_name: str | None = SETTINGS.repo_name,
140
+ ruff: bool = SETTINGS.ruff,
141
+ run_version_bump: bool = SETTINGS.run_version_bump,
142
+ script: str | None = SETTINGS.script,
143
+ ) -> None:
144
+ LOGGER.info(
145
+ strip_and_dedent("""
146
+ Running '%s' (version %s) with settings:
147
+ - coverage = %s
148
+ - description = %s
149
+ - envrc = %s
150
+ - envrc__uv = %s
151
+ - envrc__uv__native_tls = %s
152
+ - github__pull_request__pre_commit = %s
153
+ - github__pull_request__pre_commit__gitea = %s
154
+ - github__pull_request__pyright = %s
155
+ - github__pull_request__pytest__macos = %s
156
+ - github__pull_request__pytest__ubuntu = %s
157
+ - github__pull_request__pytest__windows = %s
158
+ - github__pull_request__ruff = %s
159
+ - github__push__publish = %s
160
+ - github__push__tag = %s
161
+ - github__push__tag__major = %s
162
+ - github__push__tag__major_minor = %s
163
+ - github__push__tag__latest = %s
164
+ - gitignore = %s
165
+ - package_name = %s
166
+ - pre_commit__dockerfmt = %s
167
+ - pre_commit__prettier = %s
168
+ - pre_commit__python = %s
169
+ - pre_commit__ruff = %s
170
+ - pre_commit__shell = %s
171
+ - pre_commit__taplo = %s
172
+ - pre_commit__uv = %s
173
+ - pre_commit__uv__script = %s
174
+ - pyproject = %s
175
+ - pyproject__project__optional_dependencies__scripts = %s
176
+ - pyproject__tool__uv__indexes = %s
177
+ - pyright = %s
178
+ - pytest = %s
179
+ - pytest__asyncio = %s
180
+ - pytest__ignore_warnings = %s
181
+ - pytest__timeout = %s
182
+ - python_package_name = %s
183
+ - python_version = %s
184
+ - readme = %s
185
+ - repo_name = %s
186
+ - ruff = %s
187
+ - run_version_bump = %s
188
+ - script = %s
189
+ """),
190
+ conformalize_repo.__name__,
191
+ __version__,
192
+ coverage,
193
+ description,
194
+ envrc,
195
+ envrc__uv,
196
+ envrc__uv__native_tls,
197
+ github__pull_request__pre_commit,
198
+ github__pull_request__pre_commit__gitea,
199
+ github__pull_request__pyright,
200
+ github__pull_request__pytest__macos,
201
+ github__pull_request__pytest__ubuntu,
202
+ github__pull_request__pytest__windows,
203
+ github__pull_request__ruff,
204
+ github__push__publish,
205
+ github__push__tag,
206
+ github__push__tag__major,
207
+ github__push__tag__major_minor,
208
+ github__push__tag__latest,
209
+ gitignore,
210
+ package_name,
211
+ pre_commit__dockerfmt,
212
+ pre_commit__prettier,
213
+ pre_commit__python,
214
+ pre_commit__ruff,
215
+ pre_commit__shell,
216
+ pre_commit__taplo,
217
+ pre_commit__uv,
218
+ pre_commit__uv__script,
219
+ pyproject,
220
+ pyproject__project__optional_dependencies__scripts,
221
+ pyproject__tool__uv__indexes,
222
+ pyright,
223
+ pytest,
224
+ pytest__asyncio,
225
+ pytest__ignore_warnings,
226
+ pytest__timeout,
227
+ python_package_name,
228
+ python_version,
229
+ readme,
230
+ repo_name,
231
+ ruff,
232
+ run_version_bump,
233
+ script,
234
+ )
235
+ modifications: set[Path] = set()
236
+ add_bumpversion_toml(
237
+ modifications=modifications,
238
+ pyproject=pyproject,
239
+ package_name=package_name,
240
+ python_package_name=python_package_name,
241
+ )
242
+ check_versions()
243
+ run_pre_commit_update(modifications=modifications)
244
+ run_ripgrep_and_replace(modifications=modifications, version=python_version)
245
+ update_action_file_extensions(modifications=modifications)
246
+ update_action_versions(modifications=modifications)
247
+ add_pre_commit_config_yaml(
248
+ modifications=modifications,
249
+ dockerfmt=pre_commit__dockerfmt,
250
+ prettier=pre_commit__prettier,
251
+ python=pre_commit__python,
252
+ ruff=pre_commit__ruff,
253
+ shell=pre_commit__shell,
254
+ taplo=pre_commit__taplo,
255
+ uv=pre_commit__uv,
256
+ script=script,
257
+ )
258
+ if coverage:
259
+ add_coveragerc_toml(modifications=modifications)
260
+ if envrc or envrc__uv or envrc__uv__native_tls:
261
+ add_envrc(
262
+ modifications=modifications,
263
+ uv=envrc__uv,
264
+ uv__native_tls=envrc__uv__native_tls,
265
+ python_version=python_version,
266
+ script=script,
267
+ )
268
+ if (
269
+ github__pull_request__pre_commit
270
+ or github__pull_request__pre_commit__gitea
271
+ or github__pull_request__pyright
272
+ or github__pull_request__pytest__windows
273
+ or github__pull_request__pytest__macos
274
+ or github__pull_request__pytest__ubuntu
275
+ or github__pull_request__ruff
276
+ ):
277
+ add_github_pull_request_yaml(
278
+ modifications=modifications,
279
+ pre_commit=github__pull_request__pre_commit,
280
+ pre_commit__gitea=github__pull_request__pre_commit__gitea,
281
+ pyright=github__pull_request__pyright,
282
+ pytest__windows=github__pull_request__pytest__windows,
283
+ pytest__macos=github__pull_request__pytest__macos,
284
+ pytest__ubuntu=github__pull_request__pytest__ubuntu,
285
+ pytest__timeout=pytest__timeout,
286
+ python_version=python_version,
287
+ ruff=ruff,
288
+ script=script,
289
+ )
290
+ if (
291
+ github__push__publish
292
+ or github__push__tag
293
+ or github__push__tag__major_minor
294
+ or github__push__tag__major
295
+ or github__push__tag__latest
296
+ ):
297
+ add_github_push_yaml(
298
+ modifications=modifications,
299
+ publish=github__push__publish,
300
+ tag=github__push__tag,
301
+ tag__major_minor=github__push__tag__major_minor,
302
+ tag__major=github__push__tag__major,
303
+ tag__latest=github__push__tag__latest,
304
+ )
305
+ if gitignore:
306
+ add_gitignore(modifications=modifications)
307
+ if (
308
+ pyproject
309
+ or pyproject__project__optional_dependencies__scripts
310
+ or (len(pyproject__tool__uv__indexes) >= 1)
311
+ ):
312
+ add_pyproject_toml(
313
+ modifications=modifications,
314
+ python_version=python_version,
315
+ description=description,
316
+ package_name=package_name,
317
+ readme=readme,
318
+ optional_dependencies__scripts=pyproject__project__optional_dependencies__scripts,
319
+ python_package_name=python_package_name,
320
+ tool__uv__indexes=pyproject__tool__uv__indexes,
321
+ )
322
+ if pyright:
323
+ add_pyrightconfig_json(
324
+ modifications=modifications, python_version=python_version, script=script
325
+ )
326
+ if (
327
+ pytest
328
+ or pytest__asyncio
329
+ or pytest__ignore_warnings
330
+ or (pytest__timeout is not None)
331
+ ):
332
+ add_pytest_toml(
333
+ modifications=modifications,
334
+ asyncio=pytest__asyncio,
335
+ ignore_warnings=pytest__ignore_warnings,
336
+ timeout=pytest__timeout,
337
+ coverage=coverage,
338
+ package_name=package_name,
339
+ python_package_name=python_package_name,
340
+ script=script,
341
+ )
342
+ if readme:
343
+ add_readme_md(
344
+ modifications=modifications, name=repo_name, description=description
345
+ )
346
+ if ruff:
347
+ add_ruff_toml(modifications=modifications, python_version=python_version)
348
+ if run_version_bump:
349
+ run_bump_my_version(modifications=modifications)
350
+ if len(modifications) >= 1:
351
+ LOGGER.info(
352
+ "Exiting due to %s: %s",
353
+ counted_noun(modifications, "modification"),
354
+ ", ".join(map(repr_str, sorted(modifications))),
355
+ )
356
+ sys.exit(1)
357
+
358
+
359
+ ##
360
+
361
+
362
+ def add_bumpversion_toml(
363
+ *,
364
+ modifications: MutableSet[Path] | None = None,
365
+ pyproject: bool = SETTINGS.pyproject,
366
+ package_name: str | None = SETTINGS.package_name,
367
+ python_package_name: str | None = SETTINGS.python_package_name,
368
+ ) -> None:
369
+ with yield_bumpversion_toml(modifications=modifications) as doc:
370
+ tool = get_table(doc, "tool")
371
+ bumpversion = get_table(tool, "bumpversion")
372
+ if pyproject:
373
+ files = get_aot(bumpversion, "files")
374
+ ensure_aot_contains(
375
+ files,
376
+ _add_bumpversion_toml_file(PYPROJECT_TOML, 'version = "${version}"'),
377
+ )
378
+ if (
379
+ python_package_name_use := get_python_package_name(
380
+ package_name=package_name, python_package_name=python_package_name
381
+ )
382
+ ) is not None:
383
+ files = get_aot(bumpversion, "files")
384
+ ensure_aot_contains(
385
+ files,
386
+ _add_bumpversion_toml_file(
387
+ f"src/{python_package_name_use}/__init__.py",
388
+ '__version__ = "${version}"',
389
+ ),
390
+ )
391
+
392
+
393
+ def _add_bumpversion_toml_file(path: PathLike, template: str, /) -> Table:
394
+ tab = table()
395
+ tab["filename"] = str(path)
396
+ tab["search"] = Template(template).substitute(version="{current_version}")
397
+ tab["replace"] = Template(template).substitute(version="{new_version}")
398
+ return tab
399
+
400
+
401
+ ##
402
+
403
+
404
+ def add_coveragerc_toml(*, modifications: MutableSet[Path] | None = None) -> None:
405
+ with yield_toml_doc(COVERAGERC_TOML, modifications=modifications) as doc:
406
+ html = get_table(doc, "html")
407
+ html["directory"] = ".coverage/html"
408
+ report = get_table(doc, "report")
409
+ exclude_also = get_array(report, "exclude_also")
410
+ ensure_contains(exclude_also, "@overload", "if TYPE_CHECKING:")
411
+ report["fail_under"] = 100.0
412
+ report["skip_covered"] = True
413
+ report["skip_empty"] = True
414
+ run = get_table(doc, "run")
415
+ run["branch"] = True
416
+ run["data_file"] = ".coverage/data"
417
+ run["parallel"] = True
418
+
419
+
420
+ ##
421
+
422
+
423
+ def add_envrc(
424
+ *,
425
+ modifications: MutableSet[Path] | None = None,
426
+ uv: bool = SETTINGS.envrc__uv,
427
+ uv__native_tls: bool = SETTINGS.envrc__uv__native_tls,
428
+ python_version: str = SETTINGS.python_version,
429
+ script: str | None = SETTINGS.script,
430
+ ) -> None:
431
+ with yield_text_file(ENVRC, modifications=modifications) as context:
432
+ shebang = strip_and_dedent("""
433
+ #!/usr/bin/env sh
434
+ # shellcheck source=/dev/null
435
+ """)
436
+ if search(escape(shebang), context.output, flags=MULTILINE) is None:
437
+ context.output += f"\n\n{shebang}"
438
+
439
+ echo = strip_and_dedent("""
440
+ # echo
441
+ echo_date() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" >&2; }
442
+ """)
443
+ if search(escape(echo), context.output, flags=MULTILINE) is None:
444
+ context.output += f"\n\n{echo}"
445
+
446
+ if uv:
447
+ uv_text = _add_envrc_uv_text(
448
+ native_tls=uv__native_tls, python_version=python_version, script=script
449
+ )
450
+ if search(escape(uv_text), context.output, flags=MULTILINE) is None:
451
+ context.output += f"\n\n{echo}"
452
+
453
+
454
+ def _add_envrc_uv_text(
455
+ *,
456
+ native_tls: bool = SETTINGS.envrc__uv__native_tls,
457
+ python_version: str = SETTINGS.python_version,
458
+ script: str | None = SETTINGS.script,
459
+ ) -> str:
460
+ lines: list[str] = [
461
+ strip_and_dedent("""
462
+ # uv
463
+ export UV_MANAGED_PYTHON='true'
464
+ """)
465
+ ]
466
+ if native_tls:
467
+ lines.append("export UV_NATIVE_TLS='true'")
468
+ lines.append(
469
+ strip_and_dedent(f"""
470
+ export UV_PRERELEASE='disallow'
471
+ export UV_PYTHON='{python_version}'
472
+ if ! command -v uv >/dev/null 2>&1; then
473
+ echo_date "ERROR: 'uv' not found" && exit 1
474
+ fi
475
+ activate='.venv/bin/activate'
476
+ if [ -f $activate ]; then
477
+ . $activate
478
+ else
479
+ uv venv
480
+ fi
481
+ """)
482
+ )
483
+ args: list[str] = ["uv", "sync"]
484
+ if script is None:
485
+ args.extend(["--all-extras", "--all-groups"])
486
+ args.extend(["--active", "--locked"])
487
+ if script is not None:
488
+ args.extend(["--script", script])
489
+ lines.append(join(args))
490
+ return "\n".join(lines)
491
+
492
+
493
+ ##
494
+
495
+
496
+ def add_github_pull_request_yaml(
497
+ *,
498
+ modifications: MutableSet[Path] | None = None,
499
+ pre_commit: bool = SETTINGS.github__pull_request__pre_commit,
500
+ pre_commit__gitea: bool = SETTINGS.github__pull_request__pre_commit__gitea,
501
+ pyright: bool = SETTINGS.github__pull_request__pyright,
502
+ pytest__macos: bool = SETTINGS.github__pull_request__pytest__macos,
503
+ pytest__ubuntu: bool = SETTINGS.github__pull_request__pytest__ubuntu,
504
+ pytest__windows: bool = SETTINGS.github__pull_request__pytest__windows,
505
+ pytest__timeout: int | None = SETTINGS.pytest__timeout,
506
+ python_version: str = SETTINGS.python_version,
507
+ ruff: bool = SETTINGS.github__pull_request__ruff,
508
+ script: str | None = SETTINGS.script,
509
+ ) -> None:
510
+ with yield_yaml_dict(
511
+ GITHUB_PULL_REQUEST_YAML, modifications=modifications
512
+ ) as dict_:
513
+ dict_["name"] = "pull-request"
514
+ on = get_dict(dict_, "on")
515
+ pull_request = get_dict(on, "pull_request")
516
+ branches = get_list(pull_request, "branches")
517
+ ensure_contains(branches, "master")
518
+ schedule = get_list(on, "schedule")
519
+ ensure_contains(schedule, {"cron": "0 0 * * *"})
520
+ jobs = get_dict(dict_, "jobs")
521
+ if pre_commit:
522
+ pre_commit_dict = get_dict(jobs, "pre-commit")
523
+ pre_commit_dict["runs-on"] = "ubuntu-latest"
524
+ steps = get_list(pre_commit_dict, "steps")
525
+ ensure_contains(
526
+ steps,
527
+ run_action_pre_commit_dict(
528
+ repos=LiteralScalarString(
529
+ strip_and_dedent("""
530
+ dycw/actions
531
+ pre-commit/pre-commit-hooks
532
+ """)
533
+ ),
534
+ gitea=pre_commit__gitea,
535
+ ),
536
+ )
537
+ if pyright:
538
+ pyright_dict = get_dict(jobs, "pyright")
539
+ pyright_dict["runs-on"] = "ubuntu-latest"
540
+ steps = get_list(pyright_dict, "steps")
541
+ ensure_contains(
542
+ steps,
543
+ run_action_pyright_dict(
544
+ python_version=python_version, with_requirements=script
545
+ ),
546
+ )
547
+ if pytest__macos or pytest__ubuntu or pytest__windows:
548
+ pytest_dict = get_dict(jobs, "pytest")
549
+ env = get_dict(pytest_dict, "env")
550
+ env["CI"] = "1"
551
+ pytest_dict["name"] = (
552
+ "pytest (${{matrix.os}}, ${{matrix.python-version}}, ${{matrix.resolution}})"
553
+ )
554
+ pytest_dict["runs-on"] = "${{matrix.os}}"
555
+ steps = get_list(pytest_dict, "steps")
556
+ ensure_contains(
557
+ steps,
558
+ run_action_pytest_dict(
559
+ python_version="${{matrix.python-version}}",
560
+ resolution="${{matrix.resolution}}",
561
+ with_requirements=script,
562
+ ),
563
+ )
564
+ strategy_dict = get_dict(pytest_dict, "strategy")
565
+ strategy_dict["fail-fast"] = False
566
+ matrix = get_dict(strategy_dict, "matrix")
567
+ os = get_list(matrix, "os")
568
+ if pytest__macos:
569
+ ensure_contains(os, "macos-latest")
570
+ if pytest__ubuntu:
571
+ ensure_contains(os, "ubuntu-latest")
572
+ if pytest__windows:
573
+ ensure_contains(os, "windows-latest")
574
+ python_version_dict = get_list(matrix, "python-version")
575
+ ensure_contains(python_version_dict, *yield_python_versions(python_version))
576
+ resolution = get_list(matrix, "resolution")
577
+ ensure_contains(resolution, "highest", "lowest-direct")
578
+ if pytest__timeout is not None:
579
+ pytest_dict["timeout-minutes"] = max(round(pytest__timeout / 60), 1)
580
+ if ruff:
581
+ ruff_dict = get_dict(jobs, "ruff")
582
+ ruff_dict["runs-on"] = "ubuntu-latest"
583
+ steps = get_list(ruff_dict, "steps")
584
+ ensure_contains(steps, run_action_ruff_dict())
585
+
586
+
587
+ ##
588
+
589
+
590
+ def add_github_push_yaml(
591
+ *,
592
+ modifications: MutableSet[Path] | None = None,
593
+ publish: bool = SETTINGS.github__push__publish,
594
+ tag: bool = SETTINGS.github__push__tag,
595
+ tag__major_minor: bool = SETTINGS.github__push__tag__major_minor,
596
+ tag__major: bool = SETTINGS.github__push__tag__major,
597
+ tag__latest: bool = SETTINGS.github__push__tag__latest,
598
+ ) -> None:
599
+ with yield_yaml_dict(GITHUB_PUSH_YAML, modifications=modifications) as dict_:
600
+ dict_["name"] = "push"
601
+ on = get_dict(dict_, "on")
602
+ push = get_dict(on, "push")
603
+ branches = get_list(push, "branches")
604
+ ensure_contains(branches, "master")
605
+ jobs = get_dict(dict_, "jobs")
606
+ if publish:
607
+ publish_dict = get_dict(jobs, "publish")
608
+ environment = get_dict(publish_dict, "environment")
609
+ environment["name"] = "pypi"
610
+ permissions = get_dict(publish_dict, "permissions")
611
+ permissions["id-token"] = "write"
612
+ publish_dict["runs-on"] = "ubuntu-latest"
613
+ steps = get_list(publish_dict, "steps")
614
+ ensure_contains(steps, run_action_publish_dict())
615
+ if tag or tag__major_minor or tag__major or tag__latest:
616
+ tag_dict = get_dict(jobs, "tag")
617
+ tag_dict["runs-on"] = "ubuntu-latest"
618
+ steps = get_list(tag_dict, "steps")
619
+ ensure_contains(
620
+ steps,
621
+ run_action_tag_dict(
622
+ major_minor=tag__major_minor, major=tag__major, latest=tag__latest
623
+ ),
624
+ )
625
+
626
+
627
+ ##
628
+
629
+
630
+ def add_gitignore(*, modifications: MutableSet[Path] | None = None) -> None:
631
+ with yield_text_file(GITIGNORE, modifications=modifications) as context:
632
+ text = (PATH_CONFIGS / "gitignore").read_text()
633
+ if search(escape(text), context.output, flags=MULTILINE) is None:
634
+ context.output += f"\n\n{text}"
635
+
636
+
637
+ ##
638
+
639
+
640
+ def add_pre_commit_config_yaml(
641
+ *,
642
+ modifications: MutableSet[Path] | None = None,
643
+ dockerfmt: bool = SETTINGS.pre_commit__dockerfmt,
644
+ prettier: bool = SETTINGS.pre_commit__prettier,
645
+ python: bool = SETTINGS.pre_commit__python,
646
+ ruff: bool = SETTINGS.pre_commit__ruff,
647
+ shell: bool = SETTINGS.pre_commit__shell,
648
+ taplo: bool = SETTINGS.pre_commit__taplo,
649
+ uv: bool = SETTINGS.pre_commit__uv,
650
+ script: str | None = SETTINGS.script,
651
+ ) -> None:
652
+ with yield_yaml_dict(PRE_COMMIT_CONFIG_YAML, modifications=modifications) as dict_:
653
+ _add_pre_commit_config_repo(dict_, ACTIONS_URL, CONFORMALIZE_REPO_SUB_CMD)
654
+ _add_pre_commit_config_repo(
655
+ dict_, PRE_COMMIT_HOOKS_URL, "check-executables-have-shebangs"
656
+ )
657
+ _add_pre_commit_config_repo(dict_, PRE_COMMIT_HOOKS_URL, "check-merge-conflict")
658
+ _add_pre_commit_config_repo(dict_, PRE_COMMIT_HOOKS_URL, "check-symlinks")
659
+ _add_pre_commit_config_repo(dict_, PRE_COMMIT_HOOKS_URL, "destroyed-symlinks")
660
+ _add_pre_commit_config_repo(dict_, PRE_COMMIT_HOOKS_URL, "detect-private-key")
661
+ _add_pre_commit_config_repo(dict_, PRE_COMMIT_HOOKS_URL, "end-of-file-fixer")
662
+ _add_pre_commit_config_repo(
663
+ dict_, PRE_COMMIT_HOOKS_URL, "mixed-line-ending", args=("add", ["--fix=lf"])
664
+ )
665
+ _add_pre_commit_config_repo(dict_, PRE_COMMIT_HOOKS_URL, "no-commit-to-branch")
666
+ _add_pre_commit_config_repo(
667
+ dict_,
668
+ PRE_COMMIT_HOOKS_URL,
669
+ "pretty-format-json",
670
+ args=("add", ["--autofix"]),
671
+ )
672
+ _add_pre_commit_config_repo(dict_, PRE_COMMIT_HOOKS_URL, "no-commit-to-branch")
673
+ _add_pre_commit_config_repo(dict_, PRE_COMMIT_HOOKS_URL, "trailing-whitespace")
674
+ if dockerfmt:
675
+ _add_pre_commit_config_repo(
676
+ dict_,
677
+ DOCKERFMT_URL,
678
+ "dockerfmt",
679
+ args=("add", ["--newline", "--write"]),
680
+ )
681
+ if prettier:
682
+ _add_pre_commit_config_repo(
683
+ dict_,
684
+ "local",
685
+ "prettier",
686
+ name="prettier",
687
+ entry="npx prettier --write",
688
+ language="system",
689
+ types_or=["markdown", "yaml"],
690
+ )
691
+ if python:
692
+ _add_pre_commit_config_repo(dict_, ACTIONS_URL, FORMAT_REQUIREMENTS_SUB_CMD)
693
+ _add_pre_commit_config_repo(
694
+ dict_, ACTIONS_URL, REPLACE_SEQUENCE_STRS_SUB_CMD
695
+ )
696
+ _add_pre_commit_config_repo(dict_, ACTIONS_URL, TOUCH_EMPTY_PY_SUB_CMD)
697
+ _add_pre_commit_config_repo(dict_, ACTIONS_URL, TOUCH_PY_TYPED_SUB_CMD)
698
+ if ruff:
699
+ _add_pre_commit_config_repo(
700
+ dict_, RUFF_URL, "ruff-check", args=("add", ["--fix"])
701
+ )
702
+ _add_pre_commit_config_repo(dict_, RUFF_URL, "ruff-format")
703
+ if shell:
704
+ _add_pre_commit_config_repo(dict_, SHFMT_URL, "shfmt")
705
+ _add_pre_commit_config_repo(dict_, SHELLCHECK_URL, "shellcheck")
706
+ if taplo:
707
+ _add_pre_commit_config_repo(
708
+ dict_,
709
+ TAPLO_URL,
710
+ "taplo-format",
711
+ args=(
712
+ "exact",
713
+ [
714
+ "--option",
715
+ "indent_tables=true",
716
+ "--option",
717
+ "indent_entries=true",
718
+ "--option",
719
+ "reorder_keys=true",
720
+ ],
721
+ ),
722
+ )
723
+ if uv:
724
+ _add_pre_commit_config_repo(
725
+ dict_,
726
+ UV_URL,
727
+ "uv-lock",
728
+ files=None if script is None else rf"^{escape(script)}$",
729
+ args=(
730
+ "add",
731
+ ["--upgrade", "--resolution", "highest", "--prerelease", "disallow"]
732
+ + ([] if script is None else [f"--script={script}"]),
733
+ ),
734
+ )
735
+
736
+
737
+ def _add_pre_commit_config_repo(
738
+ pre_commit_dict: StrDict,
739
+ url: str,
740
+ id_: str,
741
+ /,
742
+ *,
743
+ name: str | None = None,
744
+ entry: str | None = None,
745
+ language: str | None = None,
746
+ files: str | None = None,
747
+ types_or: list[str] | None = None,
748
+ args: tuple[Literal["add", "exact"], list[str]] | None = None,
749
+ ) -> None:
750
+ repos_list = get_list(pre_commit_dict, "repos")
751
+ repo_dict = ensure_contains_partial_dict(
752
+ repos_list, {"repo": url}, extra={} if url == "local" else {"rev": "master"}
753
+ )
754
+ hooks_list = get_list(repo_dict, "hooks")
755
+ hook_dict = ensure_contains_partial_dict(hooks_list, {"id": id_})
756
+ if name is not None:
757
+ hook_dict["name"] = name
758
+ if entry is not None:
759
+ hook_dict["entry"] = entry
760
+ if language is not None:
761
+ hook_dict["language"] = language
762
+ if files is not None:
763
+ hook_dict["files"] = files
764
+ if types_or is not None:
765
+ hook_dict["types_or"] = types_or
766
+ if args is not None:
767
+ match args:
768
+ case "add", list() as args_i:
769
+ hook_args = get_list(hook_dict, "args")
770
+ ensure_contains(hook_args, *args_i)
771
+ case "exact", list() as args_i:
772
+ hook_dict["args"] = args_i
773
+ case never:
774
+ assert_never(never)
775
+
776
+
777
+ ##
778
+
779
+
780
+ def add_pyproject_toml(
781
+ *,
782
+ modifications: MutableSet[Path] | None = None,
783
+ python_version: str = SETTINGS.python_version,
784
+ description: str | None = SETTINGS.description,
785
+ package_name: str | None = SETTINGS.package_name,
786
+ readme: bool = SETTINGS.readme,
787
+ optional_dependencies__scripts: bool = SETTINGS.pyproject__project__optional_dependencies__scripts,
788
+ python_package_name: str | None = SETTINGS.python_package_name,
789
+ tool__uv__indexes: list[tuple[str, str]] = SETTINGS.pyproject__tool__uv__indexes,
790
+ ) -> None:
791
+ with yield_toml_doc(PYPROJECT_TOML, modifications=modifications) as doc:
792
+ build_system = get_table(doc, "build-system")
793
+ build_system["build-backend"] = "uv_build"
794
+ build_system["requires"] = ["uv_build"]
795
+ project = get_table(doc, "project")
796
+ project["requires-python"] = f">= {python_version}"
797
+ if description is not None:
798
+ project["description"] = description
799
+ if package_name is not None:
800
+ project["name"] = package_name
801
+ if readme:
802
+ project["readme"] = "README.md"
803
+ project.setdefault("version", "0.1.0")
804
+ dependency_groups = get_table(doc, "dependency-groups")
805
+ dev = get_array(dependency_groups, "dev")
806
+ _ = ensure_contains_partial_str(dev, "dycw-utilities[test]")
807
+ _ = ensure_contains_partial_str(dev, "pyright")
808
+ _ = ensure_contains_partial_str(dev, "rich")
809
+ if optional_dependencies__scripts:
810
+ optional_dependencies = get_table(project, "optional-dependencies")
811
+ scripts = get_array(optional_dependencies, "scripts")
812
+ _ = ensure_contains_partial_str(scripts, "click")
813
+ if python_package_name is not None:
814
+ tool = get_table(doc, "tool")
815
+ uv = get_table(tool, "uv")
816
+ build_backend = get_table(uv, "build-backend")
817
+ build_backend["module-name"] = get_python_package_name(
818
+ package_name=package_name, python_package_name=python_package_name
819
+ )
820
+ build_backend["module-root"] = "src"
821
+ if len(tool__uv__indexes) >= 1:
822
+ tool = get_table(doc, "tool")
823
+ uv = get_table(tool, "uv")
824
+ indexes = get_aot(uv, "index")
825
+ for name, url in tool__uv__indexes:
826
+ index = table()
827
+ index["explicit"] = True
828
+ index["name"] = name
829
+ index["url"] = url
830
+ ensure_aot_contains(indexes, index)
831
+
832
+
833
+ ##
834
+
835
+
836
+ def add_pyrightconfig_json(
837
+ *,
838
+ modifications: MutableSet[Path] | None = None,
839
+ python_version: str = SETTINGS.python_version,
840
+ script: str | None = SETTINGS.script,
841
+ ) -> None:
842
+ with yield_json_dict(PYRIGHTCONFIG_JSON, modifications=modifications) as dict_:
843
+ dict_["deprecateTypingAliases"] = True
844
+ dict_["enableReachabilityAnalysis"] = False
845
+ include = get_list(dict_, "include")
846
+ ensure_contains(include, "src" if script is None else script)
847
+ dict_["pythonVersion"] = python_version
848
+ dict_["reportCallInDefaultInitializer"] = True
849
+ dict_["reportImplicitOverride"] = True
850
+ dict_["reportImplicitStringConcatenation"] = True
851
+ dict_["reportImportCycles"] = True
852
+ dict_["reportMissingSuperCall"] = True
853
+ dict_["reportMissingTypeArgument"] = False
854
+ dict_["reportMissingTypeStubs"] = False
855
+ dict_["reportPrivateImportUsage"] = False
856
+ dict_["reportPrivateUsage"] = False
857
+ dict_["reportPropertyTypeMismatch"] = True
858
+ dict_["reportUninitializedInstanceVariable"] = True
859
+ dict_["reportUnknownArgumentType"] = False
860
+ dict_["reportUnknownMemberType"] = False
861
+ dict_["reportUnknownParameterType"] = False
862
+ dict_["reportUnknownVariableType"] = False
863
+ dict_["reportUnnecessaryComparison"] = False
864
+ dict_["reportUnnecessaryTypeIgnoreComment"] = True
865
+ dict_["reportUnusedCallResult"] = True
866
+ dict_["reportUnusedImport"] = False
867
+ dict_["reportUnusedVariable"] = False
868
+ dict_["typeCheckingMode"] = "strict"
869
+
870
+
871
+ ##
872
+
873
+
874
+ def add_pytest_toml(
875
+ *,
876
+ modifications: MutableSet[Path] | None = None,
877
+ asyncio: bool = SETTINGS.pytest__asyncio,
878
+ ignore_warnings: bool = SETTINGS.pytest__ignore_warnings,
879
+ timeout: int | None = SETTINGS.pytest__timeout,
880
+ coverage: bool = SETTINGS.coverage,
881
+ package_name: str | None = SETTINGS.package_name,
882
+ python_package_name: str | None = SETTINGS.python_package_name,
883
+ script: str | None = SETTINGS.script,
884
+ ) -> None:
885
+ with yield_toml_doc(PYTEST_TOML, modifications=modifications) as doc:
886
+ pytest = get_table(doc, "pytest")
887
+ addopts = get_array(pytest, "addopts")
888
+ ensure_contains(
889
+ addopts,
890
+ "-ra",
891
+ "-vv",
892
+ "--color=auto",
893
+ "--durations=10",
894
+ "--durations-min=10",
895
+ )
896
+ if coverage and (
897
+ (
898
+ python_package_name_use := get_python_package_name(
899
+ package_name=package_name, python_package_name=python_package_name
900
+ )
901
+ )
902
+ is not None
903
+ ):
904
+ ensure_contains(
905
+ addopts,
906
+ f"--cov={python_package_name_use}",
907
+ f"--cov-config={COVERAGERC_TOML}",
908
+ "--cov-report=html",
909
+ )
910
+ pytest["collect_imported_tests"] = False
911
+ pytest["empty_parameter_set_mark"] = "fail_at_collect"
912
+ filterwarnings = get_array(pytest, "filterwarnings")
913
+ ensure_contains(filterwarnings, "error")
914
+ pytest["minversion"] = "9.0"
915
+ pytest["strict"] = True
916
+ testpaths = get_array(pytest, "testpaths")
917
+ ensure_contains(testpaths, "src/tests" if script is None else "tests")
918
+ pytest["xfail_strict"] = True
919
+ if asyncio:
920
+ pytest["asyncio_default_fixture_loop_scope"] = "function"
921
+ pytest["asyncio_mode"] = "auto"
922
+ if ignore_warnings:
923
+ filterwarnings = get_array(pytest, "filterwarnings")
924
+ ensure_contains(
925
+ filterwarnings,
926
+ "ignore::DeprecationWarning",
927
+ "ignore::ResourceWarning",
928
+ "ignore::RuntimeWarning",
929
+ )
930
+ if timeout is not None:
931
+ pytest["timeout"] = str(timeout)
932
+
933
+
934
+ ##
935
+
936
+
937
+ def add_readme_md(
938
+ *,
939
+ modifications: MutableSet[Path] | None = None,
940
+ name: str | None = SETTINGS.package_name,
941
+ description: str | None = SETTINGS.description,
942
+ ) -> None:
943
+ with yield_text_file(README_MD, modifications=modifications) as context:
944
+ lines: list[str] = []
945
+ if name is not None:
946
+ lines.append(f"# `{name}`")
947
+ if description is not None:
948
+ lines.append(description)
949
+ text = "\n\n".join(lines)
950
+ if search(escape(text), context.output, flags=MULTILINE) is None:
951
+ context.output += f"\n\n{text}"
952
+
953
+
954
+ ##
955
+
956
+
957
+ def add_ruff_toml(
958
+ *,
959
+ modifications: MutableSet[Path] | None = None,
960
+ python_version: str = SETTINGS.python_version,
961
+ ) -> None:
962
+ with yield_toml_doc(RUFF_TOML, modifications=modifications) as doc:
963
+ doc["target-version"] = f"py{python_version.replace('.', '')}"
964
+ doc["unsafe-fixes"] = True
965
+ fmt = get_table(doc, "format")
966
+ fmt["preview"] = True
967
+ fmt["skip-magic-trailing-comma"] = True
968
+ lint = get_table(doc, "lint")
969
+ lint["explicit-preview-rules"] = True
970
+ fixable = get_array(lint, "fixable")
971
+ ensure_contains(fixable, "ALL")
972
+ ignore = get_array(lint, "ignore")
973
+ ensure_contains(
974
+ ignore,
975
+ "ANN401", # any-type
976
+ "ASYNC109", # async-function-with-timeout
977
+ "C901", # complex-structure
978
+ "CPY", # flake8-copyright
979
+ "D", # pydocstyle
980
+ "E501", # line-too-long
981
+ "PD", # pandas-vet
982
+ "PERF203", # try-except-in-loop
983
+ "PLC0415", # import-outside-top-level
984
+ "PLE1205", # logging-too-many-args
985
+ "PLR0904", # too-many-public-methods
986
+ "PLR0911", # too-many-return-statements
987
+ "PLR0912", # too-many-branches
988
+ "PLR0913", # too-many-arguments
989
+ "PLR0915", # too-many-statements
990
+ "PLR2004", # magic-value-comparison
991
+ "PT012", # pytest-raises-with-multiple-statements
992
+ "PT013", # pytest-incorrect-pytest-import
993
+ "PYI041", # redundant-numeric-union
994
+ "S202", # tarfile-unsafe-members
995
+ "S310", # suspicious-url-open-usage
996
+ "S311", # suspicious-non-cryptographic-random-usage
997
+ "S602", # subprocess-popen-with-shell-equals-true
998
+ "S603", # subprocess-without-shell-equals-true
999
+ "S607", # start-process-with-partial-path
1000
+ # preview
1001
+ "S101", # assert
1002
+ # formatter
1003
+ "W191", # tab-indentation
1004
+ "E111", # indentation-with-invalid-multiple
1005
+ "E114", # indentation-with-invalid-multiple-comment
1006
+ "E117", # over-indented
1007
+ "COM812", # missing-trailing-comma
1008
+ "COM819", # prohibited-trailing-comma
1009
+ "ISC001", # single-line-implicit-string-concatenation
1010
+ "ISC002", # multi-line-implicit-string-concatenation
1011
+ )
1012
+ lint["preview"] = True
1013
+ select = get_array(lint, "select")
1014
+ selected_rules = [
1015
+ "RUF022", # unsorted-dunder-all
1016
+ "RUF029", # unused-async
1017
+ ]
1018
+ ensure_contains(select, "ALL", *selected_rules)
1019
+ extend_per_file_ignores = get_table(lint, "extend-per-file-ignores")
1020
+ test_py = get_array(extend_per_file_ignores, "test_*.py")
1021
+ test_py_rules = [
1022
+ "S101", # assert
1023
+ "SLF001", # private-member-access
1024
+ ]
1025
+ ensure_contains(test_py, *test_py_rules)
1026
+ ensure_not_contains(ignore, *selected_rules, *test_py_rules)
1027
+ bugbear = get_table(lint, "flake8-bugbear")
1028
+ extend_immutable_calls = get_array(bugbear, "extend-immutable-calls")
1029
+ ensure_contains(extend_immutable_calls, "typing.cast")
1030
+ tidy_imports = get_table(lint, "flake8-tidy-imports")
1031
+ tidy_imports["ban-relative-imports"] = "all"
1032
+ isort = get_table(lint, "isort")
1033
+ req_imps = get_array(isort, "required-imports")
1034
+ ensure_contains(req_imps, "from __future__ import annotations")
1035
+ isort["split-on-trailing-comma"] = False
1036
+
1037
+
1038
+ ##
1039
+
1040
+
1041
+ def check_versions() -> None:
1042
+ version = get_version_from_bumpversion_toml()
1043
+ try:
1044
+ set_version(version)
1045
+ except CalledProcessError:
1046
+ msg = f"Inconsistent versions; should be {version}"
1047
+ raise ValueError(msg) from None
1048
+
1049
+
1050
+ ##
1051
+
1052
+
1053
+ def get_python_package_name(
1054
+ *,
1055
+ package_name: str | None = SETTINGS.package_name,
1056
+ python_package_name: str | None = SETTINGS.python_package_name,
1057
+ ) -> str | None:
1058
+ if python_package_name is not None:
1059
+ return python_package_name
1060
+ if package_name is not None:
1061
+ return package_name.replace("-", "_")
1062
+ return None
1063
+
1064
+
1065
+ ##
1066
+
1067
+
1068
+ def get_version_from_bumpversion_toml(
1069
+ *, obj: TOMLDocument | str | None = None
1070
+ ) -> Version:
1071
+ match obj:
1072
+ case TOMLDocument() as doc:
1073
+ tool = get_table(doc, "tool")
1074
+ bumpversion = get_table(tool, "bumpversion")
1075
+ return parse_version(str(bumpversion["current_version"]))
1076
+ case str() as text:
1077
+ return get_version_from_bumpversion_toml(obj=tomlkit.parse(text))
1078
+ case None:
1079
+ with yield_bumpversion_toml() as doc:
1080
+ return get_version_from_bumpversion_toml(obj=doc)
1081
+ case never:
1082
+ assert_never(never)
1083
+
1084
+
1085
+ def get_version_from_git_show() -> Version:
1086
+ text = logged_run("git", "show", f"origin/master:{BUMPVERSION_TOML}", return_=True)
1087
+ return get_version_from_bumpversion_toml(obj=text.rstrip("\n"))
1088
+
1089
+
1090
+ def get_version_from_git_tag() -> Version:
1091
+ text = logged_run("git", "tag", "--points-at", "origin/master", return_=True)
1092
+ for line in text.splitlines():
1093
+ with suppress(ParseVersionError):
1094
+ return parse_version(line)
1095
+ msg = "No valid version from 'git tag'"
1096
+ raise ValueError(msg)
1097
+
1098
+
1099
+ ##
1100
+
1101
+
1102
+ def run_bump_my_version(*, modifications: MutableSet[Path] | None = None) -> None:
1103
+ def run_set_version(version: Version, /) -> None:
1104
+ LOGGER.info("Setting version to %s...", version)
1105
+ set_version(version)
1106
+ if modifications is not None:
1107
+ modifications.add(BUMPVERSION_TOML)
1108
+
1109
+ try:
1110
+ prev = get_version_from_git_tag()
1111
+ except (CalledProcessError, ValueError):
1112
+ try:
1113
+ prev = get_version_from_git_show()
1114
+ except (CalledProcessError, ParseVersionError, NonExistentKey):
1115
+ run_set_version(Version(0, 1, 0))
1116
+ return
1117
+ current = get_version_from_bumpversion_toml()
1118
+ patched = prev.bump_patch()
1119
+ if current not in {patched, prev.bump_minor(), prev.bump_major()}:
1120
+ run_set_version(patched)
1121
+
1122
+
1123
+ ##
1124
+
1125
+
1126
+ def run_pre_commit_update(*, modifications: MutableSet[Path] | None = None) -> None:
1127
+ cache = xdg_cache_home() / "conformalize" / get_repo_root().name
1128
+ try:
1129
+ text = cache.read_text()
1130
+ except FileNotFoundError:
1131
+ ...
1132
+ else:
1133
+ prev = ZonedDateTime.parse_iso(text.rstrip("\n"))
1134
+ if prev >= (get_now() - 12 * HOUR):
1135
+ return
1136
+ write_text(cache, get_now().format_iso())
1137
+ current = PRE_COMMIT_CONFIG_YAML.read_text()
1138
+ logged_run("pre-commit", "autoupdate", print=True)
1139
+ if (modifications is not None) and (PRE_COMMIT_CONFIG_YAML.read_text() != current):
1140
+ modifications.add(PRE_COMMIT_CONFIG_YAML)
1141
+
1142
+
1143
+ ##
1144
+
1145
+
1146
+ def run_ripgrep_and_replace(
1147
+ *,
1148
+ version: str = SETTINGS.python_version,
1149
+ modifications: MutableSet[Path] | None = None,
1150
+ ) -> None:
1151
+ result = ripgrep(
1152
+ "--files-with-matches",
1153
+ "--pcre2",
1154
+ "--type=py",
1155
+ rf'# requires-python = ">=(?!{version})\d+\.\d+"',
1156
+ )
1157
+ if result is None:
1158
+ return
1159
+ for path in result.splitlines():
1160
+ with yield_text_file(path, modifications=modifications) as context:
1161
+ context.output = sub(
1162
+ r'# requires-python = ">=\d+\.\d+"',
1163
+ rf'# requires-python = ">={version}"',
1164
+ context.input,
1165
+ flags=MULTILINE,
1166
+ )
1167
+
1168
+
1169
+ ##
1170
+
1171
+
1172
+ def set_version(version: Version, /) -> None:
1173
+ logged_run(
1174
+ "bump-my-version",
1175
+ "replace",
1176
+ "--new-version",
1177
+ str(version),
1178
+ str(BUMPVERSION_TOML),
1179
+ )
1180
+
1181
+
1182
+ ##
1183
+
1184
+
1185
+ def update_action_file_extensions(
1186
+ *, modifications: MutableSet[Path] | None = None
1187
+ ) -> None:
1188
+ try:
1189
+ paths = list(Path(".github").rglob("**/*.yml"))
1190
+ except FileNotFoundError:
1191
+ return
1192
+ for path in paths:
1193
+ new = path.with_suffix(".yaml")
1194
+ LOGGER.info("Renaming '%s' -> '%s'...", path, new)
1195
+ _ = path.rename(new)
1196
+ if modifications is not None:
1197
+ modifications.add(path)
1198
+
1199
+
1200
+ ##
1201
+
1202
+
1203
+ def update_action_versions(*, modifications: MutableSet[Path] | None = None) -> None:
1204
+ try:
1205
+ paths = list(Path(".github").rglob("**/*.yaml"))
1206
+ except FileNotFoundError:
1207
+ return
1208
+ versions = {
1209
+ "actions/checkout": "v6",
1210
+ "actions/setup-python": "v6",
1211
+ "astral-sh/ruff-action": "v3",
1212
+ "astral-sh/setup-uv": "v7",
1213
+ }
1214
+ for path, (action, version) in product(paths, versions.items()):
1215
+ text = sub(
1216
+ rf"^(\s*- uses: {action})@.+$",
1217
+ rf"\1@{version}",
1218
+ path.read_text(),
1219
+ flags=MULTILINE,
1220
+ )
1221
+ with yield_yaml_dict(path, modifications=modifications) as dict_:
1222
+ dict_.clear()
1223
+ dict_.update(YAML_INSTANCE.load(text))
1224
+
1225
+
1226
+ ##
1227
+
1228
+
1229
+ @contextmanager
1230
+ def yield_bumpversion_toml(
1231
+ *, modifications: MutableSet[Path] | None = None
1232
+ ) -> Iterator[TOMLDocument]:
1233
+ with yield_toml_doc(BUMPVERSION_TOML, modifications=modifications) as doc:
1234
+ tool = get_table(doc, "tool")
1235
+ bumpversion = get_table(tool, "bumpversion")
1236
+ bumpversion["allow_dirty"] = True
1237
+ bumpversion.setdefault("current_version", str(Version(0, 1, 0)))
1238
+ yield doc
1239
+
1240
+
1241
+ ##
1242
+
1243
+
1244
+ def yield_python_versions(
1245
+ version: str, /, *, max_: str = MAX_PYTHON_VERSION
1246
+ ) -> Iterator[str]:
1247
+ major, minor = _yield_python_version_tuple(version)
1248
+ max_major, max_minor = _yield_python_version_tuple(max_)
1249
+ if major != max_major:
1250
+ msg = f"Major versions must be equal; got {major} and {max_major}"
1251
+ raise ValueError(msg)
1252
+ if minor > max_minor:
1253
+ msg = f"Minor version must be at most {max_minor}; got {minor}"
1254
+ raise ValueError(msg)
1255
+ for i in range(minor, max_minor + 1):
1256
+ yield f"{major}.{i}"
1257
+
1258
+
1259
+ def _yield_python_version_tuple(version: str, /) -> tuple[int, int]:
1260
+ major, minor = extract_groups(r"^(\d+)\.(\d+)$", version)
1261
+ return int(major), int(minor)
1262
+
1263
+
1264
+ ##
1265
+
1266
+
1267
+ __all__ = [
1268
+ "add_bumpversion_toml",
1269
+ "add_coveragerc_toml",
1270
+ "add_envrc",
1271
+ "add_github_pull_request_yaml",
1272
+ "add_github_push_yaml",
1273
+ "add_gitignore",
1274
+ "add_pre_commit_config_yaml",
1275
+ "add_pyproject_toml",
1276
+ "add_pyrightconfig_json",
1277
+ "add_pytest_toml",
1278
+ "add_readme_md",
1279
+ "add_ruff_toml",
1280
+ "check_versions",
1281
+ "get_python_package_name",
1282
+ "get_version_from_bumpversion_toml",
1283
+ "get_version_from_git_show",
1284
+ "get_version_from_git_tag",
1285
+ "run_bump_my_version",
1286
+ "run_pre_commit_update",
1287
+ "run_ripgrep_and_replace",
1288
+ "set_version",
1289
+ "update_action_file_extensions",
1290
+ "update_action_versions",
1291
+ "yield_bumpversion_toml",
1292
+ "yield_python_versions",
1293
+ ]