libcontext 0.7.0__tar.gz → 0.7.1__tar.gz
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-0.7.0 → libcontext-0.7.1}/PKG-INFO +1 -1
- {libcontext-0.7.0 → libcontext-0.7.1}/pyproject.toml +1 -1
- {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/_envsetup.py +27 -5
- {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/mcp_server.py +32 -3
- {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_envsetup.py +21 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_mcp_server.py +54 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/.github/workflows/ci.yml +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/.github/workflows/release.yml +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/.gitignore +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/CHANGELOG.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/CONTRIBUTING.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/DEPENDENCIES.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/LICENSE +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/README.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/SECURITY.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/docs/adr/001-progressive-disclosure-over-always-on-context.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/docs/adr/002-skill-plus-cli-as-primary-integration.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/docs/adr/004-ast-only-inspection.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/docs/adr/README.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/__init__.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/_security.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/cache.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/cli.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/collector.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/config.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/diff.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/exceptions.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/inspector.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/models.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/py.typed +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/renderer.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/tests/__init__.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_cache.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_cli.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_cli_mcp_parity.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_collector.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_config.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_diff.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_inspector.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_models.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_renderer.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_security.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: libcontext
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
4
4
|
Summary: Generate optimized LLM context from Python library APIs — CLI, skill, and MCP server
|
|
5
5
|
Project-URL: Homepage, https://github.com/Syclaw/libcontext
|
|
6
6
|
Project-URL: Repository, https://github.com/Syclaw/libcontext
|
|
@@ -262,22 +262,44 @@ def resolve_python_executable(python_arg: str) -> Path:
|
|
|
262
262
|
|
|
263
263
|
|
|
264
264
|
def get_target_sys_path(python_exe: Path) -> list[str]:
|
|
265
|
-
"""Query a Python interpreter for its ``sys.path
|
|
265
|
+
"""Query a Python interpreter for its non-stdlib ``sys.path`` entries.
|
|
266
266
|
|
|
267
267
|
Runs the interpreter in a subprocess with a short timeout to
|
|
268
|
-
extract
|
|
269
|
-
|
|
268
|
+
extract site-packages and user-added paths. Stdlib paths are
|
|
269
|
+
excluded to prevent cross-version import contamination when the
|
|
270
|
+
tool interpreter differs from the target (e.g. tool on 3.13,
|
|
271
|
+
target venv on 3.11).
|
|
270
272
|
|
|
271
273
|
Args:
|
|
272
274
|
python_exe: Absolute path to the target Python executable.
|
|
273
275
|
|
|
274
276
|
Returns:
|
|
275
|
-
List of path strings from the target interpreter
|
|
277
|
+
List of non-stdlib path strings from the target interpreter.
|
|
276
278
|
|
|
277
279
|
Raises:
|
|
278
280
|
EnvironmentSetupError: If the subprocess fails or times out.
|
|
279
281
|
"""
|
|
280
|
-
|
|
282
|
+
# Filter out stdlib paths from the target interpreter's sys.path.
|
|
283
|
+
# When the target runs a different Python version from the tool,
|
|
284
|
+
# injecting stdlib paths causes C-extension mismatches (e.g.
|
|
285
|
+
# Python 3.13 loading csv.py from 3.11 stdlib → _csv ImportError).
|
|
286
|
+
script = (
|
|
287
|
+
"import json, os, sys\n"
|
|
288
|
+
"base = os.path.realpath(sys.base_prefix)\n"
|
|
289
|
+
"prefix = os.path.realpath(sys.prefix)\n"
|
|
290
|
+
"in_venv = prefix != base\n"
|
|
291
|
+
"def _under(p, root):\n"
|
|
292
|
+
" return p == root or p.startswith(root + os.sep)\n"
|
|
293
|
+
"paths = [\n"
|
|
294
|
+
" p for p in sys.path\n"
|
|
295
|
+
" if p and not (\n"
|
|
296
|
+
" in_venv\n"
|
|
297
|
+
" and _under(os.path.realpath(p), base)\n"
|
|
298
|
+
" and not _under(os.path.realpath(p), prefix)\n"
|
|
299
|
+
" )\n"
|
|
300
|
+
"]\n"
|
|
301
|
+
"print(json.dumps(paths))\n"
|
|
302
|
+
)
|
|
281
303
|
try:
|
|
282
304
|
result = subprocess.run(
|
|
283
305
|
[str(python_exe), "-c", script],
|
|
@@ -32,7 +32,12 @@ from ._security import truncate_output
|
|
|
32
32
|
from .collector import collect_package
|
|
33
33
|
from .diff import diff_packages
|
|
34
34
|
from .exceptions import PackageNotFoundError
|
|
35
|
-
from .models import
|
|
35
|
+
from .models import (
|
|
36
|
+
ModuleInfo,
|
|
37
|
+
PackageInfo,
|
|
38
|
+
_deserialize_envelope,
|
|
39
|
+
_serialize_envelope,
|
|
40
|
+
)
|
|
36
41
|
from .renderer import (
|
|
37
42
|
render_diff,
|
|
38
43
|
render_module,
|
|
@@ -79,6 +84,30 @@ def _invalidate_cache() -> None:
|
|
|
79
84
|
_collect_cached.cache_clear()
|
|
80
85
|
|
|
81
86
|
|
|
87
|
+
def _is_module_snapshot(data: dict[str, object]) -> bool:
|
|
88
|
+
"""Return True if *data* looks like a ModuleInfo dict, not PackageInfo."""
|
|
89
|
+
return "modules" not in data and (
|
|
90
|
+
"functions" in data or "classes" in data or "variables" in data
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _coerce_to_package(data: dict[str, object]) -> PackageInfo:
|
|
95
|
+
"""Build a PackageInfo from either a package or module snapshot dict.
|
|
96
|
+
|
|
97
|
+
When ``get_api_json`` is called with a ``module_name``, the envelope
|
|
98
|
+
contains a single ModuleInfo dict. This helper wraps it in a
|
|
99
|
+
synthetic PackageInfo so that ``diff_packages`` can compare them.
|
|
100
|
+
"""
|
|
101
|
+
if _is_module_snapshot(data):
|
|
102
|
+
module_name: str = data.get("name", "unknown") # type: ignore[assignment]
|
|
103
|
+
pkg_name = module_name.split(".")[0] if "." in module_name else module_name
|
|
104
|
+
return PackageInfo(
|
|
105
|
+
name=pkg_name,
|
|
106
|
+
modules=[ModuleInfo.from_dict(data)], # type: ignore[arg-type]
|
|
107
|
+
)
|
|
108
|
+
return PackageInfo.from_dict(data)
|
|
109
|
+
|
|
110
|
+
|
|
82
111
|
# ---------------------------------------------------------------------------
|
|
83
112
|
# MCP Tools
|
|
84
113
|
# ---------------------------------------------------------------------------
|
|
@@ -237,8 +266,8 @@ def diff_api(old_json: str, new_json: str, output_format: str = "markdown") -> s
|
|
|
237
266
|
except ValueError as exc:
|
|
238
267
|
return f"Error: {exc}"
|
|
239
268
|
|
|
240
|
-
old_pkg =
|
|
241
|
-
new_pkg =
|
|
269
|
+
old_pkg = _coerce_to_package(old_data)
|
|
270
|
+
new_pkg = _coerce_to_package(new_data)
|
|
242
271
|
result = diff_packages(old_pkg, new_pkg)
|
|
243
272
|
|
|
244
273
|
if output_format == "json":
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
import sys
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
@@ -81,6 +82,26 @@ def test_get_target_sys_path_bad_executable(tmp_path):
|
|
|
81
82
|
get_target_sys_path(fake)
|
|
82
83
|
|
|
83
84
|
|
|
85
|
+
def test_get_target_sys_path_excludes_base_stdlib_in_venv():
|
|
86
|
+
"""Stdlib paths from the base interpreter are excluded when in a venv."""
|
|
87
|
+
paths = get_target_sys_path(Path(sys.executable))
|
|
88
|
+
|
|
89
|
+
base = os.path.realpath(sys.base_prefix)
|
|
90
|
+
prefix = os.path.realpath(sys.prefix)
|
|
91
|
+
if prefix == base:
|
|
92
|
+
pytest.skip("not running inside a venv")
|
|
93
|
+
|
|
94
|
+
# Paths under the base Python installation (stdlib, lib-dynload, etc.)
|
|
95
|
+
# must be excluded to prevent cross-version contamination.
|
|
96
|
+
# Paths under the venv prefix itself (e.g. site-packages) are kept.
|
|
97
|
+
for p in paths:
|
|
98
|
+
rp = os.path.realpath(p)
|
|
99
|
+
is_under_base = rp.startswith(base + os.sep) or rp == base
|
|
100
|
+
is_under_venv = rp.startswith(prefix + os.sep) or rp == prefix
|
|
101
|
+
if is_under_base and not is_under_venv:
|
|
102
|
+
pytest.fail(f"base-interpreter stdlib path leaked: {p}")
|
|
103
|
+
|
|
104
|
+
|
|
84
105
|
# ---------------------------------------------------------------------------
|
|
85
106
|
# inject_target_environment
|
|
86
107
|
# ---------------------------------------------------------------------------
|
|
@@ -362,6 +362,60 @@ class TestDiffApi:
|
|
|
362
362
|
result = mcp_server.diff_api("{}", "{}")
|
|
363
363
|
assert "Error:" in result
|
|
364
364
|
|
|
365
|
+
def test_module_level_snapshots(self):
|
|
366
|
+
"""diff_api accepts module-level JSON produced by get_api_json."""
|
|
367
|
+
import dataclasses
|
|
368
|
+
|
|
369
|
+
from libcontext.models import _serialize_envelope
|
|
370
|
+
|
|
371
|
+
old_mod = ModuleInfo(
|
|
372
|
+
name="fakepkg.utils",
|
|
373
|
+
functions=[
|
|
374
|
+
FunctionInfo(
|
|
375
|
+
name="parse",
|
|
376
|
+
parameters=[ParameterInfo(name="text", annotation="str")],
|
|
377
|
+
return_annotation="dict",
|
|
378
|
+
),
|
|
379
|
+
],
|
|
380
|
+
)
|
|
381
|
+
new_mod = ModuleInfo(
|
|
382
|
+
name="fakepkg.utils",
|
|
383
|
+
functions=[
|
|
384
|
+
FunctionInfo(
|
|
385
|
+
name="parse",
|
|
386
|
+
parameters=[
|
|
387
|
+
ParameterInfo(name="text", annotation="str"),
|
|
388
|
+
ParameterInfo(name="strict", annotation="bool"),
|
|
389
|
+
],
|
|
390
|
+
return_annotation="dict",
|
|
391
|
+
),
|
|
392
|
+
],
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
old_json = json.dumps(_serialize_envelope(dataclasses.asdict(old_mod)))
|
|
396
|
+
new_json = json.dumps(_serialize_envelope(dataclasses.asdict(new_mod)))
|
|
397
|
+
result = mcp_server.diff_api(old_json, new_json)
|
|
398
|
+
|
|
399
|
+
assert "strict" in result
|
|
400
|
+
assert "Breaking" in result
|
|
401
|
+
|
|
402
|
+
def test_module_level_no_changes(self):
|
|
403
|
+
"""Identical module snapshots produce 'No changes'."""
|
|
404
|
+
import dataclasses
|
|
405
|
+
|
|
406
|
+
from libcontext.models import _serialize_envelope
|
|
407
|
+
|
|
408
|
+
mod = ModuleInfo(
|
|
409
|
+
name="fakepkg.utils",
|
|
410
|
+
functions=[
|
|
411
|
+
FunctionInfo(name="parse", parameters=[], return_annotation="dict"),
|
|
412
|
+
],
|
|
413
|
+
)
|
|
414
|
+
mod_json = json.dumps(_serialize_envelope(dataclasses.asdict(mod)))
|
|
415
|
+
result = mcp_server.diff_api(mod_json, mod_json)
|
|
416
|
+
|
|
417
|
+
assert "No changes" in result
|
|
418
|
+
|
|
365
419
|
|
|
366
420
|
# ---------------------------------------------------------------------------
|
|
367
421
|
# refresh_cache
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{libcontext-0.7.0 → libcontext-0.7.1}/docs/adr/001-progressive-disclosure-over-always-on-context.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|