compass-code 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.
Files changed (137) hide show
  1. compass_cli/__init__.py +0 -0
  2. compass_cli/commands/__init__.py +0 -0
  3. compass_cli/commands/ingest.py +310 -0
  4. compass_cli/commands/mcp.py +25 -0
  5. compass_cli/commands/query.py +181 -0
  6. compass_cli/commands/serve.py +123 -0
  7. compass_cli/commands/setup.py +171 -0
  8. compass_cli/display.py +231 -0
  9. compass_cli/errors.py +30 -0
  10. compass_cli/main.py +54 -0
  11. compass_cli/progress.py +191 -0
  12. compass_cli/result_views.py +389 -0
  13. compass_cli/types.py +41 -0
  14. compass_code-0.1.0.dist-info/METADATA +72 -0
  15. compass_code-0.1.0.dist-info/RECORD +137 -0
  16. compass_code-0.1.0.dist-info/WHEEL +4 -0
  17. compass_code-0.1.0.dist-info/entry_points.txt +2 -0
  18. compass_core/__init__.py +1 -0
  19. compass_core/contract/__init__.py +6 -0
  20. compass_core/contract/errors.py +116 -0
  21. compass_core/contract/models.py +204 -0
  22. compass_core/contract/params.py +99 -0
  23. compass_core/contract/results.py +370 -0
  24. compass_core/contract/serialize.py +59 -0
  25. compass_core/contract/types.py +76 -0
  26. compass_core/errors.py +148 -0
  27. compass_core/graph/__init__.py +0 -0
  28. compass_core/graph/db.py +205 -0
  29. compass_core/graph/schema.py +165 -0
  30. compass_core/graph/stored_ids.py +133 -0
  31. compass_core/graph/writer.py +482 -0
  32. compass_core/index/__init__.py +1 -0
  33. compass_core/index/freshness.py +55 -0
  34. compass_core/index/locate.py +155 -0
  35. compass_core/index/metadata.py +107 -0
  36. compass_core/ir/__init__.py +0 -0
  37. compass_core/ir/graph.py +65 -0
  38. compass_core/ir/models.py +75 -0
  39. compass_core/languages/__init__.py +1 -0
  40. compass_core/languages/base.py +26 -0
  41. compass_core/languages/python/__init__.py +6 -0
  42. compass_core/languages/python/extractor.py +670 -0
  43. compass_core/languages/python/import_resolver.py +107 -0
  44. compass_core/languages/registry.py +89 -0
  45. compass_core/logger.py +42 -0
  46. compass_core/phases/__init__.py +0 -0
  47. compass_core/phases/base.py +58 -0
  48. compass_core/phases/discovery.py +167 -0
  49. compass_core/phases/parse.py +80 -0
  50. compass_core/phases/persistence.py +33 -0
  51. compass_core/phases/pipeline.py +152 -0
  52. compass_core/phases/resolution.py +572 -0
  53. compass_core/phases/symbol_table.py +36 -0
  54. compass_core/phases/validation.py +76 -0
  55. compass_core/progress.py +45 -0
  56. compass_core/py.typed +0 -0
  57. compass_core/query/__init__.py +1 -0
  58. compass_core/query/cypher.py +92 -0
  59. compass_core/query/detail.py +241 -0
  60. compass_core/query/graph_map.py +127 -0
  61. compass_core/query/impact.py +273 -0
  62. compass_core/query/impact_walk.py +352 -0
  63. compass_core/query/overview.py +221 -0
  64. compass_core/query/references.py +235 -0
  65. compass_core/query/rows.py +378 -0
  66. compass_core/query/search.py +183 -0
  67. compass_core/query/status.py +49 -0
  68. compass_core/query/walk_queries.py +202 -0
  69. compass_core/render.py +408 -0
  70. compass_core/settings.py +61 -0
  71. compass_core/shared_types.py +78 -0
  72. compass_mcp/__init__.py +0 -0
  73. compass_mcp/guide.py +45 -0
  74. compass_mcp/py.typed +0 -0
  75. compass_mcp/server.py +261 -0
  76. compass_server/__init__.py +5 -0
  77. compass_server/errors.py +84 -0
  78. compass_server/main.py +42 -0
  79. compass_server/py.typed +0 -0
  80. compass_server/routers/__init__.py +0 -0
  81. compass_server/routers/query.py +182 -0
  82. compass_server/settings.py +37 -0
  83. compass_server/static/404.html +1 -0
  84. compass_server/static/_next/static/chunks/1629039a.eed2a06658ec4f9c.js +1 -0
  85. compass_server/static/_next/static/chunks/1ff282bf-bb7c483245264fc9.js +1 -0
  86. compass_server/static/_next/static/chunks/223.f881a22f446259bf.js +508 -0
  87. compass_server/static/_next/static/chunks/51fa5c19.6396973222c9d6fd.js +485 -0
  88. compass_server/static/_next/static/chunks/556.443c1682b6b20128.js +1 -0
  89. compass_server/static/_next/static/chunks/684-1aa0170f59baf77e.js +1 -0
  90. compass_server/static/_next/static/chunks/92e71660.3a42d4f70d017951.js +342 -0
  91. compass_server/static/_next/static/chunks/937-8692224bb6d1b605.js +1 -0
  92. compass_server/static/_next/static/chunks/app/_not-found/page-2c96ddf295eb6a86.js +1 -0
  93. compass_server/static/_next/static/chunks/app/layout-4bd0ba4a7d18aff9.js +1 -0
  94. compass_server/static/_next/static/chunks/app/page-5376a0a98bbd1faa.js +1 -0
  95. compass_server/static/_next/static/chunks/framework-96dcd958cfe08bd4.js +1 -0
  96. compass_server/static/_next/static/chunks/main-6dee8e626b16731a.js +1 -0
  97. compass_server/static/_next/static/chunks/main-app-460e8bc670818718.js +1 -0
  98. compass_server/static/_next/static/chunks/pages/_app-638a3e7363f92b80.js +1 -0
  99. compass_server/static/_next/static/chunks/pages/_error-6a72dbbb45cae743.js +1 -0
  100. compass_server/static/_next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  101. compass_server/static/_next/static/chunks/webpack-597057bf48ce4111.js +1 -0
  102. compass_server/static/_next/static/css/1b0fbfd8dd653f37.css +3 -0
  103. compass_server/static/_next/static/ctUZeD9K7jQUgLqmBas2L/_buildManifest.js +1 -0
  104. compass_server/static/_next/static/ctUZeD9K7jQUgLqmBas2L/_ssgManifest.js +1 -0
  105. compass_server/static/_next/static/media/0086f8992871c45b-s.woff2 +0 -0
  106. compass_server/static/_next/static/media/034d78ad42e9620c-s.woff2 +0 -0
  107. compass_server/static/_next/static/media/03b436aa846269de-s.woff2 +0 -0
  108. compass_server/static/_next/static/media/0484562807a97172-s.p.woff2 +0 -0
  109. compass_server/static/_next/static/media/08aedeceaf1dcd57-s.woff2 +0 -0
  110. compass_server/static/_next/static/media/0aa834ed78bf6d07-s.woff2 +0 -0
  111. compass_server/static/_next/static/media/29a4aea02fdee119-s.woff2 +0 -0
  112. compass_server/static/_next/static/media/29e7bbdce9332268-s.woff2 +0 -0
  113. compass_server/static/_next/static/media/36008747766f78c6-s.woff2 +0 -0
  114. compass_server/static/_next/static/media/39969fcf98a3026e-s.woff2 +0 -0
  115. compass_server/static/_next/static/media/4de1fea1a954a5b6-s.p.woff2 +0 -0
  116. compass_server/static/_next/static/media/5c0c2bcbaa4149ca-s.p.woff2 +0 -0
  117. compass_server/static/_next/static/media/67957d42bae0796d-s.woff2 +0 -0
  118. compass_server/static/_next/static/media/6d38f9d4e0f4772b-s.woff2 +0 -0
  119. compass_server/static/_next/static/media/6d664cce900333ee-s.p.woff2 +0 -0
  120. compass_server/static/_next/static/media/886030b0b59bc5a7-s.woff2 +0 -0
  121. compass_server/static/_next/static/media/8888a3826f4a3af4-s.p.woff2 +0 -0
  122. compass_server/static/_next/static/media/939c4f875ee75fbb-s.woff2 +0 -0
  123. compass_server/static/_next/static/media/a1386beebedccca4-s.woff2 +0 -0
  124. compass_server/static/_next/static/media/ab00a911ac2adf48-s.woff2 +0 -0
  125. compass_server/static/_next/static/media/b957ea75a84b6ea7-s.p.woff2 +0 -0
  126. compass_server/static/_next/static/media/bb3ef058b751a6ad-s.p.woff2 +0 -0
  127. compass_server/static/_next/static/media/c3bc380753a8436c-s.woff2 +0 -0
  128. compass_server/static/_next/static/media/cce080f35d014443-s.woff2 +0 -0
  129. compass_server/static/_next/static/media/db911767852bc875-s.woff2 +0 -0
  130. compass_server/static/_next/static/media/eafabf029ad39a43-s.p.woff2 +0 -0
  131. compass_server/static/_next/static/media/f10b8e9d91f3edcb-s.woff2 +0 -0
  132. compass_server/static/_next/static/media/f911b923c6adde36-s.woff2 +0 -0
  133. compass_server/static/_next/static/media/fe0777f1195381cb-s.woff2 +0 -0
  134. compass_server/static/index.html +1 -0
  135. compass_server/static/index.txt +25 -0
  136. compass_server/web_bundle.py +27 -0
  137. compass_server/web_limits.py +48 -0
