patchahead 0.3.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.
- patchahead/__init__.py +8 -0
- patchahead/analysis/__init__.py +52 -0
- patchahead/analysis/edits.py +143 -0
- patchahead/analysis/index.py +203 -0
- patchahead/analysis/python_ast.py +457 -0
- patchahead/apidiff/__init__.py +23 -0
- patchahead/apidiff/compare.py +366 -0
- patchahead/apidiff/download.py +95 -0
- patchahead/apidiff/surface.py +337 -0
- patchahead/ci.py +301 -0
- patchahead/cli.py +627 -0
- patchahead/config.py +284 -0
- patchahead/demo/__init__.py +256 -0
- patchahead/demo/fixtures/changes/field-rename.md +14 -0
- patchahead/demo/fixtures/changes/invoice-field-rename.md +21 -0
- patchahead/demo/fixtures/changes/kwarg-rename.md +14 -0
- patchahead/demo/fixtures/changes/method-rename.md +12 -0
- patchahead/demo/fixtures/changes/pagination-cursor.json +24 -0
- patchahead/demo/fixtures/changes/pagination-cursor.md +20 -0
- patchahead/demo/fixtures/changes/sdk-v2.md +31 -0
- patchahead/demo/fixtures/orders-service/README.md +51 -0
- patchahead/demo/fixtures/orders-service/app/__init__.py +0 -0
- patchahead/demo/fixtures/orders-service/app/client.py +15 -0
- patchahead/demo/fixtures/orders-service/app/models.py +10 -0
- patchahead/demo/fixtures/orders-service/app/order_report.py +24 -0
- patchahead/demo/fixtures/orders-service/app/order_sync.py +21 -0
- patchahead/demo/fixtures/orders-service/conftest.py +6 -0
- patchahead/demo/fixtures/orders-service/pyproject.toml +16 -0
- patchahead/demo/fixtures/orders-service/tests/test_client.py +14 -0
- patchahead/demo/fixtures/orders-service/tests/test_order_report.py +24 -0
- patchahead/demo/fixtures/orders-service/tests/test_order_sync.py +11 -0
- patchahead/demo/fixtures/orders-service/upstream/__init__.py +0 -0
- patchahead/demo/fixtures/orders-service/upstream/api_v1.py +34 -0
- patchahead/demo/fixtures/orders-service/upstream/api_v2.py +56 -0
- patchahead/demo/serve.py +189 -0
- patchahead/domain/__init__.py +67 -0
- patchahead/domain/change.py +269 -0
- patchahead/domain/completeness.py +91 -0
- patchahead/domain/impact.py +248 -0
- patchahead/domain/patch.py +81 -0
- patchahead/domain/plan.py +170 -0
- patchahead/domain/result.py +210 -0
- patchahead/domain/validation.py +200 -0
- patchahead/engine.py +609 -0
- patchahead/handlers/__init__.py +35 -0
- patchahead/handlers/base.py +211 -0
- patchahead/handlers/field_rename.py +425 -0
- patchahead/handlers/kwarg_rename.py +201 -0
- patchahead/handlers/method_rename.py +608 -0
- patchahead/handlers/pagination.py +582 -0
- patchahead/ingest/__init__.py +32 -0
- patchahead/ingest/base.py +102 -0
- patchahead/ingest/markdown.py +1138 -0
- patchahead/ingest/structured.py +218 -0
- patchahead/llm/__init__.py +28 -0
- patchahead/llm/client.py +152 -0
- patchahead/llm/proposer.py +620 -0
- patchahead/observability.py +223 -0
- patchahead/reporting.py +451 -0
- patchahead/testing/__init__.py +22 -0
- patchahead/testing/discovery.py +113 -0
- patchahead/testing/runner.py +138 -0
- patchahead/validation/__init__.py +5 -0
- patchahead/validation/completeness.py +265 -0
- patchahead/validation/engine.py +531 -0
- patchahead/web/__init__.py +13 -0
- patchahead/web/server.py +279 -0
- patchahead/web/static/index.html +650 -0
- patchahead/workspace.py +382 -0
- patchahead-0.3.0.dist-info/METADATA +368 -0
- patchahead-0.3.0.dist-info/RECORD +75 -0
- patchahead-0.3.0.dist-info/WHEEL +5 -0
- patchahead-0.3.0.dist-info/entry_points.txt +2 -0
- patchahead-0.3.0.dist-info/licenses/LICENSE +21 -0
- patchahead-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""The change-parser interface, and the format registry.
|
|
2
|
+
|
|
3
|
+
A parser turns one change document into zero or more
|
|
4
|
+
:class:`~patchahead.domain.change.BreakingChange` objects. Adding a format
|
|
5
|
+
(OpenAPI spec diffs, a vendor's changelog API, an SDK's ``CHANGELOG.md``
|
|
6
|
+
convention) means writing one class and registering it here -- no other module
|
|
7
|
+
changes.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from patchahead.domain.change import BreakingChange
|
|
18
|
+
|
|
19
|
+
log = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class IngestError(Exception):
|
|
23
|
+
"""Raised when a change document exists but cannot be read or parsed."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class ChangeDocument:
|
|
28
|
+
"""A change document loaded from disk (or supplied directly)."""
|
|
29
|
+
|
|
30
|
+
text: str
|
|
31
|
+
#: Where it came from, for error messages and evidence. May be "<string>".
|
|
32
|
+
path: str = "<string>"
|
|
33
|
+
#: Lowercased file extension, e.g. ".md". Used for parser selection.
|
|
34
|
+
suffix: str = ""
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def load(cls, path: str | Path) -> ChangeDocument:
|
|
38
|
+
file_path = Path(path).expanduser()
|
|
39
|
+
if not file_path.exists():
|
|
40
|
+
raise IngestError(f"no such change document: {file_path}")
|
|
41
|
+
if file_path.is_dir():
|
|
42
|
+
raise IngestError(f"change document is a directory: {file_path}")
|
|
43
|
+
try:
|
|
44
|
+
text = file_path.read_text(encoding="utf-8")
|
|
45
|
+
except UnicodeDecodeError as exc:
|
|
46
|
+
raise IngestError(f"{file_path} is not valid UTF-8: {exc.reason}") from exc
|
|
47
|
+
except OSError as exc:
|
|
48
|
+
raise IngestError(f"cannot read {file_path}: {exc}") from exc
|
|
49
|
+
if not text.strip():
|
|
50
|
+
raise IngestError(f"change document is empty: {file_path}")
|
|
51
|
+
return cls(text=text, path=str(file_path), suffix=file_path.suffix.lower())
|
|
52
|
+
|
|
53
|
+
def lines(self) -> list[str]:
|
|
54
|
+
return self.text.splitlines()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ChangeParser(ABC):
|
|
58
|
+
"""Turns a :class:`ChangeDocument` into breaking changes."""
|
|
59
|
+
|
|
60
|
+
#: Stable identifier, recorded on every change this parser produces.
|
|
61
|
+
name: str = ""
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def supports(self, document: ChangeDocument) -> bool:
|
|
65
|
+
"""Whether this parser can handle the document."""
|
|
66
|
+
|
|
67
|
+
@abstractmethod
|
|
68
|
+
def parse(self, document: ChangeDocument) -> list[BreakingChange]:
|
|
69
|
+
"""Extract breaking changes. May return an empty list."""
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
_PARSERS: list[ChangeParser] = []
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def register(parser: ChangeParser) -> ChangeParser:
|
|
76
|
+
"""Add a parser to the registry. Later registrations are tried first."""
|
|
77
|
+
_PARSERS.insert(0, parser)
|
|
78
|
+
return parser
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def registered() -> list[ChangeParser]:
|
|
82
|
+
return list(_PARSERS)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def parse_document(document: ChangeDocument) -> list[BreakingChange]:
|
|
86
|
+
"""Parse with the first registered parser that supports the document.
|
|
87
|
+
|
|
88
|
+
Raises :class:`IngestError` when no parser claims it, rather than guessing.
|
|
89
|
+
"""
|
|
90
|
+
for parser in _PARSERS:
|
|
91
|
+
if parser.supports(document):
|
|
92
|
+
log.debug("parsing %s with the %s parser", document.path, parser.name)
|
|
93
|
+
return parser.parse(document)
|
|
94
|
+
raise IngestError(
|
|
95
|
+
f"no parser supports {document.path}. Supported formats: "
|
|
96
|
+
f"Markdown/plain text (.md, .txt, .rst), JSON (.json), YAML (.yaml, .yml)."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def parse_file(path: str | Path) -> list[BreakingChange]:
|
|
101
|
+
"""Load and parse a change document from disk."""
|
|
102
|
+
return parse_document(ChangeDocument.load(path))
|