codecrate 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.
codecrate/markdown.py ADDED
@@ -0,0 +1,457 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections import defaultdict
5
+ from typing import Any
6
+
7
+ from .manifest import to_manifest
8
+ from .model import ClassRef, PackResult
9
+ from .parse import parse_symbols
10
+
11
+
12
+ def _anchor_for(defn_id: str, module: str, qualname: str) -> str:
13
+ # Anchors should be stable under dedupe: multiple defs can share the same
14
+ # canonical id, so we anchor by id only.
15
+ base = f"func-{defn_id}".lower()
16
+ safe = "".join(ch if ch.isalnum() else "-" for ch in base)
17
+ while "--" in safe:
18
+ safe = safe.replace("--", "-")
19
+ return safe.strip("-")
20
+
21
+
22
+ def _file_anchor(rel_path: str) -> str:
23
+ base = f"file-{rel_path}".lower()
24
+ safe = "".join(ch if ch.isalnum() else "-" for ch in base)
25
+ while "--" in safe:
26
+ safe = safe.replace("--", "-")
27
+ return safe.strip("-")
28
+
29
+
30
+ def _file_src_anchor(rel_path: str) -> str:
31
+ # Separate anchor namespace from _file_anchor(): index vs file content.
32
+ base = f"src-{rel_path}".lower()
33
+ safe = "".join(ch if ch.isalnum() else "-" for ch in base)
34
+ while "--" in safe:
35
+ safe = safe.replace("--", "-")
36
+ return safe.strip("-")
37
+
38
+
39
+ def _file_range(line_count: int) -> str:
40
+ return "(empty)" if line_count == 0 else f"(L1-{line_count})"
41
+
42
+
43
+ def _ensure_nl(s: str) -> str:
44
+ return s if (not s or s.endswith("\n")) else (s + "\n")
45
+
46
+
47
+ def _fence_lang_for(rel_path: str) -> str:
48
+ ext = rel_path.rsplit(".", 1)[-1].lower() if "." in rel_path else ""
49
+ return {
50
+ "py": "python",
51
+ "toml": "toml",
52
+ "rst": "rst",
53
+ "md": "markdown",
54
+ "txt": "text",
55
+ "ini": "ini",
56
+ "cfg": "ini",
57
+ "yaml": "yaml",
58
+ "yml": "yaml",
59
+ "json": "json",
60
+ }.get(ext, "text")
61
+
62
+
63
+ def _range_token(kind: str, key: str) -> str:
64
+ return f"<<CC:{kind}:{key}>>"
65
+
66
+
67
+ def _format_range(start: int | None, end: int | None) -> str:
68
+ if start is None or end is None or start > end:
69
+ return "(empty)"
70
+ return f"(L{start}-{end})"
71
+
72
+
73
+ def _extract_rel_path(line: str) -> str | None:
74
+ if not line.startswith("### `"):
75
+ return None
76
+ start = line.find("`") + 1
77
+ end = line.find("`", start)
78
+ if start <= 0 or end <= start:
79
+ return None
80
+ return line[start:end]
81
+
82
+
83
+ def _scan_file_blocks(lines: list[str]) -> dict[str, tuple[int, int] | None]:
84
+ ranges: dict[str, tuple[int, int] | None] = {}
85
+ in_files = False
86
+ i = 0
87
+ while i < len(lines):
88
+ line = lines[i]
89
+ if line.strip() == "## Files":
90
+ in_files = True
91
+ i += 1
92
+ continue
93
+ if in_files and line.startswith("## ") and line.strip() != "## Files":
94
+ break
95
+ if in_files and line.startswith("### `"):
96
+ rel = _extract_rel_path(line)
97
+ if rel is None:
98
+ i += 1
99
+ continue
100
+ j = i + 1
101
+ while j < len(lines) and not (
102
+ lines[j].strip().startswith("```") and lines[j].strip() != "```"
103
+ ):
104
+ j += 1
105
+ if j >= len(lines):
106
+ ranges[rel] = None
107
+ i = j
108
+ continue
109
+ start_line = j + 2
110
+ k = j + 1
111
+ while k < len(lines) and lines[k].strip() != "```":
112
+ k += 1
113
+ end_line = k
114
+ if start_line > end_line:
115
+ ranges[rel] = None
116
+ else:
117
+ ranges[rel] = (start_line, end_line)
118
+ i = k + 1
119
+ continue
120
+ i += 1
121
+ return ranges
122
+
123
+
124
+ def _scan_function_library(lines: list[str]) -> dict[str, tuple[int, int] | None]:
125
+ ranges: dict[str, tuple[int, int] | None] = {}
126
+ in_lib = False
127
+ i = 0
128
+ while i < len(lines):
129
+ line = lines[i]
130
+ if line.strip() == "## Function Library":
131
+ in_lib = True
132
+ i += 1
133
+ continue
134
+ if in_lib and line.startswith("## ") and line.strip() != "## Function Library":
135
+ break
136
+ if in_lib and line.startswith("### "):
137
+ defn_id = line.replace("###", "").strip()
138
+ j = i + 1
139
+ while j < len(lines) and lines[j].strip() != "```python":
140
+ j += 1
141
+ if j >= len(lines):
142
+ i += 1
143
+ continue
144
+ start_line = j + 2
145
+ k = j + 1
146
+ while k < len(lines) and lines[k].strip() != "```":
147
+ k += 1
148
+ end_line = k
149
+ if start_line > end_line:
150
+ ranges[defn_id] = None
151
+ else:
152
+ ranges[defn_id] = (start_line, end_line)
153
+ i = k + 1
154
+ continue
155
+ i += 1
156
+ return ranges
157
+
158
+
159
+ def _apply_context_line_numbers(
160
+ text: str,
161
+ def_line_map: dict[str, tuple[int, int]],
162
+ class_line_map: dict[str, tuple[int, int]],
163
+ def_to_canon: dict[str, str],
164
+ def_to_file: dict[str, str],
165
+ class_to_file: dict[str, str],
166
+ use_stubs: bool,
167
+ ) -> str:
168
+ lines = text.splitlines()
169
+ file_ranges = _scan_file_blocks(lines)
170
+ func_ranges = _scan_function_library(lines) if use_stubs else {}
171
+
172
+ replacements: dict[str, str] = {}
173
+ for rel, rng in file_ranges.items():
174
+ token = _range_token("FILE", rel)
175
+ if rng is None:
176
+ replacements[token] = "(empty)"
177
+ else:
178
+ replacements[token] = _format_range(rng[0], rng[1])
179
+
180
+ for class_id, loc in class_line_map.items():
181
+ rel = class_to_file.get(class_id)
182
+ token = _range_token("CLASS", class_id)
183
+ file_range = file_ranges.get(rel) if rel else None
184
+ if file_range is None:
185
+ replacements[token] = _format_range(None, None)
186
+ continue
187
+ start = file_range[0] + loc[0] - 1
188
+ end = file_range[0] + loc[1] - 1
189
+ replacements[token] = _format_range(start, end)
190
+
191
+ for local_id, loc in def_line_map.items():
192
+ token = _range_token("DEF", local_id)
193
+ if use_stubs:
194
+ canon_id = def_to_canon.get(local_id)
195
+ canon_range = func_ranges.get(canon_id) if canon_id else None
196
+ if canon_range is not None:
197
+ replacements[token] = _format_range(canon_range[0], canon_range[1])
198
+ continue
199
+ rel = def_to_file.get(local_id)
200
+ file_range = file_ranges.get(rel) if rel else None
201
+ if file_range is None:
202
+ replacements[token] = _format_range(None, None)
203
+ continue
204
+ start = file_range[0] + loc[0] - 1
205
+ end = file_range[0] + loc[1] - 1
206
+ replacements[token] = _format_range(start, end)
207
+
208
+ for token, value in replacements.items():
209
+ text = text.replace(token, value)
210
+ return text
211
+
212
+
213
+ def _has_dedupe_effect(pack: PackResult) -> bool:
214
+ """
215
+ True iff at least one definition has local_id != id (meaning dedupe actually
216
+ collapsed identical bodies and rewrote canonical ids).
217
+ """
218
+ for fp in pack.files:
219
+ for d in fp.defs:
220
+ local_id = getattr(d, "local_id", d.id)
221
+ if local_id != d.id:
222
+ return True
223
+ return False
224
+
225
+
226
+ def _read_full_text(fp) -> str:
227
+ """Return the packed file contents."""
228
+ return fp.original_text
229
+
230
+
231
+ def _render_tree(paths: list[str]) -> str:
232
+ root: dict[str, Any] = {}
233
+ for p in paths:
234
+ cur = root
235
+ parts = [x for x in p.split("/") if x]
236
+ for part in parts[:-1]:
237
+ child = cur.setdefault(part, {})
238
+ cur = child if isinstance(child, dict) else {}
239
+ cur.setdefault(parts[-1], None)
240
+
241
+ def walk(node: dict[str, Any], prefix: str = "") -> list[str]:
242
+ items = sorted(node.items(), key=lambda kv: (kv[1] is None, kv[0].lower()))
243
+ out: list[str] = []
244
+ for i, (name, child) in enumerate(items):
245
+ last = i == len(items) - 1
246
+ branch = "└─ " if last else "├─ "
247
+ out.append(prefix + branch + name)
248
+ if isinstance(child, dict):
249
+ ext = " " if last else "│ "
250
+ out.extend(walk(child, prefix + ext))
251
+ return out
252
+
253
+ return "\n".join(walk(root))
254
+
255
+
256
+ def render_markdown( # noqa: C901
257
+ pack: PackResult,
258
+ canonical_sources: dict[str, str],
259
+ layout: str = "auto",
260
+ *,
261
+ include_manifest: bool = True,
262
+ ) -> str:
263
+ lines: list[str] = []
264
+ lines.append("# Codecrate Context Pack\n\n")
265
+ # Do not leak absolute local paths; keep the header root stable + relative.
266
+ lines.append("Root: `.`\n\n")
267
+ layout_norm = (layout or "auto").strip().lower()
268
+ if layout_norm not in {"auto", "stubs", "full"}:
269
+ layout_norm = "auto"
270
+ use_stubs = layout_norm == "stubs" or (
271
+ layout_norm == "auto" and _has_dedupe_effect(pack)
272
+ )
273
+ resolved_layout = "stubs" if use_stubs else "full"
274
+ lines.append(f"Layout: `{resolved_layout}`\n\n")
275
+
276
+ def_line_map: dict[str, tuple[int, int]] = {}
277
+ class_line_map: dict[str, tuple[int, int]] = {}
278
+ def_to_canon: dict[str, str] = {}
279
+ def_to_file: dict[str, str] = {}
280
+ class_to_file: dict[str, str] = {}
281
+
282
+ for fp in pack.files:
283
+ rel = fp.path.relative_to(pack.root).as_posix()
284
+ for d in fp.defs:
285
+ def_line_map[d.local_id] = (d.def_line, d.end_line)
286
+ def_to_canon[d.local_id] = d.id
287
+ def_to_file[d.local_id] = rel
288
+ for c in fp.classes:
289
+ class_to_file[c.id] = rel
290
+
291
+ if use_stubs:
292
+ for fp in pack.files:
293
+ by_qualname: dict[str, list[ClassRef]] = defaultdict(list)
294
+ try:
295
+ parsed_classes, _ = parse_symbols(
296
+ path=fp.path, root=pack.root, text=fp.stubbed_text
297
+ )
298
+ except SyntaxError:
299
+ parsed_classes = []
300
+ for c in parsed_classes:
301
+ by_qualname[c.qualname].append(c)
302
+ for c in sorted(fp.classes, key=lambda x: (x.class_line, x.qualname)):
303
+ matches = by_qualname.get(c.qualname)
304
+ if matches:
305
+ match = matches.pop(0)
306
+ class_line_map[c.id] = (match.class_line, match.end_line)
307
+ else:
308
+ class_line_map[c.id] = (c.class_line, c.end_line)
309
+ else:
310
+ for fp in pack.files:
311
+ for c in fp.classes:
312
+ class_line_map[c.id] = (c.class_line, c.end_line)
313
+
314
+ lines.append("## How to Use This Pack\n\n")
315
+ if use_stubs:
316
+ lines.append(
317
+ "This Markdown is a self-contained *context pack* for an LLM. It\n"
318
+ "contains the repository structure, a symbol index, full canonical\n"
319
+ "definitions, and compact file stubs. Use it like this:\n\n"
320
+ "**Suggested read order**\n"
321
+ "1. **Directory Tree**: get a mental map of the project.\n"
322
+ "2. **Symbol Index**: find the file / symbol you care about (with\n"
323
+ " jump links).\n"
324
+ "3. **Function Library**: read the full implementation of a\n"
325
+ " function by ID.\n"
326
+ "4. **Files**: read file-level context; function bodies may be\n"
327
+ " stubbed.\n\n"
328
+ "**Stubs and markers**\n"
329
+ "- In the **Files** section, function bodies may be replaced with\n"
330
+ " a compact placeholder line like `... # ↪ FUNC:XXXXXXXX`.\n"
331
+ "- The 8-hex value after `FUNC:` is the function's **local_id**\n"
332
+ " (unique per occurrence in the repo).\n\n"
333
+ "**IDs (important for dedupe)**\n"
334
+ "- `id` is the **canonical** ID for a function body (deduped when\n"
335
+ " configured).\n"
336
+ "- `local_id` is unique per definition occurrence. Multiple defs\n"
337
+ " can share the same `id` but must have different `local_id`.\n\n"
338
+ "**When proposing changes**\n"
339
+ "- Reference changes by **file path** plus **function ID** (and\n"
340
+ " local_id if shown).\n"
341
+ "- Prefer emitting a unified diff patch (`--- a/...` / `+++ b/...`).\n\n"
342
+ "**Line numbers**\n"
343
+ "- All `Lx-y` ranges refer to line numbers in this markdown file.\n"
344
+ "- File ranges/classes point into **Files**; function ranges point\n"
345
+ " into **Function Library**.\n\n"
346
+ )
347
+ else:
348
+ lines.append(
349
+ "This Markdown is a self-contained *context pack* for an LLM.\n\n"
350
+ "**Suggested read order**\n"
351
+ "1. **Directory Tree**\n"
352
+ "2. **Symbol Index** (jump to file contents)\n"
353
+ "3. **Files** (full contents)\n\n"
354
+ "**When proposing changes**\n"
355
+ "- Prefer unified diffs (`--- a/...` / `+++ b/...`).\n\n"
356
+ "**Line numbers**\n"
357
+ "- `Lx-y` ranges refer to line numbers in this markdown file (the\n"
358
+ " code blocks under **Files**).\n\n"
359
+ )
360
+
361
+ if include_manifest:
362
+ lines.append("## Manifest\n\n")
363
+ lines.append("```codecrate-manifest\n")
364
+ lines.append(
365
+ json.dumps(
366
+ to_manifest(pack, minimal=not use_stubs), indent=2, sort_keys=False
367
+ )
368
+ + "\n"
369
+ )
370
+ lines.append("```\n\n")
371
+
372
+ rel_paths = [f.path.relative_to(pack.root).as_posix() for f in pack.files]
373
+ lines.append("## Directory Tree\n\n")
374
+ lines.append("```text\n")
375
+ lines.append(_render_tree(rel_paths) + "\n")
376
+ lines.append("```\n\n")
377
+
378
+ lines.append("## Symbol Index\n\n")
379
+
380
+ for fp in sorted(pack.files, key=lambda x: x.path.as_posix()):
381
+ rel = fp.path.relative_to(pack.root).as_posix()
382
+ fa = _file_anchor(rel)
383
+ sa = _file_src_anchor(rel)
384
+ file_range = _range_token("FILE", rel)
385
+ # Always provide a jump target to the file contents.
386
+ lines.append(f"### `{rel}` {file_range} — [jump](#{sa})\n")
387
+ lines.append(f'<a id="{fa}"></a>\n')
388
+
389
+ for c in sorted(fp.classes, key=lambda x: (x.class_line, x.qualname)):
390
+ class_loc = _range_token("CLASS", c.id)
391
+ lines.append(f"- `class {c.qualname}` {class_loc}\n")
392
+
393
+ for d in sorted(fp.defs, key=lambda d: (d.def_line, d.qualname)):
394
+ loc = _range_token("DEF", d.local_id)
395
+ link = "\n"
396
+ if use_stubs:
397
+ anchor = _anchor_for(d.id, d.module, d.qualname)
398
+ link = f" — [jump](#{anchor})\n"
399
+ id_display = f"**{d.id}**"
400
+ if getattr(d, "local_id", d.id) != d.id:
401
+ id_display += f" (local **{d.local_id}**)"
402
+ lines.append(f"- `{d.qualname}` → {id_display} {loc}{link}")
403
+ else:
404
+ lines.append(f"- `{d.qualname}` → {loc}\n")
405
+ lines.append("\n")
406
+
407
+ if use_stubs:
408
+ lines.append("## Function Library\n\n")
409
+ for defn_id, code in canonical_sources.items():
410
+ lines.append(f'<a id="{_anchor_for(defn_id, "", "")}"></a>\n')
411
+ lines.append(f"### {defn_id}\n")
412
+ lines.append("```python\n")
413
+ lines.append(_ensure_nl(code))
414
+ lines.append("```\n\n")
415
+
416
+ lines.append("## Files\n\n")
417
+ for fp in pack.files:
418
+ rel = fp.path.relative_to(pack.root).as_posix()
419
+ fa = _file_anchor(rel)
420
+ sa = _file_src_anchor(rel)
421
+ file_range = _range_token("FILE", rel)
422
+ lines.append(f"### `{rel}` {file_range}\n")
423
+ lines.append(f'<a id="{sa}"></a>\n')
424
+ lines.append(f"[jump to index](#{fa})\n\n")
425
+
426
+ # Compact stubs are not line-count aligned, so render as a single block.
427
+
428
+ lines.append(f"```{_fence_lang_for(rel)}\n")
429
+ if use_stubs:
430
+ lines.append(_ensure_nl(fp.stubbed_text))
431
+ else:
432
+ lines.append(_ensure_nl(_read_full_text(fp)))
433
+ lines.append("```\n\n")
434
+ # Only emit the Symbols block when there are actually symbols.
435
+ if use_stubs and fp.defs:
436
+ lines.append("**Symbols**\n\n")
437
+ if fp.module:
438
+ lines.append(f"_Module_: `{fp.module}`\n\n")
439
+ for d in sorted(fp.defs, key=lambda x: (x.def_line, x.qualname)):
440
+ anchor = _anchor_for(d.id, d.module, d.qualname)
441
+ loc = _range_token("DEF", d.local_id)
442
+ link = f" — [jump](#{anchor})\n"
443
+ id_display = f"**{d.id}**"
444
+ if getattr(d, "local_id", d.id) != d.id:
445
+ id_display += f" (local **{d.local_id}**)"
446
+ lines.append(f"- `{d.qualname}` → {id_display} {loc}{link}")
447
+ lines.append("\n")
448
+ text = "".join(lines)
449
+ return _apply_context_line_numbers(
450
+ text,
451
+ def_line_map=def_line_map,
452
+ class_line_map=class_line_map,
453
+ def_to_canon=def_to_canon,
454
+ def_to_file=def_to_file,
455
+ class_to_file=class_to_file,
456
+ use_stubs=use_stubs,
457
+ )
codecrate/mdparse.py ADDED
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from dataclasses import dataclass
6
+
7
+ _CODE_FENCE_RE = re.compile(r"^```([a-zA-Z0-9_-]+)\s*$")
8
+ _FENCE_END_RE = re.compile(r"^```\s*$")
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class PackedMarkdown:
13
+ manifest: dict
14
+ canonical_sources: dict[str, str] # id -> python code
15
+ # NOTE: we don't strictly need stubbed files for unpack, but helpful for debugging
16
+ stubbed_files: dict[str, str] # rel path -> code
17
+
18
+
19
+ def _iter_fenced_blocks(lines: list[str]):
20
+ i = 0
21
+ while i < len(lines):
22
+ m = _CODE_FENCE_RE.match(lines[i])
23
+ if not m:
24
+ i += 1
25
+ continue
26
+ lang = m.group(1)
27
+ i += 1
28
+ buf = []
29
+ while i < len(lines) and not _FENCE_END_RE.match(lines[i]):
30
+ buf.append(lines[i])
31
+ i += 1
32
+ if i < len(lines) and _FENCE_END_RE.match(lines[i]):
33
+ i += 1
34
+ yield lang, "".join(buf)
35
+
36
+
37
+ def _section_bounds(title: str, text_lines: list[str]) -> tuple[int, int]:
38
+ start = None
39
+ for i, ln in enumerate(text_lines):
40
+ if ln.strip() == title:
41
+ start = i + 1
42
+ break
43
+ if start is None:
44
+ return (0, len(text_lines))
45
+ end = len(text_lines)
46
+ for j in range(start, len(text_lines)):
47
+ if text_lines[j].startswith("## ") and text_lines[j].strip() != title:
48
+ end = j
49
+ break
50
+ return (start, end)
51
+
52
+
53
+ def _parse_function_library(text_lines: list[str]) -> dict[str, str]:
54
+ canonical_sources: dict[str, str] = {}
55
+ fl_start, fl_end = _section_bounds("## Function Library", text_lines)
56
+ for idx in range(fl_start, fl_end):
57
+ line = text_lines[idx]
58
+ if not line.startswith("### "):
59
+ continue
60
+
61
+ # Support both header styles:
62
+ # - v4 current: "### <ID>"
63
+ # - older: "### <ID> — <extra metadata>"
64
+ title = line.replace("###", "", 1).strip()
65
+ maybe_id = title.split(" — ", 1)[0].strip()
66
+ if not maybe_id:
67
+ continue
68
+
69
+ j = idx + 1
70
+ while j < fl_end and text_lines[j].strip() != "```python":
71
+ j += 1
72
+ if j < fl_end and text_lines[j].strip() == "```python":
73
+ k = j + 1
74
+ buf: list[str] = []
75
+ while k < fl_end and text_lines[k].strip() != "```":
76
+ buf.append(text_lines[k])
77
+ k += 1
78
+ if buf:
79
+ canonical_sources[maybe_id] = "\n".join(buf).rstrip() + "\n"
80
+ return canonical_sources
81
+
82
+
83
+ def _parse_stubbed_files(text_lines: list[str]) -> dict[str, str]:
84
+ stubbed_files: dict[str, str] = {}
85
+ fs_start, fs_end = _section_bounds("## Files", text_lines)
86
+ i = fs_start
87
+ while i < fs_end:
88
+ line = text_lines[i]
89
+ if line.startswith("### `") and "`" in line:
90
+ start = line.find("`") + 1
91
+ end = line.find("`", start)
92
+ rel = line[start:end]
93
+ parts: list[str] = []
94
+ j = i + 1
95
+ while j < fs_end and not (
96
+ text_lines[j].startswith("### `") and "`" in text_lines[j]
97
+ ):
98
+ fence = text_lines[j].strip()
99
+ if fence.startswith("```") and fence != "```":
100
+ k = j + 1
101
+ buf: list[str] = []
102
+ while k < fs_end and text_lines[k].strip() != "```":
103
+ buf.append(text_lines[k])
104
+ k += 1
105
+ if buf:
106
+ chunk = "\n".join(buf)
107
+ if not chunk.endswith("\n"):
108
+ chunk += "\n"
109
+ parts.append(chunk)
110
+ j = k + 1
111
+ continue
112
+ j += 1
113
+ if parts:
114
+ stubbed_files[rel] = "".join(parts)
115
+ else:
116
+ stubbed_files[rel] = ""
117
+ i = j
118
+ continue
119
+ i += 1
120
+ return stubbed_files
121
+
122
+
123
+ def parse_packed_markdown(text: str) -> PackedMarkdown:
124
+ lines = text.splitlines(keepends=True)
125
+ manifest = None
126
+ for lang, body in _iter_fenced_blocks(lines):
127
+ if lang == "codecrate-manifest":
128
+ manifest = json.loads(body)
129
+ break
130
+ if manifest is None:
131
+ raise ValueError(
132
+ "No codecrate-manifest block found. This pack cannot be used for "
133
+ "unpack/patch/validate-pack; re-run `codecrate pack` with --manifest "
134
+ "(or omit --no-manifest)."
135
+ )
136
+
137
+ text_lines = text.splitlines()
138
+ canonical_sources = _parse_function_library(text_lines)
139
+ stubbed_files = _parse_stubbed_files(text_lines)
140
+
141
+ return PackedMarkdown(
142
+ manifest=manifest,
143
+ canonical_sources=canonical_sources,
144
+ stubbed_files=stubbed_files,
145
+ )
codecrate/model.py ADDED
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+
6
+
7
+ @dataclass(frozen=True)
8
+ class DefRef:
9
+ path: Path
10
+ module: str
11
+ qualname: str
12
+ id: str
13
+ local_id: str
14
+ kind: str
15
+ decorator_start: int
16
+ def_line: int
17
+ body_start: int
18
+ end_line: int
19
+ doc_start: int | None = None
20
+ doc_end: int | None = None
21
+ is_single_line: bool = False
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class ClassRef:
26
+ path: Path
27
+ module: str
28
+ qualname: str
29
+ id: str
30
+ decorator_start: int
31
+ class_line: int
32
+ end_line: int
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class FilePack:
37
+ path: Path
38
+ module: str
39
+ original_text: str
40
+ stubbed_text: str
41
+ line_count: int
42
+ classes: list[ClassRef]
43
+ defs: list[DefRef]
44
+
45
+
46
+ @dataclass(frozen=True)
47
+ class PackResult:
48
+ root: Path
49
+ files: list[FilePack]
50
+ classes: list[ClassRef]
51
+ defs: list[DefRef]