@@ -0,0 +1,205 @@
1
+ """LadybugDB wrapper — the primary interface for graph database operations.
2
+
3
+ This module provides a thin wrapper around the real_ladybug database driver,
4
+ ensuring consistent connection handling and query execution. It is the only
5
+ file in the codebase permitted to import real_ladybug directly.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import logging
11
+ from collections.abc import Sequence
12
+ from pathlib import Path
13
+ from types import TracebackType
14
+ from typing import Any, TypeAlias, cast
15
+
16
+ import real_ladybug as lb
17
+
18
+ from compass_core.errors import IndexBusyError, QueryTimeoutError
19
+ from compass_core.settings import settings
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+
24
+ QueryParams: TypeAlias = dict[str, Any]
25
+ """Prepared-statement parameters for ``GraphDB.execute``."""
26
+
27
+ Row: TypeAlias = Sequence[Any]
28
+ """One positional result row; only ``query/rows.py`` should interpret its columns."""
29
+
30
+
31
+ def as_row(result: lb.QueryResult) -> Sequence[Any]:
32
+ """Return the next result row as a positionally-indexable sequence.
33
+
34
+ Every query executed through this module selects columns positionally
35
+ (never ``RETURN * AS map``), so ``QueryResult``'s ``list[Any] |
36
+ dict[str, Any]`` row type never actually resolves to the dict variant
37
+ here — that's the one shape ``row[0]``-style indexing can't handle.
38
+ """
39
+ row = result.get_next()
40
+ assert not isinstance(row, dict), "expected a positional row, got a dict-row result"
41
+ return row
42
+
43
+
44
+ def as_rows(result: lb.QueryResult) -> list[Row]:
45
+ """Return every remaining result row as a positionally-indexable sequence. See `as_row`."""
46
+ rows = result.get_all()
47
+ for row in rows:
48
+ assert not isinstance(row, dict), "expected a positional row, got a dict-row result"
49
+ return cast("list[Row]", rows)
50
+
51
+
52
+ class GraphDB:
53
+ """Thin wrapper around a LadybugDB database and its connection pair.
54
+
55
+ This class manages the lifecycle of the database handle and its associated
56
+ connection, providing a simplified interface for executing Cypher queries.
57
+
58
+ Attributes:
59
+ _db: The underlying LadybugDB database instance.
60
+ _conn: The active connection to the database instance.
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ db_path: str | Path | None = None,
66
+ *,
67
+ read_only: bool = False,
68
+ timeout_ms: int | None = None,
69
+ ) -> None:
70
+ """Initializes the GraphDB wrapper and opens a database connection.
71
+
72
+ If no database path is provided, it defaults to the value specified
73
+ in the LADYBUG_DB_PATH environment variable via the settings module.
74
+
75
+ Args:
76
+ db_path: Path to the LadybugDB database directory. Defaults to
77
+ settings.LADYBUG_DB_PATH.
78
+ read_only: Open the database read-only; write statements fail.
79
+ timeout_ms: Per-query timeout in milliseconds. Defaults to
80
+ settings.QUERY_TIMEOUT_MS.
81
+
82
+ Raises:
83
+ IndexBusyError: Another process holds the database file lock.
84
+ """
85
+ path = db_path or settings.LADYBUG_DB_PATH
86
+ try:
87
+ self._db = lb.Database(str(path), read_only=read_only)
88
+ except RuntimeError as exc:
89
+ # Real driver text (verified against a second process holding the
90
+ # write lock; handles within one process do not conflict):
91
+ # "IO exception: Could not set lock on file : <path>\nSee the docs: ..."
92
+ if "Could not set lock on file" in str(exc):
93
+ raise IndexBusyError(retry_after_s=2) from exc
94
+ raise
95
+ self._timeout_ms = timeout_ms or settings.QUERY_TIMEOUT_MS
96
+ try:
97
+ self._conn = lb.Connection(self._db)
98
+ self._conn.set_query_timeout(self._timeout_ms)
99
+ except BaseException:
100
+ self._db.close()
101
+ raise
102
+
103
+ # ------------------------------------------------------------------
104
+ # Core query interface
105
+ # ------------------------------------------------------------------
106
+
107
+ def execute(self, query: str, params: QueryParams | None = None) -> lb.QueryResult:
108
+ """Executes a single Cypher query.
109
+
110
+ Args:
111
+ query: The Cypher query string to execute.
112
+ params: Optional dictionary of parameters to pass as
113
+ prepared-statement arguments.
114
+
115
+ Returns:
116
+ The raw Result object containing the query output.
117
+
118
+ Raises:
119
+ QueryTimeoutError: The query exceeded the configured timeout.
120
+ """
121
+ try:
122
+ result = self._conn.execute(query, params) if params else self._conn.execute(query)
123
+ except RuntimeError as exc:
124
+ # Real driver text for an exceeded set_query_timeout is exactly "Interrupted."
125
+ if "interrupted" in str(exc).lower():
126
+ raise QueryTimeoutError(timeout_ms=self._timeout_ms) from exc
127
+ raise
128
+ assert isinstance(result, lb.QueryResult), (
129
+ "a single-statement query must return a single QueryResult, not a list"
130
+ )
131
+ return result
132
+
133
+ def execute_many(self, statements: list[str]) -> None:
134
+ """Executes multiple Cypher statements in sequence.
135
+
136
+ Results from these statements are ignored. High-level DDL or bulk
137
+ initialisation should use this method.
138
+
139
+ Args:
140
+ statements: A list of Cypher query strings to execute.
141
+ """
142
+ for stmt in statements:
143
+ self._conn.execute(stmt)
144
+
145
+ # ------------------------------------------------------------------
146
+ # Convenience helpers
147
+ # ------------------------------------------------------------------
148
+
149
+ def node_table_names(self) -> list[str]:
150
+ """Retrieves the names of all node tables (labels) in the database.
151
+
152
+ Returns:
153
+ A list of strings representing the node table names.
154
+ """
155
+ return self._conn._get_node_table_names()
156
+
157
+ def rel_table_names(self) -> list[str]:
158
+ """Retrieves the names of all relationship tables in the database.
159
+
160
+ Returns:
161
+ A list of strings representing the relationship table names.
162
+ """
163
+ return [table["name"] for table in self._conn._get_rel_table_names()]
164
+
165
+ # ------------------------------------------------------------------
166
+ # Lifecycle
167
+ # ------------------------------------------------------------------
168
+
169
+ def close(self) -> None:
170
+ """Safely closes the connection and the database handle.
171
+
172
+ Any errors during closure are logged and suppressed to ensure
173
+ cleanup completes without crashing the caller.
174
+ """
175
+ try:
176
+ self._conn.close()
177
+ except Exception:
178
+ logger.exception("Error closing database connection")
179
+ try:
180
+ self._db.close()
181
+ except Exception:
182
+ logger.exception("Error closing database handle")
183
+
184
+ def __enter__(self) -> GraphDB:
185
+ """Enables usage of GraphDB as a context manager.
186
+
187
+ Returns:
188
+ The GraphDB instance itself.
189
+ """
190
+ return self
191
+
192
+ def __exit__(
193
+ self,
194
+ exc_type: type[BaseException] | None,
195
+ exc_val: BaseException | None,
196
+ tb: TracebackType | None,
197
+ ) -> None:
198
+ """Automatically closes the database on exit from a context block.
199
+
200
+ Args:
201
+ exc_type: The type of the exception raised, if any.
202
+ exc_val: The exception instance raised, if any.
203
+ tb: The traceback object, if any.
204
+ """
205
+ self.close()
@@ -0,0 +1,165 @@
1
+ """Cypher DDL statements to initialise the Compass knowledge graph schema.
2
+
3
+ This module defines the graph database schema for storing Python source code
4
+ information, including nodes for files, classes, and functions, and their
5
+ inter-relationships.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from compass_core.graph.db import GraphDB
11
+
12
+ # ---------------------------------------------------------------------------
13
+ # DDL — node tables (must be created before rel tables)
14
+ # ---------------------------------------------------------------------------
15
+
16
+ _NODE_TABLES: list[str] = [
17
+ # File: A source file entry.
18
+ # - path (PK): Repo-relative POSIX path of the source file.
19
+ # - module_name: Dotted Python module name (e.g., "os.path").
20
+ # - content_hash: SHA-256 hex digest for change detection.
21
+ """
22
+ CREATE NODE TABLE IF NOT EXISTS File(
23
+ path STRING,
24
+ module_name STRING,
25
+ content_hash STRING,
26
+ PRIMARY KEY(path)
27
+ )
28
+ """,
29
+ # Class: A class definition extracted from source.
30
+ # - id (PK): Public id, format: '<repo-relative path>::ClassName'.
31
+ # - name: The unqualified name of the class.
32
+ # - qualified_name: Dotted module plus class name, e.g. 'app.models.User'.
33
+ # - file_path: Reference to the file where the class is defined.
34
+ # - line_start: 1-indexed line where the class definition starts.
35
+ # - line_end: 1-indexed line where the class definition ends.
36
+ # - source: The entire source code block of the class.
37
+ # - docstring: The extracted class-level docstring.
38
+ # - bases: A list of raw names for base classes (STRING[]).
39
+ """
40
+ CREATE NODE TABLE IF NOT EXISTS Class(
41
+ id STRING,
42
+ name STRING,
43
+ qualified_name STRING,
44
+ file_path STRING,
45
+ line_start INT64,
46
+ line_end INT64,
47
+ source STRING,
48
+ docstring STRING,
49
+ bases STRING[],
50
+ PRIMARY KEY(id)
51
+ )
52
+ """,
53
+ # Function: A function or method definition.
54
+ # - id (PK): Public id, format: '<repo-relative path>::[ClassName.]name'.
55
+ # - name: The name of the function or method.
56
+ # - qualified_name: Dotted module plus '[ClassName.]name', e.g. 'app.models.User.save'.
57
+ # - signature: The full 'def' line including arguments and return type.
58
+ # - file_path: Reference to the file where the function is defined.
59
+ # - line_start: 1-indexed line (including decorators) where it starts.
60
+ # - line_end: 1-indexed line where the function body ends.
61
+ # - source: The entire source code block of the function.
62
+ # - docstring: The extracted function-level docstring.
63
+ # - is_method: Boolean indicating if it's defined inside a class.
64
+ # - parent_class: The name of the owning class if is_method=True.
65
+ # - decorators: A list of decorator names applied (STRING[]).
66
+ # - raw_calls: Unresolved call strings within the body (STRING[]).
67
+ """
68
+ CREATE NODE TABLE IF NOT EXISTS Function(
69
+ id STRING,
70
+ name STRING,
71
+ qualified_name STRING,
72
+ signature STRING,
73
+ file_path STRING,
74
+ line_start INT64,
75
+ line_end INT64,
76
+ source STRING,
77
+ docstring STRING,
78
+ is_method BOOLEAN,
79
+ parent_class STRING,
80
+ decorators STRING[],
81
+ raw_calls STRING[],
82
+ PRIMARY KEY(id)
83
+ )
84
+ """,
85
+ # ExternalFunction: A call to an external library function.
86
+ # - full_ref (PK): Fully qualified name (e.g., "jwt.decode").
87
+ # - name: The name of the function.
88
+ # - library: The name of the library or package.
89
+ """
90
+ CREATE NODE TABLE IF NOT EXISTS ExternalFunction(
91
+ full_ref STRING,
92
+ name STRING,
93
+ library STRING,
94
+ PRIMARY KEY(full_ref)
95
+ )
96
+ """,
97
+ ]
98
+
99
+ # ---------------------------------------------------------------------------
100
+ # DDL — relationship tables
101
+ # ---------------------------------------------------------------------------
102
+
103
+ _REL_TABLES: list[str] = [
104
+ # CONTAINS: Unified containment model (File→Class, File→Function, Class→Function).
105
+ """
106
+ CREATE REL TABLE IF NOT EXISTS CONTAINS(
107
+ FROM File TO Class,
108
+ FROM File TO Function,
109
+ FROM Class TO Function
110
+ )
111
+ """,
112
+ # IMPORTS: Dependency between files when one module imports another.
113
+ """
114
+ CREATE REL TABLE IF NOT EXISTS IMPORTS(
115
+ FROM File TO File
116
+ )
117
+ """,
118
+ # INHERITS: Class inheritance relationship (Subclass → Superclass).
119
+ """
120
+ CREATE REL TABLE IF NOT EXISTS INHERITS(
121
+ FROM Class TO Class
122
+ )
123
+ """,
124
+ # CALLS: Internal function execution flow with metadata.
125
+ # - confidence (FLOAT): Probability score from the call graph analyzer.
126
+ # - call_type (STRING): A ``CallType`` value (e.g., "DIRECT_IMPORT", "MRO").
127
+ # - line (INT64): 1-indexed line number where the call occurs.
128
+ """
129
+ CREATE REL TABLE IF NOT EXISTS CALLS(
130
+ FROM Function TO Function,
131
+ confidence FLOAT,
132
+ call_type STRING,
133
+ line INT64
134
+ )
135
+ """,
136
+ # CALLS_EXTERN: Function execution flow to external library functions.
137
+ # - line (INT64): 1-indexed line number where the call occurs.
138
+ """
139
+ CREATE REL TABLE IF NOT EXISTS CALLS_EXTERN(
140
+ FROM Function TO ExternalFunction,
141
+ line INT64
142
+ )
143
+ """,
144
+ ]
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # Public API
149
+ # ---------------------------------------------------------------------------
150
+
151
+
152
+ def create_schema(db: GraphDB) -> None:
153
+ """Create all node and relationship tables if they do not already exist.
154
+
155
+ All statements use the 'IF NOT EXISTS' clause to ensure safety when
156
+ running against an initialized database. Node tables are always
157
+ created before relationship tables to avoid schema violations.
158
+
159
+ Args:
160
+ db: An instance of GraphDB to execute the DDL statements against.
161
+ """
162
+ for stmt in _NODE_TABLES:
163
+ db.execute(stmt)
164
+ for stmt in _REL_TABLES:
165
+ db.execute(stmt)
@@ -0,0 +1,133 @@
1
+ """The ids the graph stores for classes and functions: the public ids queries return.
2
+
3
+ Parsing and resolution key definitions by an internal, language-namespaced id
4
+ (``py::app.models::User.save``). The graph stores the public id instead
5
+ (``app/models.py::User.save``), plus the dotted qualified name, so every lookup by a
6
+ public id is a primary-key match and no reader has to translate between the two.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Mapping
12
+ from dataclasses import dataclass, replace
13
+ from pathlib import PurePosixPath
14
+
15
+ from compass_core.ir.graph import ResolvedGraph
16
+ from compass_core.ir.models import Definition
17
+
18
+ SEP = "::"
19
+ """Separator between a public id's file path and its ``[Class.]name`` tail."""
20
+
21
+
22
+ @dataclass(frozen=True, slots=True, kw_only=True)
23
+ class StoredGraph:
24
+ """A resolved graph re-keyed by public ids, ready to write.
25
+
26
+ Attributes:
27
+ graph: The graph with every definition id and edge endpoint replaced by its
28
+ public id. File paths and external references are unchanged.
29
+ qualified_names: Dotted qualified name of every definition, by public id.
30
+
31
+ Example:
32
+ ``class User`` with method ``save`` in ``app/models.py`` gives definitions with ids
33
+ ``"app/models.py::User"`` and ``"app/models.py::User.save"``, and
34
+ ``qualified_names["app/models.py::User.save"] == "app.models.User.save"``.
35
+ """
36
+
37
+ graph: ResolvedGraph
38
+ qualified_names: Mapping[str, str]
39
+
40
+
41
+ def _module_from_path(file_path: str) -> str:
42
+ parts = list(PurePosixPath(file_path).with_suffix("").parts)
43
+ if parts and parts[-1] == "__init__":
44
+ parts.pop()
45
+ return ".".join(parts)
46
+
47
+
48
+ def public_id(definition: Definition) -> str:
49
+ """The public id of a definition: ``<repo-relative path>::<[Class.]name>``.
50
+
51
+ Args:
52
+ definition: A parsed class, function or method.
53
+
54
+ Returns:
55
+ The public id.
56
+
57
+ Example:
58
+ ``User.save`` in ``app/models.py`` (internal id ``py::app.models::User.save``)
59
+ gives ``"app/models.py::User.save"``.
60
+ """
61
+ return f"{definition.file_path}{SEP}{definition.id.rsplit(SEP, 1)[-1]}"
62
+
63
+
64
+ def qualified_name(definition: Definition) -> str:
65
+ """The dotted qualified name of a definition: module plus ``[Class.]name``.
66
+
67
+ Args:
68
+ definition: A parsed class, function or method.
69
+
70
+ Returns:
71
+ The qualified name; the module comes from the internal id, or from the file
72
+ path when the id carries none.
73
+
74
+ Example:
75
+ ``User.save`` in ``app/models.py`` gives ``"app.models.User.save"``.
76
+ """
77
+ head, _, tail = definition.id.rpartition(SEP)
78
+ module = head.split(SEP, 1)[1] if SEP in head else _module_from_path(definition.file_path)
79
+ return f"{module}.{tail}" if module else tail
80
+
81
+
82
+ def with_stored_ids(graph: ResolvedGraph) -> StoredGraph:
83
+ """Re-key a resolved graph by public ids.
84
+
85
+ Edge endpoints that are not definitions of the graph keep their value, so they
86
+ still match no node and are dropped by the writer, as before.
87
+
88
+ Args:
89
+ graph: The graph produced by resolution.
90
+
91
+ Returns:
92
+ A copy with public ids, and the qualified name of every definition.
93
+
94
+ Example:
95
+ A ``CallEdge(caller_id="py::app.api::handle", callee_id="py::app.auth::parse")``
96
+ becomes ``CallEdge(caller_id="app/api.py::handle", callee_id="app/auth.py::parse")``.
97
+ """
98
+ definitions = [d for f in graph.files for d in f.definitions]
99
+ ids = {d.id: public_id(d) for d in definitions}
100
+
101
+ def new(old: str) -> str:
102
+ return ids.get(old, old)
103
+
104
+ files = [
105
+ replace(
106
+ f,
107
+ definitions=[
108
+ replace(
109
+ d, id=new(d.id), parent_id=None if d.parent_id is None else new(d.parent_id)
110
+ )
111
+ for d in f.definitions
112
+ ],
113
+ )
114
+ for f in graph.files
115
+ ]
116
+ return StoredGraph(
117
+ graph=ResolvedGraph(
118
+ files=files,
119
+ call_edges=[
120
+ replace(e, caller_id=new(e.caller_id), callee_id=new(e.callee_id))
121
+ for e in graph.call_edges
122
+ ],
123
+ external_call_edges=[
124
+ replace(e, caller_id=new(e.caller_id)) for e in graph.external_call_edges
125
+ ],
126
+ import_edges=list(graph.import_edges),
127
+ inherit_edges=[
128
+ replace(e, child_id=new(e.child_id), parent_id=new(e.parent_id))
129
+ for e in graph.inherit_edges
130
+ ],
131
+ ),
132
+ qualified_names={ids[d.id]: qualified_name(d) for d in definitions},
133
+ )