loomweave-plugin-python 1.0.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.
- loomweave_plugin_python/__init__.py +3 -0
- loomweave_plugin_python/__main__.py +15 -0
- loomweave_plugin_python/call_resolver.py +65 -0
- loomweave_plugin_python/entity_id.py +75 -0
- loomweave_plugin_python/extractor.py +1312 -0
- loomweave_plugin_python/py.typed +0 -0
- loomweave_plugin_python/pyright_session.py +1655 -0
- loomweave_plugin_python/qualname.py +48 -0
- loomweave_plugin_python/reference_resolver.py +70 -0
- loomweave_plugin_python/server.py +310 -0
- loomweave_plugin_python/stdout_guard.py +62 -0
- loomweave_plugin_python/wardline_descriptor.py +197 -0
- loomweave_plugin_python-1.0.0.data/data/share/loomweave/plugins/python/plugin.toml +71 -0
- loomweave_plugin_python-1.0.0.dist-info/METADATA +73 -0
- loomweave_plugin_python-1.0.0.dist-info/RECORD +17 -0
- loomweave_plugin_python-1.0.0.dist-info/WHEEL +4 -0
- loomweave_plugin_python-1.0.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Entry point for the ``loomweave-plugin-python`` executable.
|
|
2
|
+
|
|
3
|
+
Installs stdout discipline (``stdout_guard``) and hands control to the
|
|
4
|
+
JSON-RPC server loop. ``sys.exit`` threads the server's exit code out to
|
|
5
|
+
the host process.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
|
|
12
|
+
from loomweave_plugin_python.server import main
|
|
13
|
+
|
|
14
|
+
if __name__ == "__main__":
|
|
15
|
+
sys.exit(main())
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import TYPE_CHECKING, Literal, NotRequired, Protocol, TypedDict
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CallsEdgeProperties(TypedDict):
|
|
12
|
+
candidates: list[str]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CallsRawEdge(TypedDict):
|
|
16
|
+
kind: Literal["calls"]
|
|
17
|
+
from_id: str
|
|
18
|
+
to_id: str
|
|
19
|
+
source_byte_start: int
|
|
20
|
+
source_byte_end: int
|
|
21
|
+
confidence: Literal["resolved", "ambiguous"]
|
|
22
|
+
properties: NotRequired[CallsEdgeProperties]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Finding(TypedDict):
|
|
26
|
+
subcode: str
|
|
27
|
+
severity: Literal["info", "warning", "error"]
|
|
28
|
+
message: str
|
|
29
|
+
metadata: dict[str, object]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class UnresolvedCallSite(TypedDict):
|
|
33
|
+
caller_entity_id: str
|
|
34
|
+
site_ordinal: int
|
|
35
|
+
source_byte_start: int
|
|
36
|
+
source_byte_end: int
|
|
37
|
+
callee_expr: str
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class CallResolutionResult:
|
|
42
|
+
edges: list[CallsRawEdge] = field(default_factory=list)
|
|
43
|
+
unresolved_call_sites_total: int = 0
|
|
44
|
+
unresolved_call_sites: list[UnresolvedCallSite] = field(default_factory=list)
|
|
45
|
+
pyright_query_latency_ms: list[int] = field(default_factory=list)
|
|
46
|
+
pyright_index_parse_latency_ms: list[int] = field(default_factory=list)
|
|
47
|
+
findings: list[Finding] = field(default_factory=list)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CallResolver(Protocol):
|
|
51
|
+
def resolve_calls(
|
|
52
|
+
self,
|
|
53
|
+
file_path: str | Path,
|
|
54
|
+
function_ids: Sequence[str],
|
|
55
|
+
) -> CallResolutionResult: ...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class NoOpCallResolver:
|
|
59
|
+
def resolve_calls(
|
|
60
|
+
self,
|
|
61
|
+
file_path: str | Path,
|
|
62
|
+
function_ids: Sequence[str],
|
|
63
|
+
) -> CallResolutionResult:
|
|
64
|
+
_ = (file_path, function_ids)
|
|
65
|
+
return CallResolutionResult()
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""L2 3-segment EntityId assembler matching WP1's Rust ``entity_id()`` byte-for-byte.
|
|
2
|
+
|
|
3
|
+
Per ADR-003 + ADR-022, every Loomweave entity has a 3-segment ID of the
|
|
4
|
+
form ``{plugin_id}:{kind}:{canonical_qualified_name}``.
|
|
5
|
+
|
|
6
|
+
Validation (mirrors ``crates/loomweave-core/src/entity_id.rs``):
|
|
7
|
+
|
|
8
|
+
- ``plugin_id`` and ``kind`` must match the identifier grammar
|
|
9
|
+
``[a-z][a-z0-9_]*`` (ADR-022).
|
|
10
|
+
- No segment may contain a literal ``:`` (reserved separator).
|
|
11
|
+
- No segment may be empty.
|
|
12
|
+
|
|
13
|
+
The shared fixture ``fixtures/entity_id.json`` (Task 5) drives the
|
|
14
|
+
cross-language parity check: both the Rust assembler and this Python
|
|
15
|
+
assembler consume the same fixture rows and must produce identical
|
|
16
|
+
strings byte-for-byte.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import re
|
|
22
|
+
|
|
23
|
+
_GRAMMAR = re.compile(r"^[a-z][a-z0-9_]*$")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EntityIdError(ValueError):
|
|
27
|
+
"""Base class for all ``entity_id()`` validation failures."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class EmptySegmentError(EntityIdError):
|
|
31
|
+
"""A segment (``plugin_id``, ``kind``, or ``canonical_qualified_name``) was empty."""
|
|
32
|
+
|
|
33
|
+
def __init__(self, field: str) -> None:
|
|
34
|
+
super().__init__(f"segment {field} empty")
|
|
35
|
+
self.field = field
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class GrammarViolationError(EntityIdError):
|
|
39
|
+
"""A segment did not match the ADR-022 grammar ``[a-z][a-z0-9_]*``."""
|
|
40
|
+
|
|
41
|
+
def __init__(self, field: str, value: str) -> None:
|
|
42
|
+
super().__init__(f"segment {field} violates ADR-022 grammar [a-z][a-z0-9_]*: {value!r}")
|
|
43
|
+
self.field = field
|
|
44
|
+
self.value = value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SegmentContainsColonError(EntityIdError):
|
|
48
|
+
"""A segment contained the reserved ``:`` separator (UQ-WP1-07)."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, field: str, value: str) -> None:
|
|
51
|
+
super().__init__(f"segment {field} contains reserved ':' separator: {value!r}")
|
|
52
|
+
self.field = field
|
|
53
|
+
self.value = value
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _validate_grammar(field: str, value: str) -> None:
|
|
57
|
+
"""Mirror ``validate_grammar`` in the Rust side — empty, colon, then regex."""
|
|
58
|
+
if not value:
|
|
59
|
+
raise EmptySegmentError(field)
|
|
60
|
+
if ":" in value:
|
|
61
|
+
raise SegmentContainsColonError(field, value)
|
|
62
|
+
if not _GRAMMAR.fullmatch(value):
|
|
63
|
+
raise GrammarViolationError(field, value)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def entity_id(plugin_id: str, kind: str, canonical_qualified_name: str) -> str:
|
|
67
|
+
"""Assemble the 3-segment EntityId string with full validation."""
|
|
68
|
+
_validate_grammar("plugin_id", plugin_id)
|
|
69
|
+
_validate_grammar("kind", kind)
|
|
70
|
+
qn_field = "canonical_qualified_name"
|
|
71
|
+
if not canonical_qualified_name:
|
|
72
|
+
raise EmptySegmentError(qn_field)
|
|
73
|
+
if ":" in canonical_qualified_name:
|
|
74
|
+
raise SegmentContainsColonError(qn_field, canonical_qualified_name)
|
|
75
|
+
return f"{plugin_id}:{kind}:{canonical_qualified_name}"
|