pycmdcheck 0.1.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.
- pycmdcheck/__init__.py +104 -0
- pycmdcheck/__main__.py +6 -0
- pycmdcheck/ast_cache.py +45 -0
- pycmdcheck/checks/__init__.py +1 -0
- pycmdcheck/checks/base.py +143 -0
- pycmdcheck/checks/build.py +101 -0
- pycmdcheck/checks/changelog.py +101 -0
- pycmdcheck/checks/ci.py +87 -0
- pycmdcheck/checks/citation.py +102 -0
- pycmdcheck/checks/community.py +108 -0
- pycmdcheck/checks/dependencies.py +309 -0
- pycmdcheck/checks/docs.py +305 -0
- pycmdcheck/checks/doctests.py +129 -0
- pycmdcheck/checks/formatting.py +200 -0
- pycmdcheck/checks/imports.py +239 -0
- pycmdcheck/checks/license.py +304 -0
- pycmdcheck/checks/linting.py +304 -0
- pycmdcheck/checks/metadata.py +233 -0
- pycmdcheck/checks/py_typed.py +207 -0
- pycmdcheck/checks/python_versions.py +188 -0
- pycmdcheck/checks/structure.py +282 -0
- pycmdcheck/checks/tests.py +230 -0
- pycmdcheck/checks/typing.py +257 -0
- pycmdcheck/checks/urls.py +124 -0
- pycmdcheck/checks/version.py +184 -0
- pycmdcheck/cli.py +296 -0
- pycmdcheck/config.py +194 -0
- pycmdcheck/constants.py +29 -0
- pycmdcheck/discovery.py +127 -0
- pycmdcheck/package_layout.py +234 -0
- pycmdcheck/profiles.py +158 -0
- pycmdcheck/py.typed +0 -0
- pycmdcheck/pyproject_reader.py +226 -0
- pycmdcheck/results.py +320 -0
- pycmdcheck/runner.py +236 -0
- pycmdcheck/subprocess_runner.py +166 -0
- pycmdcheck-0.1.0.dist-info/METADATA +346 -0
- pycmdcheck-0.1.0.dist-info/RECORD +41 -0
- pycmdcheck-0.1.0.dist-info/WHEEL +4 -0
- pycmdcheck-0.1.0.dist-info/entry_points.txt +24 -0
- pycmdcheck-0.1.0.dist-info/licenses/LICENSE +21 -0
pycmdcheck/discovery.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Plugin discovery via entry points.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to discover and load check plugins
|
|
4
|
+
registered via Python entry points. Third-party packages can register
|
|
5
|
+
custom checks by adding entries to the ``pycmdcheck.checks`` entry point
|
|
6
|
+
group in their pyproject.toml.
|
|
7
|
+
|
|
8
|
+
Example entry point registration in pyproject.toml:
|
|
9
|
+
|
|
10
|
+
[project.entry-points."pycmdcheck.checks"]
|
|
11
|
+
my_check = "my_package.checks:MyCheck"
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import functools
|
|
15
|
+
import logging
|
|
16
|
+
from importlib.metadata import entry_points
|
|
17
|
+
from typing import TYPE_CHECKING
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from pycmdcheck.checks.base import Check
|
|
23
|
+
|
|
24
|
+
ENTRY_POINT_GROUP = "pycmdcheck.checks"
|
|
25
|
+
"""Entry point group name for check plugins."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@functools.lru_cache(maxsize=1)
|
|
29
|
+
def discover_checks() -> dict[str, type["Check"]]:
|
|
30
|
+
"""Discover all registered checks via entry points.
|
|
31
|
+
|
|
32
|
+
Scans the ``pycmdcheck.checks`` entry point group and loads all
|
|
33
|
+
registered check classes. Built-in checks are registered in the
|
|
34
|
+
package's pyproject.toml, and third-party packages can register
|
|
35
|
+
additional checks.
|
|
36
|
+
|
|
37
|
+
If a check fails to load (e.g., due to an import error), a
|
|
38
|
+
UserWarning is issued but discovery continues with other checks.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
A dictionary mapping check names (strings) to check classes.
|
|
42
|
+
The check classes implement the Check protocol from
|
|
43
|
+
pycmdcheck.checks.base.
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
Discover all available checks:
|
|
47
|
+
|
|
48
|
+
>>> checks = discover_checks()
|
|
49
|
+
>>> "metadata" in checks
|
|
50
|
+
True
|
|
51
|
+
>>> "tests" in checks
|
|
52
|
+
True
|
|
53
|
+
|
|
54
|
+
Get a specific check class:
|
|
55
|
+
|
|
56
|
+
>>> checks = discover_checks()
|
|
57
|
+
>>> MetadataCheck = checks["metadata"]
|
|
58
|
+
>>> instance = MetadataCheck()
|
|
59
|
+
>>> instance.name
|
|
60
|
+
'metadata'
|
|
61
|
+
"""
|
|
62
|
+
checks: dict[str, type[Check]] = {}
|
|
63
|
+
|
|
64
|
+
eps = entry_points(group=ENTRY_POINT_GROUP)
|
|
65
|
+
for ep in eps:
|
|
66
|
+
try:
|
|
67
|
+
check_class = ep.load()
|
|
68
|
+
checks[ep.name] = check_class
|
|
69
|
+
except (ImportError, AttributeError, TypeError) as e:
|
|
70
|
+
# Log warning but continue with other checks
|
|
71
|
+
logger.warning(
|
|
72
|
+
"Failed to load check '%s' from %s: %s",
|
|
73
|
+
ep.name,
|
|
74
|
+
ep.value,
|
|
75
|
+
e,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
logger.debug("Discovered %d checks: %s", len(checks), ", ".join(sorted(checks)))
|
|
79
|
+
return checks
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def list_available_checks() -> list[tuple[str, str]]:
|
|
83
|
+
"""List all available checks with their descriptions.
|
|
84
|
+
|
|
85
|
+
Discovers all registered checks and returns their names and
|
|
86
|
+
descriptions in a format suitable for display (e.g., in CLI help).
|
|
87
|
+
Results are sorted alphabetically by check name.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A list of (name, description) tuples, sorted alphabetically
|
|
91
|
+
by name. Each tuple contains:
|
|
92
|
+
|
|
93
|
+
- name: The check's unique identifier (str)
|
|
94
|
+
- description: Human-readable description of what the check does (str)
|
|
95
|
+
|
|
96
|
+
If a check's description cannot be retrieved, "No description
|
|
97
|
+
available" is used as a fallback.
|
|
98
|
+
|
|
99
|
+
Examples:
|
|
100
|
+
List all checks for display:
|
|
101
|
+
|
|
102
|
+
>>> checks = list_available_checks()
|
|
103
|
+
>>> len(checks) > 0
|
|
104
|
+
True
|
|
105
|
+
>>> name, description = checks[0]
|
|
106
|
+
>>> isinstance(name, str) and isinstance(description, str)
|
|
107
|
+
True
|
|
108
|
+
|
|
109
|
+
Display in CLI:
|
|
110
|
+
|
|
111
|
+
>>> for name, desc in list_available_checks():
|
|
112
|
+
... print(f" {name}: {desc}") # doctest: +SKIP
|
|
113
|
+
docs: Check documentation presence
|
|
114
|
+
imports: Check that package imports work
|
|
115
|
+
...
|
|
116
|
+
"""
|
|
117
|
+
checks = discover_checks()
|
|
118
|
+
result: list[tuple[str, str]] = []
|
|
119
|
+
|
|
120
|
+
for name, check_class in sorted(checks.items()):
|
|
121
|
+
try:
|
|
122
|
+
description = getattr(check_class, "description", "No description")
|
|
123
|
+
result.append((name, description))
|
|
124
|
+
except (AttributeError, TypeError):
|
|
125
|
+
result.append((name, "No description available"))
|
|
126
|
+
|
|
127
|
+
return result
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Shared utility for discovering Python package layout and files.
|
|
2
|
+
|
|
3
|
+
This module provides the :class:`PackageLayout` class, which consolidates
|
|
4
|
+
all package/directory discovery logic into a single, reusable component.
|
|
5
|
+
It replaces duplicated ``_find_package_dir``, ``_get_local_packages``, and
|
|
6
|
+
hardcoded exclusion sets that were previously scattered across check modules.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
# ── Shared exclusion sets ─────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
EXCLUDED_DIRS: frozenset[str] = frozenset(
|
|
19
|
+
{
|
|
20
|
+
"venv",
|
|
21
|
+
".venv",
|
|
22
|
+
"env",
|
|
23
|
+
".env",
|
|
24
|
+
"build",
|
|
25
|
+
"dist",
|
|
26
|
+
".git",
|
|
27
|
+
"__pycache__",
|
|
28
|
+
".tox",
|
|
29
|
+
".nox",
|
|
30
|
+
".mypy_cache",
|
|
31
|
+
".pytest_cache",
|
|
32
|
+
".ruff_cache",
|
|
33
|
+
"node_modules",
|
|
34
|
+
".eggs",
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
"""Directories that should always be skipped when walking a project tree."""
|
|
38
|
+
|
|
39
|
+
NON_PACKAGE_DIRS: frozenset[str] = frozenset(
|
|
40
|
+
{
|
|
41
|
+
"tests",
|
|
42
|
+
"test",
|
|
43
|
+
"docs",
|
|
44
|
+
"doc",
|
|
45
|
+
"build",
|
|
46
|
+
"dist",
|
|
47
|
+
"venv",
|
|
48
|
+
".venv",
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
"""Top-level directories that are never real packages in a flat layout."""
|
|
52
|
+
|
|
53
|
+
NON_MODULE_FILES: frozenset[str] = frozenset(
|
|
54
|
+
{
|
|
55
|
+
"setup.py",
|
|
56
|
+
"conftest.py",
|
|
57
|
+
"noxfile.py",
|
|
58
|
+
"tasks.py",
|
|
59
|
+
"manage.py",
|
|
60
|
+
"versioneer.py",
|
|
61
|
+
"__init__.py",
|
|
62
|
+
"__main__.py",
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
"""Top-level ``.py`` files that are tooling/scripts, not the package module."""
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class PackageLayout:
|
|
69
|
+
"""Discover and expose the layout of a Python project.
|
|
70
|
+
|
|
71
|
+
Supports both *src/* layout and flat layout. Filesystem scanning is
|
|
72
|
+
deferred until the first property access (lazy discovery).
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
project_root: Path to the project root (the directory containing
|
|
76
|
+
``pyproject.toml`` or equivalent).
|
|
77
|
+
package_name: Optional hint for the primary package name. When
|
|
78
|
+
given, it is used to select :attr:`primary_package` from the
|
|
79
|
+
discovered packages.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
project_root: Path,
|
|
85
|
+
*,
|
|
86
|
+
package_name: str | None = None,
|
|
87
|
+
) -> None:
|
|
88
|
+
self._root = project_root
|
|
89
|
+
self._package_name = package_name
|
|
90
|
+
|
|
91
|
+
# Lazily populated by ``_discover()``
|
|
92
|
+
self._is_src_layout: bool | None = None
|
|
93
|
+
self._package_dirs: list[Path] | None = None
|
|
94
|
+
|
|
95
|
+
# ── Public properties ─────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def is_src_layout(self) -> bool:
|
|
99
|
+
"""Return ``True`` if the project uses a ``src/`` layout."""
|
|
100
|
+
if self._is_src_layout is None:
|
|
101
|
+
self._discover()
|
|
102
|
+
assert self._is_src_layout is not None
|
|
103
|
+
return self._is_src_layout
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def package_dirs(self) -> list[Path]:
|
|
107
|
+
"""Return all package directories that contain ``__init__.py``."""
|
|
108
|
+
if self._package_dirs is None:
|
|
109
|
+
self._discover()
|
|
110
|
+
assert self._package_dirs is not None
|
|
111
|
+
return list(self._package_dirs)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def primary_package(self) -> Path | None:
|
|
115
|
+
"""Return the main package directory.
|
|
116
|
+
|
|
117
|
+
If *package_name* was given at construction time and a matching
|
|
118
|
+
package exists, that package is returned. Otherwise the first
|
|
119
|
+
discovered package is returned (or ``None`` if no packages exist).
|
|
120
|
+
"""
|
|
121
|
+
dirs = self.package_dirs
|
|
122
|
+
if not dirs:
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
if self._package_name:
|
|
126
|
+
for d in dirs:
|
|
127
|
+
if d.name == self._package_name:
|
|
128
|
+
return d
|
|
129
|
+
|
|
130
|
+
return dirs[0]
|
|
131
|
+
|
|
132
|
+
# ── Public methods ────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
def python_files(self, *, include_tests: bool = False) -> list[Path]:
|
|
135
|
+
"""Return all ``.py`` files inside discovered packages.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
include_tests: When ``False`` (the default), files under
|
|
139
|
+
directories named ``tests`` or ``test`` are excluded.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
Sorted list of ``.py`` file paths.
|
|
143
|
+
"""
|
|
144
|
+
result: list[Path] = []
|
|
145
|
+
for pkg_dir in self.package_dirs:
|
|
146
|
+
for py_file in pkg_dir.rglob("*.py"):
|
|
147
|
+
parts = set(py_file.relative_to(pkg_dir).parts)
|
|
148
|
+
# Always skip globally excluded dirs
|
|
149
|
+
if parts & EXCLUDED_DIRS:
|
|
150
|
+
continue
|
|
151
|
+
if not include_tests and parts & {"tests", "test"}:
|
|
152
|
+
continue
|
|
153
|
+
result.append(py_file)
|
|
154
|
+
|
|
155
|
+
# Also filter based on the full path parts (catches excluded dirs
|
|
156
|
+
# that sit *above* the package dir, e.g. a .venv ancestor).
|
|
157
|
+
result = [
|
|
158
|
+
f for f in result if not any(part in EXCLUDED_DIRS for part in f.parts)
|
|
159
|
+
]
|
|
160
|
+
return sorted(result)
|
|
161
|
+
|
|
162
|
+
def local_package_names(self) -> set[str]:
|
|
163
|
+
"""Return the names of all discovered local packages."""
|
|
164
|
+
return {d.name for d in self.package_dirs}
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def import_root(self) -> Path:
|
|
168
|
+
"""Return the directory imports resolve from (``src/`` or the root)."""
|
|
169
|
+
if self._is_src_layout is None:
|
|
170
|
+
self._discover()
|
|
171
|
+
src_dir = self._root / "src"
|
|
172
|
+
return src_dir if (self._is_src_layout and src_dir.is_dir()) else self._root
|
|
173
|
+
|
|
174
|
+
def top_level_modules(self) -> list[Path]:
|
|
175
|
+
"""Return single-file top-level modules at the import root.
|
|
176
|
+
|
|
177
|
+
Finds ``.py`` files directly under the import root (``src/`` for a
|
|
178
|
+
src-layout, otherwise the project root) that represent the package
|
|
179
|
+
itself — e.g. a single-module package ``foo.py`` — excluding common
|
|
180
|
+
tooling scripts (``setup.py``, ``conftest.py``, …). Used to support
|
|
181
|
+
single-module packages that have no package directory / ``__init__.py``.
|
|
182
|
+
"""
|
|
183
|
+
base = self.import_root
|
|
184
|
+
if not base.is_dir():
|
|
185
|
+
return []
|
|
186
|
+
modules: list[Path] = []
|
|
187
|
+
for item in sorted(base.iterdir()):
|
|
188
|
+
if (
|
|
189
|
+
item.is_file()
|
|
190
|
+
and item.suffix == ".py"
|
|
191
|
+
and item.name not in NON_MODULE_FILES
|
|
192
|
+
and not item.name.startswith(".")
|
|
193
|
+
):
|
|
194
|
+
modules.append(item)
|
|
195
|
+
return modules
|
|
196
|
+
|
|
197
|
+
# ── Internal discovery ────────────────────────────────────────────
|
|
198
|
+
|
|
199
|
+
def _discover(self) -> None:
|
|
200
|
+
"""Scan the filesystem and populate internal caches."""
|
|
201
|
+
logger.debug("Discovering package layout under %s", self._root)
|
|
202
|
+
|
|
203
|
+
dirs: list[Path] = []
|
|
204
|
+
src_dir = self._root / "src"
|
|
205
|
+
|
|
206
|
+
if src_dir.is_dir():
|
|
207
|
+
self._is_src_layout = True
|
|
208
|
+
logger.debug("Detected src/ layout")
|
|
209
|
+
for item in sorted(src_dir.iterdir()):
|
|
210
|
+
if (
|
|
211
|
+
item.is_dir()
|
|
212
|
+
and not item.name.startswith((".", "_"))
|
|
213
|
+
and (item / "__init__.py").exists()
|
|
214
|
+
):
|
|
215
|
+
logger.debug(" Found src-layout package: %s", item.name)
|
|
216
|
+
dirs.append(item)
|
|
217
|
+
else:
|
|
218
|
+
self._is_src_layout = False
|
|
219
|
+
logger.debug("No src/ directory; checking flat layout")
|
|
220
|
+
for item in sorted(self._root.iterdir()):
|
|
221
|
+
if (
|
|
222
|
+
item.is_dir()
|
|
223
|
+
and item.name not in NON_PACKAGE_DIRS
|
|
224
|
+
and not item.name.startswith(".")
|
|
225
|
+
and (item / "__init__.py").exists()
|
|
226
|
+
):
|
|
227
|
+
logger.debug(" Found flat-layout package: %s", item.name)
|
|
228
|
+
dirs.append(item)
|
|
229
|
+
|
|
230
|
+
self._package_dirs = dirs
|
|
231
|
+
logger.debug(
|
|
232
|
+
"Discovery complete: %d package(s) found",
|
|
233
|
+
len(self._package_dirs),
|
|
234
|
+
)
|
pycmdcheck/profiles.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Check profiles for pycmdcheck.
|
|
2
|
+
|
|
3
|
+
Profiles define named sets of checks and configuration overrides
|
|
4
|
+
for common use cases like pyOpenSci onboarding or strict quality gates.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
# All checks that ship with pycmdcheck
|
|
13
|
+
ALL_CHECKS: frozenset[str] = frozenset(
|
|
14
|
+
{
|
|
15
|
+
"metadata",
|
|
16
|
+
"structure",
|
|
17
|
+
"tests",
|
|
18
|
+
"linting",
|
|
19
|
+
"typing",
|
|
20
|
+
"imports",
|
|
21
|
+
"license",
|
|
22
|
+
"docs",
|
|
23
|
+
"dependencies",
|
|
24
|
+
"build",
|
|
25
|
+
"formatting",
|
|
26
|
+
"version",
|
|
27
|
+
"py_typed",
|
|
28
|
+
# New checks
|
|
29
|
+
"community",
|
|
30
|
+
"ci",
|
|
31
|
+
"changelog",
|
|
32
|
+
"citation",
|
|
33
|
+
"python_versions",
|
|
34
|
+
"urls",
|
|
35
|
+
"doctests",
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Original 13 checks (pre-pyOpenSci)
|
|
40
|
+
ORIGINAL_CHECKS: frozenset[str] = frozenset(
|
|
41
|
+
{
|
|
42
|
+
"metadata",
|
|
43
|
+
"structure",
|
|
44
|
+
"tests",
|
|
45
|
+
"linting",
|
|
46
|
+
"typing",
|
|
47
|
+
"imports",
|
|
48
|
+
"license",
|
|
49
|
+
"docs",
|
|
50
|
+
"dependencies",
|
|
51
|
+
"build",
|
|
52
|
+
"formatting",
|
|
53
|
+
"version",
|
|
54
|
+
"py_typed",
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
# Checks that require installing the package, running external tools
|
|
59
|
+
# (pytest/ruff/mypy), or network access. These produce false results when run
|
|
60
|
+
# against an arbitrary repository that has NOT been installed (e.g. a CI triage
|
|
61
|
+
# bot), so they are excluded from the static-only "triage" profile.
|
|
62
|
+
DYNAMIC_CHECKS: frozenset[str] = frozenset(
|
|
63
|
+
{
|
|
64
|
+
"tests", # runs pytest
|
|
65
|
+
"imports", # resolves imports against the running interpreter
|
|
66
|
+
"dependencies", # resolves imports against the installed environment
|
|
67
|
+
"build", # builds the package
|
|
68
|
+
"linting", # runs ruff/flake8
|
|
69
|
+
"typing", # runs mypy/pyright
|
|
70
|
+
"formatting", # runs ruff/black
|
|
71
|
+
"doctests", # runs pytest --doctest
|
|
72
|
+
"urls", # validates links over the network
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Static, no-install, no-network checks — safe to run against an arbitrary
|
|
77
|
+
# submitted repository without building or installing it.
|
|
78
|
+
STATIC_CHECKS: frozenset[str] = ALL_CHECKS - DYNAMIC_CHECKS
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class Profile:
|
|
83
|
+
"""A named profile defining which checks to run and configuration."""
|
|
84
|
+
|
|
85
|
+
name: str
|
|
86
|
+
description: str
|
|
87
|
+
checks: frozenset[str]
|
|
88
|
+
config_overrides: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
PROFILES: dict[str, Profile] = {
|
|
92
|
+
"minimal": Profile(
|
|
93
|
+
name="minimal",
|
|
94
|
+
description="Quick sanity checks (metadata, structure, license)",
|
|
95
|
+
checks=frozenset({"metadata", "structure", "license"}),
|
|
96
|
+
),
|
|
97
|
+
"triage": Profile(
|
|
98
|
+
name="triage",
|
|
99
|
+
description=(
|
|
100
|
+
"Static packaging/docs checks only — no install, external tools, "
|
|
101
|
+
"or network (for CI triage of arbitrary repositories)"
|
|
102
|
+
),
|
|
103
|
+
checks=STATIC_CHECKS,
|
|
104
|
+
config_overrides={
|
|
105
|
+
"docs": {
|
|
106
|
+
"require_readme": True,
|
|
107
|
+
"check_readme_sections": True,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
),
|
|
111
|
+
"default": Profile(
|
|
112
|
+
name="default",
|
|
113
|
+
description="Standard checks (original 13 built-in checks)",
|
|
114
|
+
checks=ORIGINAL_CHECKS,
|
|
115
|
+
),
|
|
116
|
+
"pyopensci": Profile(
|
|
117
|
+
name="pyopensci",
|
|
118
|
+
description="pyOpenSci onboarding requirements",
|
|
119
|
+
checks=ALL_CHECKS,
|
|
120
|
+
config_overrides={
|
|
121
|
+
"docs": {
|
|
122
|
+
"require_readme": True,
|
|
123
|
+
"check_docstrings": True,
|
|
124
|
+
"check_readme_sections": True,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
),
|
|
128
|
+
"strict": Profile(
|
|
129
|
+
name="strict",
|
|
130
|
+
description="All checks at maximum strictness",
|
|
131
|
+
checks=ALL_CHECKS,
|
|
132
|
+
config_overrides={
|
|
133
|
+
"docs": {
|
|
134
|
+
"require_readme": True,
|
|
135
|
+
"check_docstrings": True,
|
|
136
|
+
"check_readme_sections": True,
|
|
137
|
+
},
|
|
138
|
+
"typing": {"strict": True},
|
|
139
|
+
},
|
|
140
|
+
),
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def get_profile(name: str) -> Profile | None:
|
|
145
|
+
"""Look up a profile by name.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
name: Profile name (minimal, default, pyopensci, strict).
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
The Profile, or None if not found.
|
|
152
|
+
"""
|
|
153
|
+
return PROFILES.get(name)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def list_profiles() -> list[tuple[str, str]]:
|
|
157
|
+
"""Return all profiles as (name, description) pairs."""
|
|
158
|
+
return [(p.name, p.description) for p in PROFILES.values()]
|
pycmdcheck/py.typed
ADDED
|
File without changes
|