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/__init__.py +17 -0
- pkgdx/__main__.py +207 -0
- pkgdx/_core.py +649 -0
- pkgdx/_types.py +111 -0
- pkgdx/exceptions.py +37 -0
- pkgdx/logging/__init__.py +15 -0
- pkgdx/logging/_logging.py +49 -0
- pkgdx/logging/config.toml +48 -0
- pkgdx/py.typed +0 -0
- pkgdx/standards/__init__.py +182 -0
- pkgdx/standards/_cli.py +380 -0
- pkgdx/standards/_hooks.py +53 -0
- pkgdx/standards/hooks.toml +50 -0
- pkgdx/standards/mypy.ini +44 -0
- pkgdx/standards/pymarkdown.toml +9 -0
- pkgdx/standards/ruff.toml +57 -0
- pkgdx-0.5.0.dist-info/METADATA +277 -0
- pkgdx-0.5.0.dist-info/RECORD +22 -0
- pkgdx-0.5.0.dist-info/WHEEL +4 -0
- pkgdx-0.5.0.dist-info/entry_points.txt +7 -0
- pkgdx-0.5.0.dist-info/licenses/COPYRIGHT +15 -0
- pkgdx-0.5.0.dist-info/licenses/LICENSE +201 -0
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
|
+
]
|