mpat 0.2.2__tar.gz → 0.3.0__tar.gz
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.
- {mpat-0.2.2 → mpat-0.3.0}/PKG-INFO +2 -1
- {mpat-0.2.2 → mpat-0.3.0}/README.md +1 -0
- {mpat-0.2.2 → mpat-0.3.0}/pyproject.toml +1 -1
- {mpat-0.2.2 → mpat-0.3.0}/pyproject.toml.orig +1 -1
- {mpat-0.2.2 → mpat-0.3.0}/src/mpat/__init__.py +1 -1
- {mpat-0.2.2 → mpat-0.3.0}/src/mpat/_cli.py +21 -1
- mpat-0.3.0/src/mpat/_gitlab.py +125 -0
- {mpat-0.2.2 → mpat-0.3.0}/LICENSE +0 -0
- {mpat-0.2.2 → mpat-0.3.0}/src/mpat/_api.py +0 -0
- {mpat-0.2.2 → mpat-0.3.0}/src/mpat/_config.py +0 -0
- {mpat-0.2.2 → mpat-0.3.0}/src/mpat/_errors.py +0 -0
- {mpat-0.2.2 → mpat-0.3.0}/src/mpat/_fingerprint.py +0 -0
- {mpat-0.2.2 → mpat-0.3.0}/src/mpat/_generated.py +0 -0
- {mpat-0.2.2 → mpat-0.3.0}/src/mpat/_hooks.py +0 -0
- {mpat-0.2.2 → mpat-0.3.0}/src/mpat/_lock.py +0 -0
- {mpat-0.2.2 → mpat-0.3.0}/src/mpat/_pytest_plugin.py +0 -0
- {mpat-0.2.2 → mpat-0.3.0}/src/mpat/_registry.py +0 -0
- {mpat-0.2.2 → mpat-0.3.0}/src/mpat/_targets.py +0 -0
- {mpat-0.2.2 → mpat-0.3.0}/src/mpat/_until.py +0 -0
- {mpat-0.2.2 → mpat-0.3.0}/src/mpat/py.typed +0 -0
- {mpat-0.2.2 → mpat-0.3.0}/src/mpat/testing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mpat
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Lockfile-based upstream drift tracking for Python monkeypatches
|
|
5
5
|
Author: Daniel Yudelevich
|
|
6
6
|
License-Expression: MIT
|
|
@@ -140,6 +140,7 @@ next to it. `mpat.toml` wins when both exist.
|
|
|
140
140
|
mpat lock # fingerprint everything, write mpat.lock, commit it
|
|
141
141
|
mpat check # exit 1 unless every target is ok
|
|
142
142
|
mpat check --json
|
|
143
|
+
mpat check --gitlab # GitLab Code Quality report
|
|
143
144
|
mpat show somelib.client.Client.request
|
|
144
145
|
mpat diff somelib.client.Client.request
|
|
145
146
|
```
|
|
@@ -119,6 +119,7 @@ next to it. `mpat.toml` wins when both exist.
|
|
|
119
119
|
mpat lock # fingerprint everything, write mpat.lock, commit it
|
|
120
120
|
mpat check # exit 1 unless every target is ok
|
|
121
121
|
mpat check --json
|
|
122
|
+
mpat check --gitlab # GitLab Code Quality report
|
|
122
123
|
mpat show somelib.client.Client.request
|
|
123
124
|
mpat diff somelib.client.Client.request
|
|
124
125
|
```
|
|
@@ -22,6 +22,7 @@ from mpat._fingerprint import (
|
|
|
22
22
|
compare,
|
|
23
23
|
fingerprint,
|
|
24
24
|
)
|
|
25
|
+
from mpat._gitlab import code_quality_report
|
|
25
26
|
from mpat._lock import (
|
|
26
27
|
LOCK_FILENAME,
|
|
27
28
|
MISSING,
|
|
@@ -167,6 +168,13 @@ def cmd_check(args: argparse.Namespace) -> int:
|
|
|
167
168
|
)
|
|
168
169
|
if args.json:
|
|
169
170
|
print(json.dumps([dataclasses.asdict(r) for r in results], indent=_JSON_INDENT))
|
|
171
|
+
elif args.gitlab:
|
|
172
|
+
print(
|
|
173
|
+
json.dumps(
|
|
174
|
+
code_quality_report(results, root=config.root, base=args.repo_root),
|
|
175
|
+
indent=_JSON_INDENT,
|
|
176
|
+
)
|
|
177
|
+
)
|
|
170
178
|
elif not results:
|
|
171
179
|
print(_NO_DECLARATIONS)
|
|
172
180
|
else:
|
|
@@ -218,7 +226,19 @@ def _parser() -> argparse.ArgumentParser:
|
|
|
218
226
|
lock_parser = sub.add_parser("lock", help="fingerprint declared targets and write mpat.lock")
|
|
219
227
|
lock_parser.set_defaults(fn=cmd_lock)
|
|
220
228
|
check_parser = sub.add_parser("check", help="compare mpat.lock against installed upstream")
|
|
221
|
-
check_parser.
|
|
229
|
+
output = check_parser.add_mutually_exclusive_group()
|
|
230
|
+
output.add_argument("--json", action="store_true")
|
|
231
|
+
output.add_argument(
|
|
232
|
+
"--gitlab",
|
|
233
|
+
action="store_true",
|
|
234
|
+
help="print a GitLab Code Quality report of every non-ok target",
|
|
235
|
+
)
|
|
236
|
+
check_parser.add_argument(
|
|
237
|
+
"--repo-root",
|
|
238
|
+
type=Path,
|
|
239
|
+
default=None,
|
|
240
|
+
help="base for report paths (default: the nearest .git above the mpat root)",
|
|
241
|
+
)
|
|
222
242
|
check_parser.set_defaults(fn=cmd_check)
|
|
223
243
|
show_parser = sub.add_parser("show", help="print fingerprint and source of a target")
|
|
224
244
|
show_parser.add_argument("target")
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from mpat._fingerprint import BODY, MOVED, NO_SOURCE, OK, SIGNATURE, VALUE
|
|
9
|
+
from mpat._lock import MISSING, REVIEW, STALE, UNLOCKED, CheckResult
|
|
10
|
+
|
|
11
|
+
SEVERITY_BLOCKER = "blocker"
|
|
12
|
+
SEVERITY_MAJOR = "major"
|
|
13
|
+
SEVERITY_MINOR = "minor"
|
|
14
|
+
SEVERITY_INFO = "info"
|
|
15
|
+
|
|
16
|
+
_GIT = ".git"
|
|
17
|
+
_CHECK_NAME_PREFIX = "mpat"
|
|
18
|
+
_FIRST_LINE = 1
|
|
19
|
+
_FINGERPRINT_SEPARATOR = "|"
|
|
20
|
+
|
|
21
|
+
_SEVERITIES = {
|
|
22
|
+
MISSING: SEVERITY_BLOCKER,
|
|
23
|
+
MOVED: SEVERITY_BLOCKER,
|
|
24
|
+
SIGNATURE: SEVERITY_MAJOR,
|
|
25
|
+
BODY: SEVERITY_MAJOR,
|
|
26
|
+
VALUE: SEVERITY_MAJOR,
|
|
27
|
+
UNLOCKED: SEVERITY_MAJOR,
|
|
28
|
+
NO_SOURCE: SEVERITY_MINOR,
|
|
29
|
+
REVIEW: SEVERITY_MINOR,
|
|
30
|
+
STALE: SEVERITY_INFO,
|
|
31
|
+
}
|
|
32
|
+
_DEFAULT_SEVERITY = SEVERITY_MAJOR
|
|
33
|
+
|
|
34
|
+
_DESCRIPTIONS = {
|
|
35
|
+
MISSING: "no longer exists upstream",
|
|
36
|
+
MOVED: "moved upstream",
|
|
37
|
+
SIGNATURE: "the upstream signature changed",
|
|
38
|
+
BODY: "the upstream body changed",
|
|
39
|
+
VALUE: "the upstream value changed",
|
|
40
|
+
NO_SOURCE: "upstream source is unavailable, only the signature is locked",
|
|
41
|
+
UNLOCKED: f"not in the lock; run '{_CHECK_NAME_PREFIX} lock'",
|
|
42
|
+
STALE: f"in the lock but no longer declared; run '{_CHECK_NAME_PREFIX} lock' to prune",
|
|
43
|
+
REVIEW: "past its review_by date",
|
|
44
|
+
}
|
|
45
|
+
_UNKNOWN_DESCRIPTION = "drifted"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def repo_root(root: Path) -> Path | None:
|
|
49
|
+
"""The working tree `root` sits in, found by the `.git` a checkout always has.
|
|
50
|
+
|
|
51
|
+
A worktree and a submodule carry `.git` as a file rather than a directory, so
|
|
52
|
+
existence, not `is_dir`, is the test.
|
|
53
|
+
"""
|
|
54
|
+
for candidate in (root, *root.parents):
|
|
55
|
+
if (candidate / _GIT).exists():
|
|
56
|
+
return candidate
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _report_path(declared_in: str, *, root: Path, base: Path) -> str:
|
|
61
|
+
absolute = Path(declared_in)
|
|
62
|
+
if not absolute.is_absolute():
|
|
63
|
+
absolute = root / absolute
|
|
64
|
+
try:
|
|
65
|
+
return absolute.relative_to(base).as_posix()
|
|
66
|
+
except ValueError:
|
|
67
|
+
return Path(declared_in).as_posix()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _line_in(text: str, target: str) -> int:
|
|
71
|
+
for number, line in enumerate(text.splitlines(), start=_FIRST_LINE):
|
|
72
|
+
if f'"{target}"' in line or f"'{target}'" in line:
|
|
73
|
+
return number
|
|
74
|
+
return _FIRST_LINE
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _declaration_line(path: Path, target: str, cache: dict[Path, str]) -> int:
|
|
78
|
+
if path not in cache:
|
|
79
|
+
try:
|
|
80
|
+
cache[path] = path.read_text(encoding="utf-8")
|
|
81
|
+
except (OSError, UnicodeDecodeError):
|
|
82
|
+
cache[path] = ""
|
|
83
|
+
return _line_in(cache[path], target)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _description(result: CheckResult) -> str:
|
|
87
|
+
what = _DESCRIPTIONS.get(result.status, _UNKNOWN_DESCRIPTION)
|
|
88
|
+
note = f" ({result.note})" if result.note else ""
|
|
89
|
+
return f"{result.role} {result.target}: {what}{note}"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _fingerprint(path: str, result: CheckResult) -> str:
|
|
93
|
+
identity = _FINGERPRINT_SEPARATOR.join((path, result.role, result.target))
|
|
94
|
+
return hashlib.sha256(identity.encode("utf-8")).hexdigest()
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def code_quality_report(
|
|
98
|
+
results: Sequence[CheckResult], *, root: Path, base: Path | None = None
|
|
99
|
+
) -> list[dict[str, Any]]:
|
|
100
|
+
"""Render the non-OK results as GitLab Code Quality issues.
|
|
101
|
+
|
|
102
|
+
`location.path` is resolved against the repository root, not against `root`,
|
|
103
|
+
because GitLab reads it relative to the checkout: a `mpat check` run in
|
|
104
|
+
`services/api` must still report `services/api/src/patches.py`.
|
|
105
|
+
"""
|
|
106
|
+
base = base or repo_root(root) or root
|
|
107
|
+
cache: dict[Path, str] = {}
|
|
108
|
+
report: list[dict[str, Any]] = []
|
|
109
|
+
for result in results:
|
|
110
|
+
if result.status == OK:
|
|
111
|
+
continue
|
|
112
|
+
path = _report_path(result.declared_in, root=root, base=base)
|
|
113
|
+
report.append(
|
|
114
|
+
{
|
|
115
|
+
"description": _description(result),
|
|
116
|
+
"check_name": f"{_CHECK_NAME_PREFIX}/{result.status}",
|
|
117
|
+
"fingerprint": _fingerprint(path, result),
|
|
118
|
+
"severity": _SEVERITIES.get(result.status, _DEFAULT_SEVERITY),
|
|
119
|
+
"location": {
|
|
120
|
+
"path": path,
|
|
121
|
+
"lines": {"begin": _declaration_line(base / path, result.target, cache)},
|
|
122
|
+
},
|
|
123
|
+
}
|
|
124
|
+
)
|
|
125
|
+
return report
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|