pythograph 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.
- pythograph/__init__.py +7 -0
- pythograph/__main__.py +6 -0
- pythograph/cli/__init__.py +1 -0
- pythograph/cli/common.py +73 -0
- pythograph/cli/graph_commands.py +383 -0
- pythograph/cli/main.py +450 -0
- pythograph/exchange/__init__.py +1 -0
- pythograph/exchange/document.py +280 -0
- pythograph/exchange/order.py +168 -0
- pythograph/exchange/persistence.py +144 -0
- pythograph/exchange/scope.py +271 -0
- pythograph/exchange/template.py +177 -0
- pythograph/graph/__init__.py +0 -0
- pythograph/graph/build.py +511 -0
- pythograph/graph/calls.py +381 -0
- pythograph/graph/collect.py +383 -0
- pythograph/graph/document.py +272 -0
- pythograph/graph/framework.py +176 -0
- pythograph/graph/framework_table.py +1728 -0
- pythograph/graph/index.py +262 -0
- pythograph/graph/limitations.py +149 -0
- pythograph/graph/model.py +120 -0
- pythograph/graph/mro.py +314 -0
- pythograph/graph/revision.py +73 -0
- pythograph/graph/roots.py +122 -0
- pythograph/graph/scope.py +882 -0
- pythograph/graph/traversal.py +474 -0
- pythograph/graph/values.py +129 -0
- pythograph/persistence/__init__.py +0 -0
- pythograph/persistence/command.py +288 -0
- pythograph/persistence/django/__init__.py +0 -0
- pythograph/persistence/django/apps.py +231 -0
- pythograph/persistence/django/catalog.py +1120 -0
- pythograph/persistence/django/declarations.py +128 -0
- pythograph/persistence/django/fields.py +352 -0
- pythograph/persistence/django/queries.py +998 -0
- pythograph/persistence/django/settings.py +384 -0
- pythograph/persistence/location.py +33 -0
- pythograph/persistence/model.py +102 -0
- pythograph/persistence/names.py +214 -0
- pythograph/persistence/scope.py +261 -0
- pythograph/persistence/sql.py +795 -0
- pythograph/persistence/sqlalchemy/__init__.py +0 -0
- pythograph/persistence/sqlalchemy/catalog.py +1082 -0
- pythograph/persistence/sqlalchemy/queries.py +540 -0
- pythograph/persistence/sqltext.py +296 -0
- pythograph/py.typed +0 -0
- pythograph/routes/__init__.py +1 -0
- pythograph/routes/classes.py +493 -0
- pythograph/routes/command.py +137 -0
- pythograph/routes/django/__init__.py +1 -0
- pythograph/routes/django/drf.py +389 -0
- pythograph/routes/django/extract.py +527 -0
- pythograph/routes/django/settings.py +141 -0
- pythograph/routes/django/urlconf.py +951 -0
- pythograph/routes/django/views.py +378 -0
- pythograph/routes/flask/__init__.py +1 -0
- pythograph/routes/flask/extract.py +1018 -0
- pythograph/routes/flask/rules.py +256 -0
- pythograph/routes/model.py +134 -0
- pythograph/routes/pattern.py +883 -0
- pythograph/routes/versions.py +232 -0
- pythograph/source/__init__.py +1 -0
- pythograph/source/evaluate.py +164 -0
- pythograph/source/project.py +371 -0
- pythograph/source/symbols.py +562 -0
- pythograph-0.1.0.dist-info/METADATA +336 -0
- pythograph-0.1.0.dist-info/RECORD +71 -0
- pythograph-0.1.0.dist-info/WHEEL +4 -0
- pythograph-0.1.0.dist-info/entry_points.txt +2 -0
- pythograph-0.1.0.dist-info/licenses/LICENSE +21 -0
pythograph/__init__.py
ADDED
pythograph/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""명령행 인터페이스(인자 해석, 종료 코드 계약)."""
|
pythograph/cli/common.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""CLI 공통 도우미: 사용법·입력 오류 예외, 시각·프로젝트 경로 검증.
|
|
2
|
+
|
|
3
|
+
`main`과 `graph_commands`가 함께 쓴다(순환 import를 피하려고 따로 둔다).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
#: 계약이 금지하는 식별자 문자다(제어 문자, U+2028/2029, UTF-8로 쓸 수 없는 짝 없는 서로게이트).
|
|
14
|
+
FORBIDDEN = re.compile("[\u0000-\u001f\u007f-\u009f\u2028\u2029\ud800-\udfff]")
|
|
15
|
+
|
|
16
|
+
#: `--generated-at` 형식이다.
|
|
17
|
+
TIMESTAMP = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,6})?Z")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class UsageError(Exception):
|
|
21
|
+
"""사용법 오류(64). 메시지는 해결 방향을 담는다."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class InputError(Exception):
|
|
25
|
+
"""입력 오류(2). 메시지는 원인과 해결 방향을 담는다."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_timestamp(value: str | None) -> datetime | None:
|
|
29
|
+
"""`--generated-at` 값을 해석한다.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
value: 값.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
UTC 시각 또는 None.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
UsageError: 형식 위반.
|
|
39
|
+
"""
|
|
40
|
+
if value is None:
|
|
41
|
+
return None
|
|
42
|
+
if TIMESTAMP.fullmatch(value) is None:
|
|
43
|
+
raise UsageError("--generated-at takes a UTC timestamp such as 2026-01-01T00:00:00.000Z.")
|
|
44
|
+
try:
|
|
45
|
+
return datetime.strptime(value[:-1] + ("" if "." in value else ".0"), "%Y-%m-%dT%H:%M:%S.%f").replace(
|
|
46
|
+
tzinfo=timezone.utc
|
|
47
|
+
)
|
|
48
|
+
except ValueError as error:
|
|
49
|
+
raise UsageError("--generated-at takes a valid UTC timestamp such as 2026-01-01T00:00:00.000Z.") from error
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def resolve_project(argument: str) -> Path:
|
|
53
|
+
"""프로젝트 경로를 realpath로 정규화하고 디렉터리인지 확인한다.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
argument: `--project` 값.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
realpath.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
InputError: 디렉터리가 아니거나 계약이 금지하는 문자를 담을 때.
|
|
63
|
+
"""
|
|
64
|
+
try:
|
|
65
|
+
root = Path(os.path.realpath(argument))
|
|
66
|
+
is_directory = root.is_dir()
|
|
67
|
+
except (OSError, ValueError):
|
|
68
|
+
is_directory = False
|
|
69
|
+
if not is_directory:
|
|
70
|
+
raise InputError("--project does not name a readable directory; pass the project root.")
|
|
71
|
+
if FORBIDDEN.search(root.as_posix()):
|
|
72
|
+
raise InputError("the project path contains characters the exchange format forbids; rename or move it.")
|
|
73
|
+
return root
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
"""`graph`·`reach`·`impact` 명령: 인자 해석, 그래프 생성, 문서 조립.
|
|
2
|
+
|
|
3
|
+
`reach`는 root가 기대는 쪽(`dependencies`), `impact`는 root에 기대는 쪽(`dependents`)을 isthmus `language-traversal`
|
|
4
|
+
v1로 낸다. 그래프 정점이 아닌 root가 섞이면 나머지 root로 순회한 문서를 쓰고 64로 끝난다(tsograph·cartograph와 같은
|
|
5
|
+
root-not-found 규칙). 문서 없는 순수 사용법 오류도 64이며 표준 출력이 비어 있어 구별된다.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
import sys
|
|
12
|
+
from dataclasses import dataclass, replace
|
|
13
|
+
from datetime import datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import TextIO
|
|
16
|
+
|
|
17
|
+
from pythograph import __version__
|
|
18
|
+
from pythograph.cli.common import InputError, UsageError, parse_timestamp, resolve_project
|
|
19
|
+
from pythograph.exchange.document import DocumentLimitError, encode_document
|
|
20
|
+
from pythograph.graph.build import build_graph
|
|
21
|
+
from pythograph.graph.document import (
|
|
22
|
+
GraphHeader,
|
|
23
|
+
TraversalInput,
|
|
24
|
+
build_graph_document,
|
|
25
|
+
build_traversal_document,
|
|
26
|
+
)
|
|
27
|
+
from pythograph.graph.model import DISPATCH_MODES, CallGraph
|
|
28
|
+
from pythograph.graph.revision import git_revision
|
|
29
|
+
from pythograph.graph.roots import FORBIDDEN_CHARACTERS, RootsError, collect_roots
|
|
30
|
+
from pythograph.graph.traversal import (
|
|
31
|
+
MAX_REACHED,
|
|
32
|
+
MAX_TRAVERSAL_DEPTH,
|
|
33
|
+
TraversalRequest,
|
|
34
|
+
TraversalResult,
|
|
35
|
+
traverse,
|
|
36
|
+
)
|
|
37
|
+
from pythograph.source.project import Project
|
|
38
|
+
|
|
39
|
+
GRAPH_USAGE = """Usage: pythograph graph --project <root> [--include-tests] [--revision <id>]
|
|
40
|
+
[--generated-at <timestamp>] [--format json]
|
|
41
|
+
|
|
42
|
+
Build the project's Python call graph with the standard-library ast and write a pythograph-graph v1
|
|
43
|
+
snapshot (nodes, edges with evidence tiers, unresolved-call counts, limitations) to stdout.
|
|
44
|
+
|
|
45
|
+
Options:
|
|
46
|
+
--project <root> Project root; node ids and locations are relative to it
|
|
47
|
+
--include-tests Also make test sources graph nodes
|
|
48
|
+
--revision <id> Source revision to record (default: git HEAD when the work tree is clean)
|
|
49
|
+
--generated-at <timestamp> Fixed generatedAt (YYYY-MM-DDTHH:MM:SS.sssZ) for byte-identical output
|
|
50
|
+
--format json Output format (json is the only format)
|
|
51
|
+
|
|
52
|
+
Exit codes: 0 success, 2 unreadable project or oversized output, 64 usage error.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
_TRAVERSAL_OPTIONS = """Options:
|
|
56
|
+
--project <root> Project root; ids are pythograph symbol ids (routes and schema symbol.usr)
|
|
57
|
+
--dispatch <mode> direct (default), bound (currently the direct graph), or candidates
|
|
58
|
+
(also subclass overrides as candidate edges)
|
|
59
|
+
--max-depth <n> Maximum edge count from a root, 1-128 (default 128)
|
|
60
|
+
--max-reached <n> Maximum reached symbols, 1-100000 (default 100000)
|
|
61
|
+
--roots-from <file|-> More roots from a JSON string array or a bridge-facts document (- is stdin)
|
|
62
|
+
--include-tests Also make test sources graph nodes
|
|
63
|
+
--revision <id> Source revision to record (default: git HEAD when the work tree is clean)
|
|
64
|
+
--generated-at <timestamp> Fixed generatedAt (YYYY-MM-DDTHH:MM:SS.sssZ) for byte-identical output
|
|
65
|
+
--format json Output format (json is the only format)
|
|
66
|
+
|
|
67
|
+
Ids that are not graph nodes are listed without symbol (root-not-found); the document is still
|
|
68
|
+
written and the command exits 64. Exit codes: 0 success, 2 unreadable project or oversized output,
|
|
69
|
+
64 usage error (empty stdout) or root-not-found (document written).
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
REACH_USAGE = (
|
|
73
|
+
"""Usage: pythograph reach --project <root> [options] [--] <id>...
|
|
74
|
+
|
|
75
|
+
Write the symbols the roots depend on (direction "dependencies") as an isthmus language-traversal
|
|
76
|
+
v1 document.
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
+ _TRAVERSAL_OPTIONS
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
IMPACT_USAGE = (
|
|
83
|
+
"""Usage: pythograph impact --project <root> [options] [--] <id>...
|
|
84
|
+
|
|
85
|
+
Write the symbols that depend on the roots (direction "dependents") as an isthmus
|
|
86
|
+
language-traversal v1 document.
|
|
87
|
+
|
|
88
|
+
"""
|
|
89
|
+
+ _TRAVERSAL_OPTIONS
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
#: graph 명령의 값 옵션이다.
|
|
93
|
+
_GRAPH_VALUES = ("--project", "--format", "--generated-at", "--revision")
|
|
94
|
+
|
|
95
|
+
#: reach·impact 명령의 값 옵션이다.
|
|
96
|
+
_TRAVERSAL_VALUES = (*_GRAPH_VALUES, "--dispatch", "--max-depth", "--max-reached", "--roots-from")
|
|
97
|
+
|
|
98
|
+
#: 불리언 옵션이다.
|
|
99
|
+
_FLAGS = ("--include-tests", "--help")
|
|
100
|
+
|
|
101
|
+
#: `--revision` 최대 길이다.
|
|
102
|
+
MAX_REVISION_LENGTH = 256
|
|
103
|
+
|
|
104
|
+
#: 양의 정수 인자 형식이다.
|
|
105
|
+
_INTEGER = re.compile(r"[1-9][0-9]{0,6}")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class RootNotFoundExit(Exception):
|
|
109
|
+
"""정점이 아닌 root가 있어 문서를 쓰고 64로 끝낸다.
|
|
110
|
+
|
|
111
|
+
Attributes:
|
|
112
|
+
text: 표준 출력에 쓸 문서.
|
|
113
|
+
missing: 정점이 아닌 root 수.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, text: str, missing: int) -> None:
|
|
117
|
+
"""예외를 만든다.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
text: 문서.
|
|
121
|
+
missing: 정점이 아닌 root 수.
|
|
122
|
+
"""
|
|
123
|
+
super().__init__("root-not-found")
|
|
124
|
+
self.text = text
|
|
125
|
+
self.missing = missing
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@dataclass(frozen=True)
|
|
129
|
+
class CommandLine:
|
|
130
|
+
"""해석한 인자.
|
|
131
|
+
|
|
132
|
+
Attributes:
|
|
133
|
+
values: 값 옵션.
|
|
134
|
+
flags: 불리언 옵션.
|
|
135
|
+
positional: 위치 인자.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
values: dict[str, str]
|
|
139
|
+
flags: set[str]
|
|
140
|
+
positional: list[str]
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def parse_command_line(arguments: list[str], value_flags: tuple[str, ...], usage: str, positional: bool) -> CommandLine:
|
|
144
|
+
"""옵션과 위치 인자를 나눈다. `--` 뒤는 모두 위치 인자다.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
arguments: 인자 목록.
|
|
148
|
+
value_flags: 허용하는 값 옵션.
|
|
149
|
+
usage: 오류 문구에 붙일 사용법.
|
|
150
|
+
positional: 위치 인자를 받는지.
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
해석한 인자.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
UsageError: 모르는·반복·값 없는 옵션, 받지 않는 위치 인자.
|
|
157
|
+
"""
|
|
158
|
+
result = CommandLine({}, set(), [])
|
|
159
|
+
index = 0
|
|
160
|
+
while index < len(arguments):
|
|
161
|
+
token = arguments[index]
|
|
162
|
+
if token == "--" and positional:
|
|
163
|
+
result.positional.extend(arguments[index + 1 :])
|
|
164
|
+
break
|
|
165
|
+
if not token.startswith("--"):
|
|
166
|
+
if not positional:
|
|
167
|
+
raise UsageError("unexpected positional argument.\n" + usage)
|
|
168
|
+
result.positional.append(token)
|
|
169
|
+
index += 1
|
|
170
|
+
continue
|
|
171
|
+
index = _parse_option(arguments, index, value_flags, usage, result)
|
|
172
|
+
return result
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _parse_option(
|
|
176
|
+
arguments: list[str], index: int, value_flags: tuple[str, ...], usage: str, result: CommandLine
|
|
177
|
+
) -> int:
|
|
178
|
+
"""옵션 하나를 해석한다.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
arguments: 인자 목록.
|
|
182
|
+
index: 옵션 위치.
|
|
183
|
+
value_flags: 허용하는 값 옵션.
|
|
184
|
+
usage: 사용법.
|
|
185
|
+
result: 채울 결과.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
다음 인자 위치.
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
UsageError: 잘못된 옵션.
|
|
192
|
+
"""
|
|
193
|
+
name, has_inline, inline = arguments[index].partition("=")
|
|
194
|
+
if name in _FLAGS and not has_inline and name not in result.flags:
|
|
195
|
+
result.flags.add(name)
|
|
196
|
+
return index + 1
|
|
197
|
+
if name not in value_flags or name in result.values:
|
|
198
|
+
raise UsageError("unknown or repeated option.\n" + usage)
|
|
199
|
+
value = inline if has_inline else (arguments[index + 1] if index + 1 < len(arguments) else "")
|
|
200
|
+
if not value or (not has_inline and value.startswith("--")):
|
|
201
|
+
raise UsageError(f"{name} needs a value.\n" + usage)
|
|
202
|
+
result.values[name] = value
|
|
203
|
+
return index + (1 if has_inline else 2)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def run_graph(arguments: list[str]) -> str:
|
|
207
|
+
"""graph 명령을 실행한다.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
arguments: graph 뒤 인자.
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
JSON 문서.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
UsageError: 사용법 오류.
|
|
217
|
+
InputError: 입력 오류.
|
|
218
|
+
"""
|
|
219
|
+
line = parse_command_line(arguments, _GRAPH_VALUES, GRAPH_USAGE, positional=False)
|
|
220
|
+
if "--help" in line.flags:
|
|
221
|
+
return GRAPH_USAGE
|
|
222
|
+
root, header = _common(line, GRAPH_USAGE)
|
|
223
|
+
graph = build_graph(Project.open(root), "--include-tests" in line.flags)
|
|
224
|
+
try:
|
|
225
|
+
return encode_document(build_graph_document(header, graph))
|
|
226
|
+
except DocumentLimitError as error:
|
|
227
|
+
# 스냅샷은 isthmus 입력이 아니므로 상한만 알리고, isthmus가 받는 순회 문서(reach·impact)를 안내한다.
|
|
228
|
+
raise InputError(
|
|
229
|
+
"the graph snapshot would exceed the 16 Mi character output limit; scan a smaller project root, "
|
|
230
|
+
"or use reach/impact, which write only the traversed symbols."
|
|
231
|
+
) from error
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def run_traversal(arguments: list[str], direction: str, stdin: TextIO | None = None) -> str:
|
|
235
|
+
"""reach·impact 명령을 실행한다.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
arguments: 명령 뒤 인자.
|
|
239
|
+
direction: `dependencies`(reach) 또는 `dependents`(impact).
|
|
240
|
+
stdin: 표준 입력(`--roots-from -`, 테스트 주입용).
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
JSON 문서.
|
|
244
|
+
|
|
245
|
+
Raises:
|
|
246
|
+
UsageError: 사용법 오류.
|
|
247
|
+
InputError: 입력 오류.
|
|
248
|
+
RootNotFoundExit: 정점이 아닌 root가 있을 때(문서를 싣는다).
|
|
249
|
+
"""
|
|
250
|
+
usage = REACH_USAGE if direction == "dependencies" else IMPACT_USAGE
|
|
251
|
+
line = parse_command_line(arguments, _TRAVERSAL_VALUES, usage, positional=True)
|
|
252
|
+
if "--help" in line.flags:
|
|
253
|
+
return usage
|
|
254
|
+
dispatch = line.values.get("--dispatch", "direct")
|
|
255
|
+
if dispatch not in DISPATCH_MODES:
|
|
256
|
+
raise UsageError("--dispatch must be direct, bound, or candidates.")
|
|
257
|
+
max_depth = _bounded(line.values.get("--max-depth"), MAX_TRAVERSAL_DEPTH, "--max-depth")
|
|
258
|
+
max_reached = _bounded(line.values.get("--max-reached"), MAX_REACHED, "--max-reached")
|
|
259
|
+
try:
|
|
260
|
+
requested = collect_roots(line.positional, line.values.get("--roots-from"), stdin or sys.stdin)
|
|
261
|
+
except RootsError as error:
|
|
262
|
+
raise UsageError(str(error)) from error
|
|
263
|
+
root, header = _common(line, usage)
|
|
264
|
+
graph = build_graph(Project.open(root), "--include-tests" in line.flags)
|
|
265
|
+
request = TraversalRequest(requested, direction, max_depth, max_reached, dispatch)
|
|
266
|
+
document, missing = traversal_document(graph, header, request)
|
|
267
|
+
text = _encode(document)
|
|
268
|
+
if missing:
|
|
269
|
+
raise RootNotFoundExit(text, missing)
|
|
270
|
+
return text
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def traversal_document(
|
|
274
|
+
graph: CallGraph, header: GraphHeader, request: TraversalRequest
|
|
275
|
+
) -> tuple[dict[str, object], int]:
|
|
276
|
+
"""정점인 root로만 순회하고, root 인덱스를 요청 순서로 옮겨 문서를 만든다.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
graph: 호출 그래프.
|
|
280
|
+
header: 머리 필드.
|
|
281
|
+
request: 요청 순서의 전체 root를 담은 순회 요청.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
(문서, 정점이 아닌 root 수).
|
|
285
|
+
"""
|
|
286
|
+
nodes = graph.node_map()
|
|
287
|
+
resolved = tuple(root for root in request.root_ids if root in nodes)
|
|
288
|
+
missing = frozenset(root for root in request.root_ids if root not in nodes)
|
|
289
|
+
result = TraversalResult((), (), False, False)
|
|
290
|
+
if resolved:
|
|
291
|
+
result = remap_roots(traverse(graph, replace(request, root_ids=resolved)), resolved, request.root_ids)
|
|
292
|
+
data = TraversalInput(header, graph, request.direction, request.dispatch, request.root_ids, missing, result)
|
|
293
|
+
return build_traversal_document(data), len(missing)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def remap_roots(result: TraversalResult, resolved: tuple[str, ...], requested: tuple[str, ...]) -> TraversalResult:
|
|
297
|
+
"""정점인 root 순서의 인덱스를 요청 순서의 인덱스로 옮긴다(순서를 보존하므로 규칙 결과가 그대로다).
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
result: 순회 결과.
|
|
301
|
+
resolved: 정점인 root(요청 순서 유지).
|
|
302
|
+
requested: 요청 순서의 전체 root.
|
|
303
|
+
|
|
304
|
+
Returns:
|
|
305
|
+
옮긴 순회 결과.
|
|
306
|
+
"""
|
|
307
|
+
if resolved == requested:
|
|
308
|
+
return result
|
|
309
|
+
position = {root: index for index, root in enumerate(requested)}
|
|
310
|
+
mapping = [position[root] for root in resolved]
|
|
311
|
+
reached = tuple(replace(entry, roots=tuple(mapping[index] for index in entry.roots)) for entry in result.reached)
|
|
312
|
+
return replace(result, reached=reached)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _bounded(value: str | None, limit: int, name: str) -> int:
|
|
316
|
+
"""1~limit 정수 인자를 해석한다(없으면 limit).
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
value: 값.
|
|
320
|
+
limit: 상한(기본값).
|
|
321
|
+
name: 옵션 이름.
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
정수.
|
|
325
|
+
|
|
326
|
+
Raises:
|
|
327
|
+
UsageError: 형식·범위 위반.
|
|
328
|
+
"""
|
|
329
|
+
if value is None:
|
|
330
|
+
return limit
|
|
331
|
+
if _INTEGER.fullmatch(value) is None or int(value) > limit:
|
|
332
|
+
raise UsageError(f"{name} takes an integer from 1 to {limit}.")
|
|
333
|
+
return int(value)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _common(line: CommandLine, usage: str) -> tuple[Path, GraphHeader]:
|
|
337
|
+
"""공통 인자(project·format·revision·generated-at)를 검증하고 머리 필드를 만든다.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
line: 해석한 인자.
|
|
341
|
+
usage: 사용법.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
(프로젝트 realpath, 머리 필드).
|
|
345
|
+
|
|
346
|
+
Raises:
|
|
347
|
+
UsageError: 사용법 오류.
|
|
348
|
+
"""
|
|
349
|
+
if line.values.get("--format", "json") != "json":
|
|
350
|
+
raise UsageError("--format supports only json.")
|
|
351
|
+
project_argument = line.values.get("--project")
|
|
352
|
+
if project_argument is None:
|
|
353
|
+
raise UsageError("--project <root> is required.\n" + usage)
|
|
354
|
+
revision = line.values.get("--revision")
|
|
355
|
+
if revision is not None and (len(revision) > MAX_REVISION_LENGTH or FORBIDDEN_CHARACTERS.search(revision)):
|
|
356
|
+
raise UsageError(f"--revision must be 1-{MAX_REVISION_LENGTH} characters without control characters.")
|
|
357
|
+
generated_at = parse_timestamp(line.values.get("--generated-at"))
|
|
358
|
+
root = resolve_project(project_argument)
|
|
359
|
+
header = GraphHeader(
|
|
360
|
+
tool_version=__version__,
|
|
361
|
+
generated_at=generated_at or datetime.now(timezone.utc),
|
|
362
|
+
project=root.as_posix(),
|
|
363
|
+
revision=revision if revision is not None else git_revision(root),
|
|
364
|
+
)
|
|
365
|
+
return root, header
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _encode(document: dict[str, object]) -> str:
|
|
369
|
+
"""문서를 직렬화한다.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
document: 문서.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
JSON 문자열.
|
|
376
|
+
|
|
377
|
+
Raises:
|
|
378
|
+
InputError: 출력이 상한을 넘을 때.
|
|
379
|
+
"""
|
|
380
|
+
try:
|
|
381
|
+
return encode_document(document)
|
|
382
|
+
except DocumentLimitError as error:
|
|
383
|
+
raise InputError(str(error)) from error
|