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.
Files changed (75) hide show
  1. patchahead/__init__.py +8 -0
  2. patchahead/analysis/__init__.py +52 -0
  3. patchahead/analysis/edits.py +143 -0
  4. patchahead/analysis/index.py +203 -0
  5. patchahead/analysis/python_ast.py +457 -0
  6. patchahead/apidiff/__init__.py +23 -0
  7. patchahead/apidiff/compare.py +366 -0
  8. patchahead/apidiff/download.py +95 -0
  9. patchahead/apidiff/surface.py +337 -0
  10. patchahead/ci.py +301 -0
  11. patchahead/cli.py +627 -0
  12. patchahead/config.py +284 -0
  13. patchahead/demo/__init__.py +256 -0
  14. patchahead/demo/fixtures/changes/field-rename.md +14 -0
  15. patchahead/demo/fixtures/changes/invoice-field-rename.md +21 -0
  16. patchahead/demo/fixtures/changes/kwarg-rename.md +14 -0
  17. patchahead/demo/fixtures/changes/method-rename.md +12 -0
  18. patchahead/demo/fixtures/changes/pagination-cursor.json +24 -0
  19. patchahead/demo/fixtures/changes/pagination-cursor.md +20 -0
  20. patchahead/demo/fixtures/changes/sdk-v2.md +31 -0
  21. patchahead/demo/fixtures/orders-service/README.md +51 -0
  22. patchahead/demo/fixtures/orders-service/app/__init__.py +0 -0
  23. patchahead/demo/fixtures/orders-service/app/client.py +15 -0
  24. patchahead/demo/fixtures/orders-service/app/models.py +10 -0
  25. patchahead/demo/fixtures/orders-service/app/order_report.py +24 -0
  26. patchahead/demo/fixtures/orders-service/app/order_sync.py +21 -0
  27. patchahead/demo/fixtures/orders-service/conftest.py +6 -0
  28. patchahead/demo/fixtures/orders-service/pyproject.toml +16 -0
  29. patchahead/demo/fixtures/orders-service/tests/test_client.py +14 -0
  30. patchahead/demo/fixtures/orders-service/tests/test_order_report.py +24 -0
  31. patchahead/demo/fixtures/orders-service/tests/test_order_sync.py +11 -0
  32. patchahead/demo/fixtures/orders-service/upstream/__init__.py +0 -0
  33. patchahead/demo/fixtures/orders-service/upstream/api_v1.py +34 -0
  34. patchahead/demo/fixtures/orders-service/upstream/api_v2.py +56 -0
  35. patchahead/demo/serve.py +189 -0
  36. patchahead/domain/__init__.py +67 -0
  37. patchahead/domain/change.py +269 -0
  38. patchahead/domain/completeness.py +91 -0
  39. patchahead/domain/impact.py +248 -0
  40. patchahead/domain/patch.py +81 -0
  41. patchahead/domain/plan.py +170 -0
  42. patchahead/domain/result.py +210 -0
  43. patchahead/domain/validation.py +200 -0
  44. patchahead/engine.py +609 -0
  45. patchahead/handlers/__init__.py +35 -0
  46. patchahead/handlers/base.py +211 -0
  47. patchahead/handlers/field_rename.py +425 -0
  48. patchahead/handlers/kwarg_rename.py +201 -0
  49. patchahead/handlers/method_rename.py +608 -0
  50. patchahead/handlers/pagination.py +582 -0
  51. patchahead/ingest/__init__.py +32 -0
  52. patchahead/ingest/base.py +102 -0
  53. patchahead/ingest/markdown.py +1138 -0
  54. patchahead/ingest/structured.py +218 -0
  55. patchahead/llm/__init__.py +28 -0
  56. patchahead/llm/client.py +152 -0
  57. patchahead/llm/proposer.py +620 -0
  58. patchahead/observability.py +223 -0
  59. patchahead/reporting.py +451 -0
  60. patchahead/testing/__init__.py +22 -0
  61. patchahead/testing/discovery.py +113 -0
  62. patchahead/testing/runner.py +138 -0
  63. patchahead/validation/__init__.py +5 -0
  64. patchahead/validation/completeness.py +265 -0
  65. patchahead/validation/engine.py +531 -0
  66. patchahead/web/__init__.py +13 -0
  67. patchahead/web/server.py +279 -0
  68. patchahead/web/static/index.html +650 -0
  69. patchahead/workspace.py +382 -0
  70. patchahead-0.3.0.dist-info/METADATA +368 -0
  71. patchahead-0.3.0.dist-info/RECORD +75 -0
  72. patchahead-0.3.0.dist-info/WHEEL +5 -0
  73. patchahead-0.3.0.dist-info/entry_points.txt +2 -0
  74. patchahead-0.3.0.dist-info/licenses/LICENSE +21 -0
  75. 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))