libcontext 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.
- libcontext/__init__.py +57 -0
- libcontext/cli.py +218 -0
- libcontext/collector.py +290 -0
- libcontext/config.py +151 -0
- libcontext/inspector.py +399 -0
- libcontext/models.py +92 -0
- libcontext/py.typed +0 -0
- libcontext/renderer.py +366 -0
- libcontext-0.1.0.dist-info/METADATA +282 -0
- libcontext-0.1.0.dist-info/RECORD +13 -0
- libcontext-0.1.0.dist-info/WHEEL +4 -0
- libcontext-0.1.0.dist-info/entry_points.txt +2 -0
- libcontext-0.1.0.dist-info/licenses/LICENSE +21 -0
libcontext/renderer.py
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"""Markdown renderer — converts PackageInfo into LLM-optimized context.
|
|
2
|
+
|
|
3
|
+
Produces a structured Markdown document designed to give GitHub Copilot
|
|
4
|
+
(or any LLM) the best possible understanding of a library's public API.
|
|
5
|
+
|
|
6
|
+
The output format prioritises:
|
|
7
|
+
- Complete function/method signatures with type hints
|
|
8
|
+
- Concise docstrings (first paragraph only by default)
|
|
9
|
+
- Clear module hierarchy for correct import generation
|
|
10
|
+
- Compact representation to maximise useful content within context limits
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from .inspector import is_public_member
|
|
16
|
+
from .models import (
|
|
17
|
+
ClassInfo,
|
|
18
|
+
FunctionInfo,
|
|
19
|
+
ModuleInfo,
|
|
20
|
+
PackageInfo,
|
|
21
|
+
ParameterInfo,
|
|
22
|
+
VariableInfo,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
# Parameter & signature formatting
|
|
27
|
+
# ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _format_param(param: ParameterInfo) -> str:
|
|
31
|
+
"""Format a single parameter for display in a signature."""
|
|
32
|
+
parts: list[str] = []
|
|
33
|
+
|
|
34
|
+
parts.append(param.name)
|
|
35
|
+
|
|
36
|
+
if param.annotation:
|
|
37
|
+
parts.append(f": {param.annotation}")
|
|
38
|
+
|
|
39
|
+
if param.default is not None:
|
|
40
|
+
parts.append(f" = {param.default}")
|
|
41
|
+
|
|
42
|
+
return "".join(parts)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _format_signature(func: FunctionInfo, *, compact: bool = False) -> str:
|
|
46
|
+
"""Build a human-readable function signature string.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
func: The function to format.
|
|
50
|
+
compact: If True, omit ``self``/``cls`` and decorators.
|
|
51
|
+
"""
|
|
52
|
+
# Filter implicit params
|
|
53
|
+
params = func.parameters
|
|
54
|
+
if compact:
|
|
55
|
+
params = [p for p in params if p.name not in ("self", "cls")]
|
|
56
|
+
|
|
57
|
+
# Detect positional-only / keyword-only boundaries
|
|
58
|
+
formatted_parts: list[str] = []
|
|
59
|
+
prev_kind: str | None = None
|
|
60
|
+
|
|
61
|
+
for param in params:
|
|
62
|
+
# Insert / separator after positional-only params
|
|
63
|
+
if prev_kind == "POSITIONAL_ONLY" and param.kind != "POSITIONAL_ONLY":
|
|
64
|
+
formatted_parts.append("/")
|
|
65
|
+
|
|
66
|
+
# Insert * separator before keyword-only (when no *args)
|
|
67
|
+
if param.kind == "KEYWORD_ONLY" and prev_kind not in (
|
|
68
|
+
"VAR_POSITIONAL",
|
|
69
|
+
"KEYWORD_ONLY",
|
|
70
|
+
):
|
|
71
|
+
formatted_parts.append("*")
|
|
72
|
+
|
|
73
|
+
formatted_parts.append(_format_param(param))
|
|
74
|
+
prev_kind = param.kind
|
|
75
|
+
|
|
76
|
+
# Trailing / if ALL params are positional-only
|
|
77
|
+
if params and all(p.kind == "POSITIONAL_ONLY" for p in params):
|
|
78
|
+
formatted_parts.append("/")
|
|
79
|
+
|
|
80
|
+
params_str = ", ".join(formatted_parts)
|
|
81
|
+
prefix = "async def" if func.is_async else "def"
|
|
82
|
+
ret = f" -> {func.return_annotation}" if func.return_annotation else ""
|
|
83
|
+
|
|
84
|
+
return f"{prefix} {func.name}({params_str}){ret}"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _first_paragraph(text: str | None) -> str | None:
|
|
88
|
+
"""Extract the first paragraph of a docstring."""
|
|
89
|
+
if not text:
|
|
90
|
+
return None
|
|
91
|
+
lines: list[str] = []
|
|
92
|
+
for line in text.strip().splitlines():
|
|
93
|
+
stripped = line.strip()
|
|
94
|
+
if not stripped and lines:
|
|
95
|
+
break
|
|
96
|
+
if stripped:
|
|
97
|
+
lines.append(stripped)
|
|
98
|
+
return " ".join(lines) if lines else None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
# Component renderers
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _render_variable(var: VariableInfo) -> str:
|
|
107
|
+
"""Render a variable/constant as a Markdown list item."""
|
|
108
|
+
parts = [f"`{var.name}"]
|
|
109
|
+
if var.annotation:
|
|
110
|
+
parts.append(f": {var.annotation}")
|
|
111
|
+
if var.value is not None:
|
|
112
|
+
# Truncate very long values
|
|
113
|
+
val = var.value if len(var.value) <= 80 else var.value[:77] + "..."
|
|
114
|
+
parts.append(f" = {val}")
|
|
115
|
+
parts.append("`")
|
|
116
|
+
return f"- {''.join(parts)}"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _render_function(func: FunctionInfo, *, heading: str = "-") -> str:
|
|
120
|
+
"""Render a function as a Markdown block."""
|
|
121
|
+
lines: list[str] = []
|
|
122
|
+
|
|
123
|
+
# Decorators (only non-trivial ones)
|
|
124
|
+
notable_decorators = [
|
|
125
|
+
d
|
|
126
|
+
for d in func.decorators
|
|
127
|
+
if d not in ("property", "classmethod", "staticmethod")
|
|
128
|
+
]
|
|
129
|
+
for dec in notable_decorators:
|
|
130
|
+
lines.append(f"{heading} `@{dec}`")
|
|
131
|
+
|
|
132
|
+
sig = _format_signature(func, compact=True)
|
|
133
|
+
lines.append(f"{heading} `{sig}`")
|
|
134
|
+
|
|
135
|
+
doc = _first_paragraph(func.docstring)
|
|
136
|
+
if doc:
|
|
137
|
+
indent = " " if heading == "-" else ""
|
|
138
|
+
lines.append(f"{indent}{doc}")
|
|
139
|
+
|
|
140
|
+
return "\n".join(lines)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _render_class(cls: ClassInfo) -> str:
|
|
144
|
+
"""Render a class as a Markdown section."""
|
|
145
|
+
lines: list[str] = []
|
|
146
|
+
|
|
147
|
+
# Class header
|
|
148
|
+
bases_str = f"({', '.join(cls.bases)})" if cls.bases else ""
|
|
149
|
+
dec_str = ""
|
|
150
|
+
for dec in cls.decorators:
|
|
151
|
+
dec_str += f"`@{dec}` "
|
|
152
|
+
lines.append(f"#### {dec_str}`class {cls.name}{bases_str}`")
|
|
153
|
+
|
|
154
|
+
# Docstring
|
|
155
|
+
doc = _first_paragraph(cls.docstring)
|
|
156
|
+
if doc:
|
|
157
|
+
lines.append("")
|
|
158
|
+
lines.append(doc)
|
|
159
|
+
|
|
160
|
+
# Class variables (public only)
|
|
161
|
+
public_vars = [v for v in cls.class_variables if is_public_member(v.name)]
|
|
162
|
+
if public_vars:
|
|
163
|
+
lines.append("")
|
|
164
|
+
lines.append("**Attributes:**")
|
|
165
|
+
for var in public_vars:
|
|
166
|
+
lines.append(_render_variable(var))
|
|
167
|
+
|
|
168
|
+
# Methods — include public + useful dunders
|
|
169
|
+
visible_methods = [
|
|
170
|
+
m for m in cls.methods if is_public_member(m.name, is_method=True)
|
|
171
|
+
]
|
|
172
|
+
if visible_methods:
|
|
173
|
+
lines.append("")
|
|
174
|
+
lines.append("**Methods:**")
|
|
175
|
+
for method in visible_methods:
|
|
176
|
+
lines.append(_render_function(method))
|
|
177
|
+
|
|
178
|
+
# Inner classes
|
|
179
|
+
public_inner = [c for c in cls.inner_classes if is_public_member(c.name)]
|
|
180
|
+
for inner in public_inner:
|
|
181
|
+
lines.append("")
|
|
182
|
+
lines.append(_render_class(inner))
|
|
183
|
+
|
|
184
|
+
return "\n".join(lines)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _render_module(module: ModuleInfo) -> str:
|
|
188
|
+
"""Render a module as a Markdown section."""
|
|
189
|
+
lines: list[str] = []
|
|
190
|
+
|
|
191
|
+
lines.append(f"### `{module.name}`")
|
|
192
|
+
|
|
193
|
+
doc = _first_paragraph(module.docstring)
|
|
194
|
+
if doc:
|
|
195
|
+
lines.append("")
|
|
196
|
+
lines.append(doc)
|
|
197
|
+
|
|
198
|
+
# Determine public API boundary
|
|
199
|
+
exports = set(module.all_exports) if module.all_exports is not None else None
|
|
200
|
+
|
|
201
|
+
def _is_public(name: str) -> bool:
|
|
202
|
+
if exports is not None:
|
|
203
|
+
return name in exports
|
|
204
|
+
return is_public_member(name)
|
|
205
|
+
|
|
206
|
+
# Classes
|
|
207
|
+
public_classes = [c for c in module.classes if _is_public(c.name)]
|
|
208
|
+
for cls in public_classes:
|
|
209
|
+
lines.append("")
|
|
210
|
+
lines.append(_render_class(cls))
|
|
211
|
+
|
|
212
|
+
# Functions
|
|
213
|
+
public_functions = [f for f in module.functions if _is_public(f.name)]
|
|
214
|
+
if public_functions:
|
|
215
|
+
lines.append("")
|
|
216
|
+
lines.append("**Functions:**")
|
|
217
|
+
for func in public_functions:
|
|
218
|
+
lines.append("")
|
|
219
|
+
lines.append(_render_function(func))
|
|
220
|
+
|
|
221
|
+
# Constants (UPPER_CASE variables)
|
|
222
|
+
public_constants = [
|
|
223
|
+
v for v in module.variables if _is_public(v.name) and v.name.isupper()
|
|
224
|
+
]
|
|
225
|
+
if public_constants:
|
|
226
|
+
lines.append("")
|
|
227
|
+
lines.append("**Constants:**")
|
|
228
|
+
for var in public_constants:
|
|
229
|
+
lines.append(_render_variable(var))
|
|
230
|
+
|
|
231
|
+
# Module-level variables (non-constant public variables)
|
|
232
|
+
public_vars = [
|
|
233
|
+
v for v in module.variables if _is_public(v.name) and not v.name.isupper()
|
|
234
|
+
]
|
|
235
|
+
if public_vars:
|
|
236
|
+
lines.append("")
|
|
237
|
+
lines.append("**Module Variables:**")
|
|
238
|
+
for var in public_vars:
|
|
239
|
+
lines.append(_render_variable(var))
|
|
240
|
+
|
|
241
|
+
return "\n".join(lines)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# Public API
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
# Markers used to delimit auto-generated sections in existing files
|
|
249
|
+
BEGIN_MARKER = "<!-- BEGIN LIBCONTEXT: {name} -->"
|
|
250
|
+
END_MARKER = "<!-- END LIBCONTEXT: {name} -->"
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def render_package(
|
|
254
|
+
package: PackageInfo,
|
|
255
|
+
*,
|
|
256
|
+
include_readme: bool = True,
|
|
257
|
+
max_readme_lines: int = 100,
|
|
258
|
+
extra_context: str | None = None,
|
|
259
|
+
) -> str:
|
|
260
|
+
"""Render a :class:`PackageInfo` as Markdown optimised for LLM context.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
package: The collected package information.
|
|
264
|
+
include_readme: Whether to include the README overview section.
|
|
265
|
+
max_readme_lines: Truncate the README after this many lines.
|
|
266
|
+
extra_context: Additional free-form context to append (e.g. from
|
|
267
|
+
``[tool.libcontext] extra_context``).
|
|
268
|
+
|
|
269
|
+
Returns:
|
|
270
|
+
A complete Markdown string ready to be written to a file or stdout.
|
|
271
|
+
"""
|
|
272
|
+
lines: list[str] = []
|
|
273
|
+
|
|
274
|
+
# --- Header --------------------------------------------------------
|
|
275
|
+
version = f" v{package.version}" if package.version else ""
|
|
276
|
+
lines.append(f"# {package.name}{version} — API Reference")
|
|
277
|
+
lines.append("")
|
|
278
|
+
|
|
279
|
+
if package.summary:
|
|
280
|
+
lines.append(f"> {package.summary}")
|
|
281
|
+
lines.append("")
|
|
282
|
+
|
|
283
|
+
# --- README overview -----------------------------------------------
|
|
284
|
+
if include_readme and package.readme:
|
|
285
|
+
lines.append("## Overview")
|
|
286
|
+
lines.append("")
|
|
287
|
+
readme_lines = package.readme.strip().splitlines()
|
|
288
|
+
if len(readme_lines) > max_readme_lines:
|
|
289
|
+
readme_lines = readme_lines[:max_readme_lines]
|
|
290
|
+
readme_lines.append("")
|
|
291
|
+
readme_lines.append("*(README truncated)*")
|
|
292
|
+
lines.extend(readme_lines)
|
|
293
|
+
lines.append("")
|
|
294
|
+
|
|
295
|
+
# --- Extra context -------------------------------------------------
|
|
296
|
+
if extra_context:
|
|
297
|
+
lines.append("## Notes")
|
|
298
|
+
lines.append("")
|
|
299
|
+
lines.append(extra_context.strip())
|
|
300
|
+
lines.append("")
|
|
301
|
+
|
|
302
|
+
# --- API Reference -------------------------------------------------
|
|
303
|
+
modules = package.non_empty_modules
|
|
304
|
+
if modules:
|
|
305
|
+
lines.append("## API Reference")
|
|
306
|
+
lines.append("")
|
|
307
|
+
|
|
308
|
+
for module in modules:
|
|
309
|
+
lines.append(_render_module(module))
|
|
310
|
+
lines.append("")
|
|
311
|
+
lines.append("---")
|
|
312
|
+
lines.append("")
|
|
313
|
+
|
|
314
|
+
return "\n".join(lines)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def inject_into_file(
|
|
318
|
+
content: str,
|
|
319
|
+
package_name: str,
|
|
320
|
+
existing: str | None = None,
|
|
321
|
+
) -> str:
|
|
322
|
+
"""Inject generated context into an existing file using markers.
|
|
323
|
+
|
|
324
|
+
If the file already contains a ``<!-- BEGIN LIBCONTEXT: {name} -->`` /
|
|
325
|
+
``<!-- END LIBCONTEXT: {name} -->`` block for this package, that block
|
|
326
|
+
is replaced. Otherwise, the content is appended at the end.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
content: The generated Markdown context.
|
|
330
|
+
package_name: Package name used in the markers.
|
|
331
|
+
existing: Current file contents (if any).
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
The updated file contents.
|
|
335
|
+
"""
|
|
336
|
+
begin = BEGIN_MARKER.format(name=package_name)
|
|
337
|
+
end = END_MARKER.format(name=package_name)
|
|
338
|
+
block = f"{begin}\n{content}\n{end}"
|
|
339
|
+
|
|
340
|
+
if existing is None:
|
|
341
|
+
return block
|
|
342
|
+
|
|
343
|
+
begin_idx = existing.find(begin)
|
|
344
|
+
end_idx = existing.find(end)
|
|
345
|
+
|
|
346
|
+
if begin_idx != -1 and end_idx != -1 and begin_idx < end_idx:
|
|
347
|
+
# Well-formed existing section — replace it
|
|
348
|
+
before = existing[:begin_idx]
|
|
349
|
+
after = existing[end_idx + len(end) :]
|
|
350
|
+
return f"{before}{block}{after}"
|
|
351
|
+
|
|
352
|
+
if begin_idx != -1 or end_idx != -1:
|
|
353
|
+
# Malformed markers (only one present, or reversed order).
|
|
354
|
+
# Remove any stale markers before appending the clean block.
|
|
355
|
+
cleaned = existing
|
|
356
|
+
if begin_idx != -1:
|
|
357
|
+
cleaned = cleaned[:begin_idx] + cleaned[begin_idx + len(begin) :]
|
|
358
|
+
# Recalculate end_idx after potential removal
|
|
359
|
+
end_idx_clean = cleaned.find(end)
|
|
360
|
+
if end_idx_clean != -1:
|
|
361
|
+
cleaned = cleaned[:end_idx_clean] + cleaned[end_idx_clean + len(end) :]
|
|
362
|
+
existing = cleaned
|
|
363
|
+
|
|
364
|
+
# Append
|
|
365
|
+
separator = "\n\n" if existing.strip() else ""
|
|
366
|
+
return f"{existing.rstrip()}{separator}{block}\n"
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: libcontext
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate optimized LLM context from Python library APIs for GitHub Copilot
|
|
5
|
+
Project-URL: Homepage, https://github.com/Syclaw/libcontext
|
|
6
|
+
Project-URL: Repository, https://github.com/Syclaw/libcontext
|
|
7
|
+
Project-URL: Issues, https://github.com/Syclaw/libcontext/issues
|
|
8
|
+
Author: Jonathan VARELA
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ast,context,copilot,documentation,introspection
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: click>=8.0
|
|
25
|
+
Requires-Dist: tomli>=1.0; python_version < '3.11'
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy>=1.10; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.4.0; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# libcontext
|
|
34
|
+
|
|
35
|
+
[](https://github.com/Syclaw/libcontext/actions/workflows/ci.yml)
|
|
36
|
+
[](https://pypi.org/project/libcontext/)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
[](https://pypi.org/project/libcontext/)
|
|
39
|
+
|
|
40
|
+
> Make your AI coding assistant aware of any Python library's API — especially the ones it doesn't already know.
|
|
41
|
+
|
|
42
|
+
**libcontext** inspects any installed Python package via static AST analysis (no code execution) and generates a compact Markdown API reference. Add it to your [`.github/copilot-instructions.md`](https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions) and GitHub Copilot will automatically include it as context in **Chat, Agent, and Code Review** interactions.
|
|
43
|
+
|
|
44
|
+
## Why This Exists
|
|
45
|
+
|
|
46
|
+
When you ask Copilot Chat how to use a library, or when Copilot Agent generates code that depends on one, the quality of the output depends entirely on what the model knows about that library's API.
|
|
47
|
+
|
|
48
|
+
For popular, well-established libraries, LLMs generally have good knowledge from training data. But for many real-world scenarios, the model is working blind:
|
|
49
|
+
|
|
50
|
+
- **Internal / private libraries** — Zero training data exists. The model has never seen the API.
|
|
51
|
+
- **Niche open-source packages** — Sparse or outdated training data leads to hallucinated methods and wrong signatures.
|
|
52
|
+
- **New versions of any library** — Training data has a cutoff. The model knows v2, you're using v3.
|
|
53
|
+
|
|
54
|
+
GitHub Copilot supports [repository custom instructions](https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions) — a `.github/copilot-instructions.md` file that is automatically included as context. According to GitHub's [support matrix](https://docs.github.com/en/copilot/reference/custom-instructions-support), this file is loaded by:
|
|
55
|
+
|
|
56
|
+
| Copilot feature | Uses custom instructions |
|
|
57
|
+
|---|---|
|
|
58
|
+
| **Copilot Chat** (VS Code, JetBrains, Visual Studio, Eclipse, Xcode, github.com) | ✅ Yes |
|
|
59
|
+
| **Copilot coding agent** (PR generation, agent mode) | ✅ Yes |
|
|
60
|
+
| **Copilot code review** | ✅ Yes |
|
|
61
|
+
| **Inline code completion** (autocomplete as you type) | ❌ Not currently |
|
|
62
|
+
|
|
63
|
+
libcontext bridges the knowledge gap by pre-generating a structured API reference from installed packages and placing it where Copilot can find it.
|
|
64
|
+
|
|
65
|
+
## When libcontext makes the biggest difference
|
|
66
|
+
|
|
67
|
+
| Scenario | Impact | Why |
|
|
68
|
+
|---|---|---|
|
|
69
|
+
| **Internal / private libraries** | 🔴 Critical | Zero training data exists for proprietary code |
|
|
70
|
+
| **Niche open-source packages** | 🟠 High | Sparse training data → hallucinated methods and wrong signatures |
|
|
71
|
+
| **New versions of any library** | 🟠 High | Training data has a cutoff — the LLM knows v2, you're using v3 |
|
|
72
|
+
| **Popular, stable libraries** | ⚪ Low | The LLM already has good knowledge from training data |
|
|
73
|
+
|
|
74
|
+
> **If Copilot has ever suggested a function that doesn't exist** in one of your dependencies, or got the parameters wrong — libcontext can help prevent that.
|
|
75
|
+
|
|
76
|
+
## Quick Start
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install libcontext
|
|
80
|
+
|
|
81
|
+
# Generate context for any installed package
|
|
82
|
+
libctx requests -o .github/copilot-instructions.md
|
|
83
|
+
|
|
84
|
+
# Done — Copilot Chat and Agent now know the complete requests API
|
|
85
|
+
# (15 modules, 44 classes, 70 functions → ~800 lines of compact reference)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## How It Works
|
|
89
|
+
|
|
90
|
+
1. **AST parsing** — Reads source files of installed packages using Python's `ast` module. No code is ever executed, making it safe for any package.
|
|
91
|
+
2. **Extraction** — Classes, functions, methods, parameters, type annotations, decorators, and docstrings are collected.
|
|
92
|
+
3. **Compact rendering** — Everything is rendered into structured Markdown optimised for LLM context windows (signatures and docstrings only, no implementation code).
|
|
93
|
+
4. **Marker injection** — Output is wrapped in `<!-- BEGIN/END LIBCONTEXT -->` markers, so re-running updates only its section without touching the rest of the file.
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
installed package libcontext .github/copilot-instructions.md
|
|
97
|
+
(source files) ──▶ (AST analysis) ──▶ (compact API reference)
|
|
98
|
+
│
|
|
99
|
+
▼
|
|
100
|
+
Copilot Chat, Agent &
|
|
101
|
+
Code Review now know
|
|
102
|
+
the full API
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Installation
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pip install libcontext
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Or with [uv](https://docs.astral.sh/uv/):
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
uv add libcontext
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
For development:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
git clone https://github.com/Syclaw/libcontext.git
|
|
121
|
+
cd libcontext
|
|
122
|
+
uv sync --all-extras # or: pip install -e ".[dev]"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Usage
|
|
126
|
+
|
|
127
|
+
### Command Line
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Generate context for an installed library (stdout)
|
|
131
|
+
libctx requests
|
|
132
|
+
|
|
133
|
+
# Write to the Copilot instructions file
|
|
134
|
+
libctx requests -o .github/copilot-instructions.md
|
|
135
|
+
|
|
136
|
+
# Multiple libraries at once
|
|
137
|
+
libctx requests httpx pydantic -o .github/copilot-instructions.md
|
|
138
|
+
|
|
139
|
+
# Include private members
|
|
140
|
+
libctx mypackage --include-private
|
|
141
|
+
|
|
142
|
+
# Without the README
|
|
143
|
+
libctx mypackage --no-readme
|
|
144
|
+
|
|
145
|
+
# With an explicit configuration file
|
|
146
|
+
libctx mypackage --config path/to/pyproject.toml
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Python API
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from libcontext import collect_package, render_package
|
|
153
|
+
|
|
154
|
+
# Collect the API of an installed package
|
|
155
|
+
pkg = collect_package("requests")
|
|
156
|
+
|
|
157
|
+
# Generate the Markdown
|
|
158
|
+
context = render_package(pkg)
|
|
159
|
+
print(context)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Injection into an Existing File
|
|
163
|
+
|
|
164
|
+
When using `-o`, libcontext injects content between markers:
|
|
165
|
+
|
|
166
|
+
```markdown
|
|
167
|
+
<!-- BEGIN LIBCONTEXT: requests -->
|
|
168
|
+
... generated content ...
|
|
169
|
+
<!-- END LIBCONTEXT: requests -->
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Subsequent runs update only that section, preserving the rest of the file.
|
|
173
|
+
|
|
174
|
+
## Configuration (Optional)
|
|
175
|
+
|
|
176
|
+
Library authors can customise what libcontext exposes from their package by adding a `[tool.libcontext]` section to their `pyproject.toml`. **The library does not need to depend on libcontext** — this is purely opt-in metadata that libcontext reads at generation time.
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
┌──────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
|
|
180
|
+
│ libcontext │ │ Library B │ │ Your project │
|
|
181
|
+
│ (CLI tool) │ │ (any Python pkg) │ │ (end user) │
|
|
182
|
+
│ │ │ │ │ │
|
|
183
|
+
│ Reads │ │ Can optionally add │ │ Runs: │
|
|
184
|
+
│ [tool.libcontext]│◀────│ [tool.libcontext] │ │ libctx lib_b │
|
|
185
|
+
│ from library B │ │ to pyproject.toml │ │ │
|
|
186
|
+
└──────────────────┘ └──────────────────────┘ └──────────────────────┘
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```toml
|
|
190
|
+
[tool.libcontext]
|
|
191
|
+
# Only include specific modules
|
|
192
|
+
include_modules = ["mylib.core", "mylib.models"]
|
|
193
|
+
|
|
194
|
+
# Exclude modules
|
|
195
|
+
exclude_modules = ["mylib._internal", "mylib.tests"]
|
|
196
|
+
|
|
197
|
+
# Include private members
|
|
198
|
+
include_private = false
|
|
199
|
+
|
|
200
|
+
# Free-form extra context
|
|
201
|
+
extra_context = """
|
|
202
|
+
This library uses the Repository pattern for data access.
|
|
203
|
+
All async operations use httpx internally.
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
# Maximum README lines
|
|
207
|
+
max_readme_lines = 150
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Output Example
|
|
211
|
+
|
|
212
|
+
```markdown
|
|
213
|
+
# requests v2.31.0 — API Reference
|
|
214
|
+
|
|
215
|
+
> Python HTTP for Humans.
|
|
216
|
+
|
|
217
|
+
## Overview
|
|
218
|
+
|
|
219
|
+
# Requests
|
|
220
|
+
Requests is a simple HTTP library for Python...
|
|
221
|
+
|
|
222
|
+
## API Reference
|
|
223
|
+
|
|
224
|
+
### `requests`
|
|
225
|
+
|
|
226
|
+
#### `class Session()`
|
|
227
|
+
A Requests session. Provides cookie persistence, connection-pooling, and configuration.
|
|
228
|
+
|
|
229
|
+
**Methods:**
|
|
230
|
+
- `def get(url: str, **kwargs) -> Response`
|
|
231
|
+
Sends a GET request.
|
|
232
|
+
- `def post(url: str, data: Any = None, json: Any = None, **kwargs) -> Response`
|
|
233
|
+
Sends a POST request.
|
|
234
|
+
|
|
235
|
+
**Functions:**
|
|
236
|
+
|
|
237
|
+
- `def get(url: str, params: dict | None = None, **kwargs) -> Response`
|
|
238
|
+
Sends a GET request.
|
|
239
|
+
- `def post(url: str, data: Any = None, **kwargs) -> Response`
|
|
240
|
+
Sends a POST request.
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Architecture
|
|
244
|
+
|
|
245
|
+
| Module | Description |
|
|
246
|
+
|---|---|
|
|
247
|
+
| `models.py` | Dataclasses to represent Python components |
|
|
248
|
+
| `inspector.py` | Static AST analysis (no code execution) |
|
|
249
|
+
| `collector.py` | Discovery and collection of all modules in a package |
|
|
250
|
+
| `config.py` | Reads `[tool.libcontext]` from pyproject.toml |
|
|
251
|
+
| `renderer.py` | LLM-optimised Markdown generation |
|
|
252
|
+
| `cli.py` | CLI entry point (`libctx`) |
|
|
253
|
+
|
|
254
|
+
## Development
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# Install in development mode
|
|
258
|
+
uv sync --all-extras
|
|
259
|
+
|
|
260
|
+
# Run tests
|
|
261
|
+
uv run pytest
|
|
262
|
+
|
|
263
|
+
# Run tests with coverage
|
|
264
|
+
uv run pytest --cov=libcontext
|
|
265
|
+
|
|
266
|
+
# Lint & format
|
|
267
|
+
uv run ruff check src/ tests/
|
|
268
|
+
uv run ruff format src/ tests/
|
|
269
|
+
|
|
270
|
+
# Type checking
|
|
271
|
+
uv run mypy src/libcontext
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed contribution guidelines.
|
|
275
|
+
|
|
276
|
+
## Dependencies
|
|
277
|
+
|
|
278
|
+
See [DEPENDENCIES.md](DEPENDENCIES.md) for the full list of dependencies and their licenses.
|
|
279
|
+
|
|
280
|
+
## License
|
|
281
|
+
|
|
282
|
+
MIT — see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
libcontext/__init__.py,sha256=3VbCO4BeksaABIhiMgjz3WQNgZ-tRtXjIdQ923HU6_A,1489
|
|
2
|
+
libcontext/cli.py,sha256=Xov4bd5ZiYlL3DnDD_h46Z7Nt3PGJnAcjJndtuodNkk,6676
|
|
3
|
+
libcontext/collector.py,sha256=sVgZav9lspoKUIXVTnatYiDzebBc3svo6bu__e8qv14,9961
|
|
4
|
+
libcontext/config.py,sha256=E6EzxigCBQfD8dzFmOK-JK9QxTryRiCfrRvR2reBebM,5363
|
|
5
|
+
libcontext/inspector.py,sha256=aIQ_fAx9B0RNZKQMhet96eYMqJgiCioDAXCCLiyScDs,12511
|
|
6
|
+
libcontext/models.py,sha256=XL9ohheI9GN12ui9AKknKYZKzICtOTdt0lnmZvOCEC0,2644
|
|
7
|
+
libcontext/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
libcontext/renderer.py,sha256=XWQfH101_nffunusOekIXI8n1BWp2OZzDqel0pxGIIw,12016
|
|
9
|
+
libcontext-0.1.0.dist-info/METADATA,sha256=EExmcaFEIEyhL7iL9itNOLs0CiIXqQTAWeh_VsZm2Kg,10678
|
|
10
|
+
libcontext-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
11
|
+
libcontext-0.1.0.dist-info/entry_points.txt,sha256=a5nWJ2NNcQKxys77Ov84qZjFj7fsmi7fVHuQL3ulcsw,47
|
|
12
|
+
libcontext-0.1.0.dist-info/licenses/LICENSE,sha256=xogpsEBXhxtnSGpSzzCsA9qGXEpoZACBfEZW4clFXNc,1093
|
|
13
|
+
libcontext-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jonathan VARELA
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|