pkgdx 0.5.0__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/_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]
pkgdx/exceptions.py ADDED
@@ -0,0 +1,37 @@
1
+ """Custom exceptions for `pkgdx`."""
2
+
3
+
4
+ class Error(Exception):
5
+ """Base-class for all exceptions raised by `pkgdx`."""
6
+
7
+
8
+ class ProjectRootNotFoundError(Error):
9
+ """Raised when the project root cannot be determined."""
10
+
11
+
12
+ class PrekConfigError(Error):
13
+ """Raised on a malformed consumer `prek.toml` configuration."""
14
+
15
+
16
+ class PrekRepoRevisionError(Error):
17
+ """Raised on missing `rev` key in Prek config."""
18
+
19
+
20
+ class PrekRepoURLMissingError(Error):
21
+ """Raised on missing `repo` key in Prek config."""
22
+
23
+
24
+ class ProjectRepoURLMissingError(Error):
25
+ """Raised on missing `repository` key in `pkgdx` metadata."""
26
+
27
+
28
+ class GitTopLevelError(Error):
29
+ """Raised on failed `git rev-parse --show-toplevel` command."""
30
+
31
+
32
+ class RichHandlerNotFound(Error):
33
+ """Raised on missing Rich logging handler."""
34
+
35
+
36
+ class ConfigDirError(Error):
37
+ """Raised on an invalid `PKGDX_CONFIG_DIR`."""
@@ -0,0 +1,15 @@
1
+ """__init__ for pkgdx logging module."""
2
+
3
+ from ._logging import (
4
+ CLI_CONSOLE,
5
+ GLOBAL_CONSOLE,
6
+ configure_cli_logging,
7
+ configure_pkg_logging,
8
+ )
9
+
10
+ __all__: list[str] = [
11
+ "CLI_CONSOLE",
12
+ "GLOBAL_CONSOLE",
13
+ "configure_cli_logging",
14
+ "configure_pkg_logging",
15
+ ]
@@ -0,0 +1,49 @@
1
+ """Logging configuration for `pkgdx`.
2
+
3
+ The Rich global `Console` (`GLOBAL_CONSOLE`) is configured to use STDERR
4
+ (`reconfigure(stderr=True)`) for logging and progress display. A separate
5
+ `Console` instance (`CLI_CONSOLE`) is used for CLI output on STDOUT.
6
+
7
+ When a `RichHandler` (logging handler) and `Status`/`Progress` share the same
8
+ `Console` instance (`GLOBAL_CONSOLE`), the output can be automatically
9
+ coordinated by Rich.
10
+
11
+ NOTE: When `RichHandler` is initialised without a `Console` instance, it will
12
+ use the global `Console` returned by `get_console()` (`GLOBAL_CONSOLE`).
13
+ """
14
+
15
+ import logging.config
16
+ import pathlib
17
+ import sys
18
+ import tomllib
19
+
20
+ from rich import get_console, reconfigure
21
+ from rich.console import Console
22
+
23
+ CONFIG_PATH = pathlib.Path(__file__).parent / "config.toml"
24
+ CONFIG_STR = CONFIG_PATH.read_text(encoding="utf-8")
25
+
26
+ reconfigure(stderr=True)
27
+ GLOBAL_CONSOLE = get_console()
28
+ CLI_CONSOLE = Console(force_terminal=sys.stdout.isatty())
29
+
30
+
31
+ def configure_cli_logging(level: int = logging.WARNING) -> None:
32
+ """Configure CLI logging using the `config.toml` settings.
33
+
34
+ NOTE: CLI logs are prevented from propagating to the root logger to avoid
35
+ duplication.
36
+
37
+ Args:
38
+ level: The desired logging level. Defaults to `logging.WARNING`.
39
+ """
40
+ logging.config.dictConfig(tomllib.loads(CONFIG_STR))
41
+
42
+ logger = logging.getLogger("pkgdx")
43
+ logger.setLevel(level)
44
+ logger.propagate = False
45
+
46
+
47
+ def configure_pkg_logging() -> None:
48
+ """Configure package logging using a `NullHandler`."""
49
+ logging.getLogger("pkgdx").addHandler(logging.NullHandler())
@@ -0,0 +1,48 @@
1
+ version = 1
2
+ incremental = false
3
+ disable_existing_loggers = false
4
+
5
+ [formatters.stdlib-standard-format]
6
+ format = "%(levelname)s: %(message)s"
7
+ datefmt = "%Y-%m-%dT%H:%M:%S%z"
8
+
9
+ [formatters.rich-standard-format]
10
+ format = "%(message)s"
11
+ datefmt = "[%Y-%m-%dT%H:%M:%S%z]"
12
+
13
+ [formatters.rich-verbose-format]
14
+ format = "[%(funcName)s] %(message)s"
15
+ datefmt = "[%Y-%m-%dT%H:%M:%S%z]"
16
+
17
+ [formatters.rich-extra-verbose-format]
18
+ format = "[%(levelname)s|%(module)s|L%(lineno)d] %(asctime)s: %(message)s"
19
+ datefmt = "%Y-%m-%dT%H:%M:%S%z"
20
+
21
+ [filters]
22
+ # Unused, but kept to match expected `dictConfig` structure
23
+
24
+ [handlers.rich-standard-handler]
25
+ class = "rich.logging.RichHandler"
26
+ formatter = "rich-standard-format"
27
+ show_path = false
28
+ rich_tracebacks = true
29
+ markup = true
30
+ omit_repeated_times = true
31
+
32
+ [handlers.rich-verbose-handler]
33
+ class = "rich.logging.RichHandler"
34
+ formatter = "rich-verbose-format"
35
+ show_path = true
36
+ rich_tracebacks = true
37
+ markup = false
38
+ omit_repeated_times = false
39
+
40
+ [handlers.stdlib-standard-handler]
41
+ class = "logging.StreamHandler"
42
+ formatter = "stdlib-standard-format"
43
+ stream = "ext://sys.stderr"
44
+
45
+ [loggers.pkgdx]
46
+ level = "WARNING"
47
+ handlers = ["rich-verbose-handler"]
48
+ propagate = true
pkgdx/py.typed ADDED
File without changes
@@ -0,0 +1,182 @@
1
+ """__init__ for `pkgdx.standards` module.
2
+
3
+ Provides access to configuration files for supported tools such as; `mypy`,
4
+ `prek`, `pymarkdown` and `ruff`.
5
+ """
6
+
7
+ import logging
8
+ import os
9
+ import sys
10
+ from functools import lru_cache
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ logger = logging.getLogger(__package__)
15
+
16
+ MODULE_ROOT: Path = Path(__file__).parent
17
+ MYPY_CONFIG: Path = MODULE_ROOT / "mypy.ini"
18
+ PREK_CONFIG: Path = MODULE_ROOT / "hooks.toml"
19
+ PYMARKDOWN_CONFIG: Path = MODULE_ROOT / "pymarkdown.toml"
20
+ RUFF_CONFIG: Path = MODULE_ROOT / "ruff.toml"
21
+
22
+ CONFIG_DIR_ENV: str = "PKGDX_CONFIG_DIR"
23
+ BUNDLED_CONFIGS: tuple[Path, ...] = (
24
+ MYPY_CONFIG,
25
+ PREK_CONFIG,
26
+ PYMARKDOWN_CONFIG,
27
+ RUFF_CONFIG,
28
+ )
29
+
30
+
31
+ def user_config_dir() -> Path:
32
+ """Get the user config directory.
33
+
34
+ The environment is read on every call; nothing is cached. The platform
35
+ directory is returned even if it does not exist.
36
+
37
+ Raises:
38
+ ConfigDirError: If `PKGDX_CONFIG_DIR` is set but not a directory.
39
+
40
+ Returns:
41
+ The `PKGDX_CONFIG_DIR` path, else the platform config directory.
42
+ """
43
+ from pkgdx.exceptions import ConfigDirError
44
+
45
+ env_value = os.environ.get(CONFIG_DIR_ENV)
46
+ if env_value:
47
+ env_dir = Path(env_value).expanduser()
48
+ if not env_dir.is_dir():
49
+ msg = f"`{CONFIG_DIR_ENV}` is not an existing directory: {env_dir}"
50
+ raise ConfigDirError(msg)
51
+ return env_dir
52
+
53
+ appdata = os.environ.get("APPDATA")
54
+ if sys.platform == "win32" and appdata:
55
+ return Path(appdata) / "pkgdx"
56
+
57
+ xdg = os.environ.get("XDG_CONFIG_HOME")
58
+ if xdg and Path(xdg).is_absolute():
59
+ return Path(xdg) / "pkgdx"
60
+ return Path.home() / ".config" / "pkgdx"
61
+
62
+
63
+ def resolve_config(bundled: Path) -> Path:
64
+ """Resolve a config file, preferring a user override.
65
+
66
+ Args:
67
+ bundled: A bundled config path (for example `RUFF_CONFIG`).
68
+
69
+ Raises:
70
+ ConfigDirError: If `PKGDX_CONFIG_DIR` is set but not a directory.
71
+
72
+ Returns:
73
+ The user config file if it exists, else the bundled file.
74
+ """
75
+ user_file = user_config_dir() / bundled.name
76
+ if user_file.is_file():
77
+ logger.debug("Using user config %s", user_file)
78
+ return user_file
79
+ logger.debug("Using bundled config %s", bundled)
80
+ return bundled
81
+
82
+
83
+ @lru_cache(maxsize=1)
84
+ def _load_prek_config() -> dict[str, Any]:
85
+ """Load Prek config.
86
+
87
+ Returns:
88
+ The Prek hooks TOML as a dict."""
89
+ import tomllib
90
+
91
+ # Bundled only: validates the package's own template, never a user copy
92
+ with PREK_CONFIG.open("rb") as f:
93
+ return tomllib.load(f)
94
+
95
+
96
+ @lru_cache(maxsize=1)
97
+ def get_pkgdx_repository() -> str:
98
+ """Get repository URL from pkgdx metadata.
99
+
100
+ Raises:
101
+ ProjectRepoURLMissingError: On missing repository URL in metadata.
102
+
103
+ Returns:
104
+ Repository URL.
105
+ """
106
+ import urllib.parse
107
+ from importlib.metadata import metadata
108
+
109
+ from pkgdx.exceptions import ProjectRepoURLMissingError
110
+
111
+ pkgdx_metadata = metadata("pkgdx")
112
+
113
+ entry: str
114
+ for entry in pkgdx_metadata.get_all("Project-URL", failobj=[]):
115
+ if entry.lower().startswith("repository"):
116
+ _, URL = entry.split(", ", 1)
117
+ parsed = urllib.parse.urlparse(URL)
118
+ if parsed.scheme and parsed.netloc:
119
+ return URL
120
+ msg = "Missing repository URL in `pkgdx` metadata (`pyproject.toml`)"
121
+ raise ProjectRepoURLMissingError(msg)
122
+
123
+
124
+ @lru_cache(maxsize=1)
125
+ def get_config_revision() -> str:
126
+ """Get repo revision from Prek config.
127
+
128
+ Raises:
129
+ PrekRepoRevisionError: On missing revision value.
130
+
131
+ Returns:
132
+ Revision tag.
133
+ """
134
+ from pkgdx.exceptions import PrekRepoRevisionError
135
+
136
+ config = _load_prek_config()
137
+ REPO_URL: str = get_pkgdx_repository()
138
+
139
+ for repo in config.get("repos", []):
140
+ if repo.get("repo") == REPO_URL:
141
+ revision: str | None = repo.get("rev", None)
142
+ if revision:
143
+ return revision
144
+ msg = "Missing `rev` key in Prek config"
145
+ raise PrekRepoRevisionError(msg)
146
+ msg = "`pkgdx` metadata URL missing/mismatch for Prek config"
147
+ raise PrekRepoRevisionError(msg)
148
+
149
+
150
+ @lru_cache(maxsize=1)
151
+ def get_config_repository() -> str:
152
+ """Get repo URL from Prek config.
153
+
154
+ Raises:
155
+ ProjectRepoURLMissingError: On missing repository URL.
156
+
157
+ Returns:
158
+ Repository URL.
159
+ """
160
+ from pkgdx.exceptions import ProjectRepoURLMissingError
161
+
162
+ config = _load_prek_config()
163
+ REPO_URL = get_pkgdx_repository()
164
+
165
+ for repo in config.get("repos", []):
166
+ if repo.get("repo") == REPO_URL:
167
+ return REPO_URL
168
+
169
+ msg = "`pkgdx` repository URL from metadata not found in Prek config"
170
+ raise ProjectRepoURLMissingError(msg)
171
+
172
+
173
+ __all__: list[str] = [
174
+ "BUNDLED_CONFIGS",
175
+ "CONFIG_DIR_ENV",
176
+ "MYPY_CONFIG",
177
+ "PREK_CONFIG",
178
+ "PYMARKDOWN_CONFIG",
179
+ "RUFF_CONFIG",
180
+ "resolve_config",
181
+ "user_config_dir",
182
+ ]