libcontext 0.7.0__tar.gz → 0.7.2__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.2}/PKG-INFO +1 -1
- {libcontext-0.7.0 → libcontext-0.7.2}/pyproject.toml +1 -1
- {libcontext-0.7.0 → libcontext-0.7.2}/src/libcontext/_envsetup.py +54 -6
- {libcontext-0.7.0 → libcontext-0.7.2}/src/libcontext/mcp_server.py +32 -3
- {libcontext-0.7.0 → libcontext-0.7.2}/tests/test_envsetup.py +27 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/tests/test_mcp_server.py +54 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/.github/workflows/ci.yml +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/.github/workflows/release.yml +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/.gitignore +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/CHANGELOG.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/CONTRIBUTING.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/DEPENDENCIES.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/LICENSE +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/README.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/SECURITY.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/docs/adr/001-progressive-disclosure-over-always-on-context.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/docs/adr/002-skill-plus-cli-as-primary-integration.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/docs/adr/004-ast-only-inspection.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/docs/adr/README.md +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/src/libcontext/__init__.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/src/libcontext/_security.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/src/libcontext/cache.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/src/libcontext/cli.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/src/libcontext/collector.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/src/libcontext/config.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/src/libcontext/diff.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/src/libcontext/exceptions.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/src/libcontext/inspector.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/src/libcontext/models.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/src/libcontext/py.typed +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/src/libcontext/renderer.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/tests/__init__.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/tests/test_cache.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/tests/test_cli.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/tests/test_cli_mcp_parity.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/tests/test_collector.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/tests/test_config.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/tests/test_diff.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/tests/test_inspector.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/tests/test_models.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/tests/test_renderer.py +0 -0
- {libcontext-0.7.0 → libcontext-0.7.2}/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.2
|
|
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
|
|
@@ -46,6 +46,50 @@ _INTERPRETER_CANDIDATES = (
|
|
|
46
46
|
Path("bin") / "python3", # Unix alternative
|
|
47
47
|
)
|
|
48
48
|
|
|
49
|
+
# Script executed in the *target* interpreter to collect site-packages
|
|
50
|
+
# directories. Kept as a module constant so it can be tested directly.
|
|
51
|
+
# Uses only stdlib modules guaranteed present in Python 3.9+.
|
|
52
|
+
_SITE_PACKAGES_SCRIPT = """\
|
|
53
|
+
import json, site, sys, os
|
|
54
|
+
|
|
55
|
+
paths = []
|
|
56
|
+
|
|
57
|
+
# site-packages directories (includes system site-packages when the
|
|
58
|
+
# venv was created with --system-site-packages)
|
|
59
|
+
try:
|
|
60
|
+
paths.extend(site.getsitepackages())
|
|
61
|
+
except AttributeError:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
# User site-packages (e.g. ~/.local/lib/python3.x/site-packages),
|
|
65
|
+
# honoured only when ENABLE_USER_SITE is not explicitly disabled.
|
|
66
|
+
if site.ENABLE_USER_SITE:
|
|
67
|
+
try:
|
|
68
|
+
user = site.getusersitepackages()
|
|
69
|
+
if isinstance(user, str):
|
|
70
|
+
paths.append(user)
|
|
71
|
+
except AttributeError:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
# .pth files in site-packages can inject arbitrary paths into sys.path
|
|
75
|
+
# (editable installs, namespace packages, etc.). Collect them by
|
|
76
|
+
# diffing sys.path against site-packages — any path that is not a
|
|
77
|
+
# site-packages dir and not under sys.base_prefix is .pth-injected.
|
|
78
|
+
site_set = set(os.path.realpath(p) for p in paths)
|
|
79
|
+
base = os.path.realpath(sys.base_prefix)
|
|
80
|
+
for p in sys.path:
|
|
81
|
+
if not p:
|
|
82
|
+
continue
|
|
83
|
+
rp = os.path.realpath(p)
|
|
84
|
+
if rp in site_set:
|
|
85
|
+
continue
|
|
86
|
+
if rp == base or rp.startswith(base + os.sep):
|
|
87
|
+
continue
|
|
88
|
+
paths.append(p)
|
|
89
|
+
|
|
90
|
+
print(json.dumps(paths))
|
|
91
|
+
"""
|
|
92
|
+
|
|
49
93
|
|
|
50
94
|
def _has_python_interpreter(venv_dir: Path) -> bool:
|
|
51
95
|
"""Check whether a directory contains a recognisable Python interpreter."""
|
|
@@ -262,22 +306,26 @@ def resolve_python_executable(python_arg: str) -> Path:
|
|
|
262
306
|
|
|
263
307
|
|
|
264
308
|
def get_target_sys_path(python_exe: Path) -> list[str]:
|
|
265
|
-
"""Query a
|
|
309
|
+
"""Query a target interpreter for its package-discovery paths.
|
|
266
310
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
311
|
+
Collects site-packages directories (including system site-packages
|
|
312
|
+
for ``--system-site-packages`` venvs), user site-packages, and
|
|
313
|
+
any paths injected by ``.pth`` files (editable installs, namespace
|
|
314
|
+
packages). Stdlib paths are **excluded** to prevent cross-version
|
|
315
|
+
import contamination when tool and target run different Python
|
|
316
|
+
versions.
|
|
270
317
|
|
|
271
318
|
Args:
|
|
272
319
|
python_exe: Absolute path to the target Python executable.
|
|
273
320
|
|
|
274
321
|
Returns:
|
|
275
|
-
List of path strings
|
|
322
|
+
List of path strings suitable for prepending to ``sys.path``
|
|
323
|
+
in the tool process.
|
|
276
324
|
|
|
277
325
|
Raises:
|
|
278
326
|
EnvironmentSetupError: If the subprocess fails or times out.
|
|
279
327
|
"""
|
|
280
|
-
script =
|
|
328
|
+
script = _SITE_PACKAGES_SCRIPT
|
|
281
329
|
try:
|
|
282
330
|
result = subprocess.run(
|
|
283
331
|
[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,32 @@ 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_returns_site_packages_not_stdlib():
|
|
86
|
+
"""Returns site-packages paths and excludes base-interpreter stdlib."""
|
|
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
|
+
# At least one site-packages directory must be present.
|
|
95
|
+
assert any("site-packages" in p for p in paths), (
|
|
96
|
+
f"no site-packages found in returned paths: {paths}"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# No path should resolve under the base Python installation
|
|
100
|
+
# (stdlib, lib-dynload, zip archives) unless it is also under
|
|
101
|
+
# the venv prefix.
|
|
102
|
+
def _under(child: str, root: str) -> bool:
|
|
103
|
+
return child == root or child.startswith(root + os.sep)
|
|
104
|
+
|
|
105
|
+
for p in paths:
|
|
106
|
+
rp = os.path.realpath(p)
|
|
107
|
+
if _under(rp, base) and not _under(rp, prefix):
|
|
108
|
+
pytest.fail(f"base-interpreter stdlib path leaked: {p}")
|
|
109
|
+
|
|
110
|
+
|
|
84
111
|
# ---------------------------------------------------------------------------
|
|
85
112
|
# inject_target_environment
|
|
86
113
|
# ---------------------------------------------------------------------------
|
|
@@ -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.2}/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
|