pkgdx 0.5.0rc1__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.
pkgdx/__init__.py ADDED
@@ -0,0 +1,17 @@
1
+ """__init__ for pkgdx."""
2
+
3
+ import contextlib
4
+ from importlib import metadata
5
+ from pathlib import Path
6
+
7
+ from pkgdx import exceptions, standards
8
+ from pkgdx.logging import configure_pkg_logging
9
+
10
+ __all__: list[str] = ["exceptions", "standards"]
11
+
12
+ PKGDX_ROOT: Path = Path(__file__).parent
13
+
14
+ configure_pkg_logging()
15
+
16
+ with contextlib.suppress(metadata.PackageNotFoundError):
17
+ __version__: str = metadata.version("pkgdx")
pkgdx/__main__.py ADDED
@@ -0,0 +1,207 @@
1
+ """Main entry point for pkgdx pre-commit hooks and CLI."""
2
+
3
+ import argparse
4
+ import logging
5
+ import sys
6
+ import textwrap
7
+ from collections.abc import Callable
8
+ from typing import NoReturn
9
+
10
+ from pkgdx import __version__, _core, exceptions
11
+ from pkgdx.logging import CLI_CONSOLE, configure_cli_logging
12
+ from pkgdx.standards import _hooks
13
+ from pkgdx.standards._cli import add_subparser as add_standards_subparser
14
+
15
+ logger = logging.getLogger(__package__)
16
+
17
+
18
+ __all__: list[str] = [
19
+ "detect_secrets",
20
+ "main",
21
+ "mypy_typing",
22
+ "pymarkdown_lint",
23
+ "ruff_format",
24
+ "ruff_lint",
25
+ ]
26
+
27
+
28
+ # PRE-COMMIT ENTRYPOINTS [project.scripts]
29
+
30
+
31
+ def _run_hook(func: Callable[[list[str]], int]) -> NoReturn:
32
+ """Run a config hook, reporting `pkgdx` errors without a traceback.
33
+
34
+ Args:
35
+ func: The core hook logic, called with `sys.argv[1:]`.
36
+ """
37
+ try:
38
+ code = func(sys.argv[1:])
39
+ except exceptions.Error as err:
40
+ sys.stderr.write(f"pkgdx: {err}\n")
41
+ sys.exit(_core.ExitCode.EX_FAILURE)
42
+ sys.exit(code)
43
+
44
+
45
+ def ruff_format() -> NoReturn:
46
+ """Run ruff formatting with the configured settings.
47
+
48
+ NOTE: Relates to pre-commit hook ID `pkgdx-format`, with
49
+ entry point `pkgdx-format-hook`.
50
+ """
51
+ _run_hook(_hooks.core_ruff_format)
52
+
53
+
54
+ def ruff_lint() -> NoReturn:
55
+ """Run ruff linting with the configured settings.
56
+
57
+ NOTE: Relates to pre-commit hook ID `pkgdx-lint`, with
58
+ entry point `pkgdx-lint-hook`.
59
+ """
60
+ _run_hook(_hooks.core_ruff_lint)
61
+
62
+
63
+ def mypy_typing() -> NoReturn:
64
+ """Run mypy type checking with the configured settings.
65
+
66
+ NOTE: Relates to pre-commit hook ID `pkgdx-typing`, with
67
+ entry point `pkgdx-typing-hook`.
68
+ """
69
+ _run_hook(_hooks.core_mypy_typing)
70
+
71
+
72
+ def detect_secrets() -> NoReturn:
73
+ """Run detect-secrets with the provided arguments.
74
+
75
+ NOTE: Relates to pre-commit hook ID `pkgdx-secrets`, with
76
+ entry point `pkgdx-secrets-hook`.
77
+ """
78
+ sys.exit(_hooks.core_detect_secrets(sys.argv[1:]))
79
+
80
+
81
+ def pymarkdown_lint() -> NoReturn:
82
+ """Run pymarkdown linting with the configured settings.
83
+
84
+ NOTE: Relates to pre-commit hook ID `pkgdx-markdown`, with
85
+ entry point `pkgdx-markdown-hook`.
86
+ """
87
+ _run_hook(_hooks.core_pymarkdown_lint)
88
+
89
+
90
+ # MAIN CLI COMMANDS
91
+
92
+
93
+ class CLIArgumentParser(argparse.ArgumentParser):
94
+ """Custom CLI ArgumentParser."""
95
+
96
+ def error(self, message: str) -> NoReturn:
97
+ """Handle CLI errors by printing a formatted message and exiting.
98
+
99
+ Args:
100
+ message: The error message to display.
101
+ """
102
+
103
+ CLI_CONSOLE.print(f"[red]ERROR:[/red] [b]{message}[/b]", end="\n\n")
104
+ self.print_usage(sys.stderr)
105
+ CLI_CONSOLE.print(
106
+ f"\n[dim]Try [cyan]{self.prog} --help[/cyan].[/dim]",
107
+ )
108
+
109
+ sys.exit(_core.ExitCode.EX_USAGE)
110
+
111
+
112
+ def main() -> NoReturn:
113
+ """Provide a unified `pkgdx` CLI entrypoint - init."""
114
+
115
+ # Provides a base parser for shared arguments across subcommands
116
+ base_parser = CLIArgumentParser(add_help=False)
117
+ base_parser.add_argument(
118
+ "-h",
119
+ "--help",
120
+ action="help",
121
+ help="Show this help message and exit",
122
+ )
123
+
124
+ description = textwrap.dedent(
125
+ """pkgdx centralises creation and implementation of coding standards
126
+ across projects, providing a single source of truth for toolchain
127
+ configuration."""
128
+ )
129
+
130
+ epilog = textwrap.fill(
131
+ "For more information see https://gitlab.com/andyrids/pkgdx.",
132
+ width=79,
133
+ initial_indent="",
134
+ )
135
+
136
+ # Main parser for the CLI
137
+ parser = CLIArgumentParser(
138
+ prog=__package__,
139
+ description=textwrap.fill(description, width=79),
140
+ usage="USAGE:\n %(prog)s [options] <command>",
141
+ epilog=epilog,
142
+ add_help=False,
143
+ parents=[base_parser],
144
+ formatter_class=_core.CLIGFormatter,
145
+ )
146
+
147
+ parser._optionals.title = "OPTIONS"
148
+
149
+ parser.add_argument(
150
+ "-d",
151
+ "--debug",
152
+ action="store_true",
153
+ help="Show debug logging output",
154
+ )
155
+
156
+ parser.add_argument(
157
+ "-v",
158
+ "--version",
159
+ action="version",
160
+ version=f"v{__version__}",
161
+ help="Show program's version number and exit",
162
+ )
163
+
164
+ subparsers = parser.add_subparsers(
165
+ title="COMMANDS",
166
+ # Mitigate subparser usage display issue by explicitly setting prog
167
+ # https://github.com/python/cpython/issues/86463
168
+ prog=parser.prog,
169
+ dest="command",
170
+ required=False,
171
+ metavar="<command>",
172
+ )
173
+
174
+ add_standards_subparser(subparsers, base_parser)
175
+
176
+ args = parser.parse_args()
177
+
178
+ # Empty command (`pkgdx`) shows header and help
179
+ if args.command is None:
180
+ sys.stdout.write(
181
+ f"Package Developer Experience (DX) Toolkit [v{__version__}]\n\n"
182
+ )
183
+
184
+ # Prints; USAGE, description, OPTIONS, COMMANDS & epilog
185
+ parser.print_help()
186
+
187
+ sys.exit(_core.ExitCode.EX_OK)
188
+
189
+ # Configure logging based on debug flag
190
+ is_debug = args.debug
191
+ configure_cli_logging(logging.DEBUG if is_debug else logging.WARNING)
192
+
193
+ ctx = _core.CLIContext(args=args, console=CLI_CONSOLE, is_debug=is_debug)
194
+
195
+ try:
196
+ exit_code = int(args.func(ctx))
197
+ except exceptions.Error as err:
198
+ logger.error(str(err))
199
+ exit_code = _core.ExitCode.EX_FAILURE
200
+ except Exception:
201
+ logger.exception("Unexpected error")
202
+ exit_code = _core.ExitCode.EX_SYNTAX
203
+ sys.exit(exit_code)
204
+
205
+
206
+ if __name__ == "__main__":
207
+ main()
pkgdx/_core.py ADDED
@@ -0,0 +1,370 @@
1
+ """Core logic and file-system operations for the `pkgdx init` command."""
2
+
3
+ import argparse
4
+ import dataclasses
5
+ import logging
6
+ import os
7
+ import re
8
+ import subprocess
9
+ import sys
10
+ from collections.abc import Iterable
11
+ from enum import IntEnum
12
+ from itertools import chain
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ import tomlkit
17
+ from rich.console import Console
18
+ from tomlkit import TOMLDocument
19
+ from tomlkit.items import Table
20
+
21
+ from pkgdx import exceptions, standards
22
+ from pkgdx._types import HookBuiltin, HookLocal, HookRemote
23
+
24
+ logger = logging.getLogger(__package__)
25
+
26
+
27
+ class ExitCode(IntEnum):
28
+ """Exit codes for the CLI commands.
29
+
30
+ NOTE: Members can be passed to `sys.exit` as `int` type checks pass
31
+ due to `IntEnum` inheritance.
32
+ """
33
+
34
+ EX_OK = 0
35
+ EX_FAILURE = 1
36
+ EX_SYNTAX = 2
37
+ EX_USAGE = 64
38
+
39
+
40
+ @dataclasses.dataclass(frozen=True, slots=True)
41
+ class CLIContext:
42
+ """Centralized state for the CLI commands.
43
+
44
+ Attributes:
45
+ args: The parsed command-line arguments from argparse.
46
+ console: The Rich console instance used for terminal output.
47
+ is_debug: Flag indicating if the CLI is running in debug mode.
48
+ """
49
+
50
+ args: argparse.Namespace
51
+ console: Console
52
+ is_debug: bool = False
53
+
54
+
55
+ class CLIGFormatter(argparse.RawDescriptionHelpFormatter):
56
+ """Custom formatter for the CLI help output.
57
+
58
+ NOTE: Based on CLI guidance from https://clig.dev/.
59
+ """
60
+
61
+ def add_usage(
62
+ self,
63
+ usage: str | None,
64
+ actions: Iterable[argparse.Action],
65
+ groups: Iterable[argparse._MutuallyExclusiveGroup],
66
+ prefix: str | None = None,
67
+ ) -> None:
68
+ """Add a custom usage message to the CLI help output.
69
+
70
+ Args:
71
+ usage: The usage string to display.
72
+ actions: The list of actions for the parser.
73
+ groups: The list of mutually exclusive groups for the parser.
74
+ prefix: The prefix for the usage message.
75
+ """
76
+
77
+ super().add_usage(usage, actions, groups, prefix=prefix or "")
78
+
79
+
80
+ def get_git_toplevel() -> Path:
81
+ """Get the root path of the consuming Git repository.
82
+
83
+ Returns:
84
+ The Git top-level directory path.
85
+ """
86
+ try:
87
+ result = subprocess.run(
88
+ ["git", "rev-parse", "--show-toplevel"],
89
+ check=True,
90
+ text=True,
91
+ capture_output=True,
92
+ )
93
+ except subprocess.CalledProcessError as e:
94
+ msg = "Failed `git rev-parse --show-toplevel`"
95
+ raise exceptions.GitTopLevelError(msg) from e
96
+ else:
97
+ return Path(result.stdout.strip())
98
+
99
+
100
+ def get_project_root() -> Path:
101
+ """Get the root path of the consuming repo.
102
+
103
+ Returns:
104
+ The root path of the consuming repo.
105
+ """
106
+ cwd = Path.cwd().resolve()
107
+ for directory in chain([cwd], cwd.parents):
108
+ if (directory / "pyproject.toml").exists():
109
+ return directory
110
+
111
+ venv_parent = Path(sys.prefix).parent
112
+ if (venv_parent / "pyproject.toml").exists():
113
+ return venv_parent
114
+
115
+ msg = f"Cannot identify project root:- `{sys.prefix=}` | `{cwd=}`."
116
+ raise exceptions.ProjectRootNotFoundError(msg)
117
+
118
+
119
+ def install_prek_hooks(root: Path) -> None:
120
+ """Install pre-commit hooks if not installed.
121
+
122
+ Args:
123
+ root: The root path of the consuming repo.
124
+ """
125
+ precommit_config = root / ".git" / "hooks" / "pre-commit"
126
+ if precommit_config.exists():
127
+ logger.debug("Git shims already installed at `.git/hooks/pre-commit`")
128
+ return
129
+ try:
130
+ subprocess.run(
131
+ [sys.executable, "-m", "prek", "install"],
132
+ cwd=root,
133
+ capture_output=True,
134
+ check=True,
135
+ )
136
+ except (subprocess.CalledProcessError, FileNotFoundError):
137
+ logger.exception("Failed to install Git shims")
138
+ else:
139
+ logger.debug("Git shims installed at `.git/hooks/pre-commit`")
140
+
141
+
142
+ def update_prek_hooks(root: Path) -> None:
143
+ """Check for pre-commit hook updates.
144
+
145
+ NOTE: Updates the pre-commit hooks based on available tagged versions in
146
+ the remote `pkgdx` repo.
147
+
148
+ Args:
149
+ root: The root path of the consuming repo.
150
+ """
151
+ try:
152
+ subprocess.run(
153
+ [sys.executable, "-m", "prek", "update", "--check", "-v"],
154
+ cwd=root,
155
+ capture_output=True,
156
+ check=True,
157
+ text=True,
158
+ )
159
+ except subprocess.CalledProcessError as e:
160
+ msg = re.sub(r"\n\s|\n", "", e.stdout or e.stderr or "Unknown error")
161
+ match e.returncode:
162
+ case 1:
163
+ logger.warning(msg)
164
+ logger.warning("Update remote hook revisions - `prek update`")
165
+ case 2:
166
+ logger.error(msg)
167
+ case _:
168
+ logger.exception(msg)
169
+ except FileNotFoundError:
170
+ logger.exception("Check `prek.toml` config exists")
171
+ else:
172
+ logger.debug("Remote hook revisions are up-to-date")
173
+
174
+
175
+ def _create_repo_table(
176
+ doc: TOMLDocument, name: str, revision: str | None = None
177
+ ) -> Table:
178
+ """Create a new repository table in the pre-commit configuration.
179
+
180
+ Args:
181
+ doc: The TOMLDocument to parse.
182
+ name: The name of the repository.
183
+ revision: The revision of the repository.
184
+ """
185
+ new_repo = tomlkit.table()
186
+ new_repo["repo"] = name
187
+ if revision:
188
+ new_repo["rev"] = revision
189
+
190
+ new_repo.add(tomlkit.nl())
191
+
192
+ new_repo["hooks"] = tomlkit.aot()
193
+ doc["repos"].append(new_repo)
194
+
195
+ logger.debug("Created `[repo]` - %s | %s", name, revision)
196
+ return new_repo
197
+
198
+
199
+ def _get_repo_table(
200
+ doc: TOMLDocument, name: str, revision: str | None = None
201
+ ) -> tuple[Table, bool]:
202
+ """Get an existing or create a new repository table.
203
+
204
+ Args:
205
+ doc: The TOMLDocument to parse.
206
+ name: The name of the repository.
207
+ revision: The revision of the repository.
208
+
209
+ Returns:
210
+ A tuple containing the table and a boolean indicating any changes.
211
+ """
212
+ for repo in doc["repos"]:
213
+ if repo.get("repo") == name:
214
+ if "hooks" not in repo:
215
+ repo["hooks"] = tomlkit.aot()
216
+ return repo, True
217
+ return repo, False
218
+ return _create_repo_table(doc, name, revision), True
219
+
220
+
221
+ def _inject_missing_hooks(
222
+ table: dict[str, Any],
223
+ expected_hooks: list[HookBuiltin | HookLocal | HookRemote],
224
+ ) -> bool:
225
+ """Inject missing hooks into the repository table.
226
+
227
+ Args:
228
+ table: The repository table.
229
+ expected_hooks: The expected hooks to be present in the table.
230
+
231
+ Returns:
232
+ A boolean indicating if the table was modified.
233
+ """
234
+ changed = False
235
+ existing_id_set = {hook.get("id") for hook in table.get("hooks", [])}
236
+
237
+ for expected in expected_hooks:
238
+ if expected["id"] not in existing_id_set:
239
+ logger.debug("Detected missing hook `%s`", expected["id"])
240
+ hook_table = tomlkit.table()
241
+ for key, value in expected.items():
242
+ if isinstance(value, list):
243
+ array = tomlkit.array()
244
+ for item in value:
245
+ array.append(item)
246
+ hook_table[key] = array
247
+ else:
248
+ hook_table[key] = value
249
+
250
+ hook_table.add(tomlkit.nl())
251
+ table["hooks"].append(hook_table)
252
+ changed = True
253
+ return changed
254
+
255
+
256
+ def _validate_prek_repos(doc: TOMLDocument) -> None:
257
+ """Validate the consumer `prek.toml` `[[repos]]` shape.
258
+
259
+ NOTE: A missing or empty `repos` key stays valid - the template
260
+ injection path in `setup_prek_config` creates it.
261
+
262
+ Args:
263
+ doc: The parsed consumer `prek.toml` document.
264
+
265
+ Raises:
266
+ exceptions.PrekConfigError: On a malformed repo/hook entry.
267
+ """
268
+ repos = doc.get("repos")
269
+ if repos is None:
270
+ return
271
+ if not isinstance(repos, list):
272
+ msg = "`repos` must be an array of tables"
273
+ raise exceptions.PrekConfigError(msg)
274
+
275
+ for index, repo in enumerate(repos):
276
+ is_table = isinstance(repo, dict)
277
+ if not is_table or not isinstance(repo.get("repo"), str):
278
+ msg = f"`repos[{index}]` missing string `repo` key"
279
+ raise exceptions.PrekConfigError(msg)
280
+
281
+ hooks = repo.get("hooks", [])
282
+ if not isinstance(hooks, list):
283
+ msg = f"`repos[{index}].hooks` must be an array of tables"
284
+ raise exceptions.PrekConfigError(msg)
285
+
286
+ for hook_index, hook in enumerate(hooks):
287
+ is_table = isinstance(hook, dict)
288
+ if not is_table or not isinstance(hook.get("id"), str):
289
+ msg = (
290
+ f"`repos[{index}].hooks[{hook_index}]`"
291
+ " missing string `id` key"
292
+ )
293
+ raise exceptions.PrekConfigError(msg)
294
+
295
+
296
+ def setup_prek_config(root: Path, reset: bool = False) -> None:
297
+ """Configure pre-commit hooks in the consuming repo.
298
+
299
+ Args:
300
+ root: The root path of the consuming repo.
301
+ reset: Clean installation flag. Defaults to False.
302
+ """
303
+ changed = False
304
+ config_existing = root / "prek.toml"
305
+ config_template = standards.resolve_config(standards.PREK_CONFIG)
306
+
307
+ if reset or not config_existing.exists():
308
+ logger.debug("Creating `prek.toml` from template")
309
+ config_existing.write_text(config_template.read_text())
310
+ return
311
+
312
+ # Consuming project `prek.toml`
313
+ doc_consumer: TOMLDocument = tomlkit.parse(config_existing.read_text())
314
+ _validate_prek_repos(doc_consumer)
315
+
316
+ if "repos" not in doc_consumer or not doc_consumer.get("repos", []):
317
+ logger.debug("Existing `prek.toml` missing or has empty `[repos]`")
318
+ doc_consumer["repos"] = tomlkit.aot()
319
+ changed = True
320
+
321
+ # `pkgdx` template `prek.toml`
322
+ doc_pkgdx: TOMLDocument = tomlkit.parse(config_template.read_text())
323
+ for table in doc_pkgdx.get("repos", []):
324
+ name, rev = table.get("repo"), table.get("rev")
325
+ table_consumer, tchanged = _get_repo_table(doc_consumer, name, rev)
326
+
327
+ table_pkgdx = table.get("hooks", [])
328
+ hchanged = _inject_missing_hooks(table_consumer, table_pkgdx)
329
+
330
+ changed = changed or tchanged or hchanged
331
+
332
+ if changed:
333
+ config_text = tomlkit.dumps(doc_consumer)
334
+ # Normalise spacing between tables
335
+ config_text = re.sub(r"\n+\[\[repos", "\n\n[[repos", config_text)
336
+
337
+ config_existing.write_text(config_text)
338
+ logger.debug("Added missing pre-commit hooks to `prek.toml`")
339
+ else:
340
+ logger.debug("Existing `prek.toml` is correct & unchanged")
341
+
342
+
343
+ def create_secrets_baseline(root: Path) -> None:
344
+ """Run detect-secrets to create a `.secrets.baseline` file."""
345
+ secrets_baseline = root / ".secrets.baseline"
346
+ if secrets_baseline.exists():
347
+ logger.debug("Using existing `.secrets.baseline`")
348
+ return
349
+
350
+ logger.debug("Creating `.secrets.baseline` at %s", root)
351
+ try:
352
+ with open(secrets_baseline, "w") as f:
353
+ subprocess.run(
354
+ [
355
+ sys.executable,
356
+ "-m",
357
+ "detect_secrets",
358
+ "scan",
359
+ "--exclude-files",
360
+ r"(.*\.lock)",
361
+ ],
362
+ cwd=root,
363
+ env=os.environ | {"PYTHONUTF8": "1"},
364
+ stdout=f,
365
+ check=True,
366
+ )
367
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
368
+ secrets_baseline.unlink(missing_ok=True)
369
+ msg = "Error creating `.secrets.baseline`"
370
+ raise exceptions.Error(msg) from e
pkgdx/_types.py ADDED
@@ -0,0 +1,111 @@
1
+ """Type definitions for Pre-commit hooks and repositories in the TOML config.
2
+
3
+ This module defines `TypedDict` classes for representing remote, local and
4
+ builtin hooks and repositories as they appear in the Pre-commit configuration.
5
+ """
6
+
7
+ from typing import Literal, NotRequired, TypedDict
8
+
9
+
10
+ class HookRemote(TypedDict):
11
+ """Represents a Pre-commit remote hook entry in the TOML config.
12
+
13
+ Attributes:
14
+ id: The unique identifier for the remote hook.
15
+ name: The name of the remote hook.
16
+ entry: The entry point for the remote hook.
17
+ args: The list of arguments for the remote hook.
18
+ language: The programming language of the remote hook.
19
+ types_or: The list of file types the hook applies to.
20
+ exclude: The pattern for files to exclude from the hook.
21
+ require_serial: Flag indicating if the hook requires serial execution.
22
+ pass_filenames: Flag indicating if filenames should be passed to the
23
+ hook.
24
+ """
25
+
26
+ id: str
27
+ name: NotRequired[str]
28
+ entry: NotRequired[str]
29
+ args: NotRequired[list[str]]
30
+ language: NotRequired[str]
31
+ types_or: NotRequired[list[str]]
32
+ exclude: NotRequired[str]
33
+ require_serial: NotRequired[bool]
34
+ pass_filenames: NotRequired[bool]
35
+
36
+
37
+ class RepoRemote(TypedDict):
38
+ """Represents a Pre-commit remote repo entry in the TOML config.
39
+
40
+ Attributes:
41
+ repo: The URL of the remote repository.
42
+ rev: The revision of the remote repository.
43
+ hooks: A list of remote hooks associated with the repository.
44
+ """
45
+
46
+ repo: str
47
+ rev: NotRequired[str]
48
+ hooks: list[HookRemote]
49
+
50
+
51
+ class HookLocal(TypedDict):
52
+ """Represents a Pre-commit local hook entry in the TOML config.
53
+
54
+ Attributes:
55
+ id: The unique identifier for the local hook.
56
+ name: The name of the local hook.
57
+ entry: The entry point for the local hook.
58
+ args: The list of arguments for the local hook.
59
+ language: The programming language of the local hook.
60
+ types_or: The list of file types the hook applies to.
61
+ exclude: The pattern for files to exclude from the hook.
62
+ require_serial: Flag indicating if the hook requires serial execution.
63
+ pass_filenames: Flag indicating if filenames should be passed to the
64
+ hook.
65
+ """
66
+
67
+ id: str
68
+ name: str
69
+ entry: str
70
+ args: NotRequired[list[str]]
71
+ language: str
72
+ types_or: NotRequired[list[str]]
73
+ exclude: NotRequired[str]
74
+ require_serial: NotRequired[bool]
75
+ pass_filenames: NotRequired[bool]
76
+
77
+
78
+ class RepoLocal(TypedDict):
79
+ """Represents a Pre-commit local repo entry in the TOML config.
80
+
81
+ Attributes:
82
+ repo: The literal string "local" indicating a local repository.
83
+ rev: The revision of the local repository.
84
+ hooks: A list of local hooks associated with the repository.
85
+ """
86
+
87
+ repo: Literal["local"]
88
+ rev: NotRequired[str]
89
+ hooks: list[HookLocal]
90
+
91
+
92
+ class HookBuiltin(TypedDict):
93
+ """Represents a Pre-commit builtin hook entry in the TOML config.
94
+
95
+ Attributes:
96
+ id: The unique identifier for the builtin hook.
97
+ """
98
+
99
+ id: str
100
+
101
+
102
+ class RepoBuiltin(TypedDict):
103
+ """Represents a Pre-commit builtin repo entry in the TOML config.
104
+
105
+ Attributes:
106
+ repo: The literal string "builtin" indicating a builtin repository.
107
+ hooks: A list of builtin hooks associated with the repository.
108
+ """
109
+
110
+ repo: Literal["builtin"]
111
+ hooks: list[HookBuiltin]