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.
Files changed (45) hide show
  1. {libcontext-0.7.0 → libcontext-0.7.1}/PKG-INFO +1 -1
  2. {libcontext-0.7.0 → libcontext-0.7.1}/pyproject.toml +1 -1
  3. {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/_envsetup.py +27 -5
  4. {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/mcp_server.py +32 -3
  5. {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_envsetup.py +21 -0
  6. {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_mcp_server.py +54 -0
  7. {libcontext-0.7.0 → libcontext-0.7.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  8. {libcontext-0.7.0 → libcontext-0.7.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  9. {libcontext-0.7.0 → libcontext-0.7.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  10. {libcontext-0.7.0 → libcontext-0.7.1}/.github/workflows/ci.yml +0 -0
  11. {libcontext-0.7.0 → libcontext-0.7.1}/.github/workflows/release.yml +0 -0
  12. {libcontext-0.7.0 → libcontext-0.7.1}/.gitignore +0 -0
  13. {libcontext-0.7.0 → libcontext-0.7.1}/CHANGELOG.md +0 -0
  14. {libcontext-0.7.0 → libcontext-0.7.1}/CONTRIBUTING.md +0 -0
  15. {libcontext-0.7.0 → libcontext-0.7.1}/DEPENDENCIES.md +0 -0
  16. {libcontext-0.7.0 → libcontext-0.7.1}/LICENSE +0 -0
  17. {libcontext-0.7.0 → libcontext-0.7.1}/README.md +0 -0
  18. {libcontext-0.7.0 → libcontext-0.7.1}/SECURITY.md +0 -0
  19. {libcontext-0.7.0 → libcontext-0.7.1}/docs/adr/001-progressive-disclosure-over-always-on-context.md +0 -0
  20. {libcontext-0.7.0 → libcontext-0.7.1}/docs/adr/002-skill-plus-cli-as-primary-integration.md +0 -0
  21. {libcontext-0.7.0 → libcontext-0.7.1}/docs/adr/004-ast-only-inspection.md +0 -0
  22. {libcontext-0.7.0 → libcontext-0.7.1}/docs/adr/README.md +0 -0
  23. {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/__init__.py +0 -0
  24. {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/_security.py +0 -0
  25. {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/cache.py +0 -0
  26. {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/cli.py +0 -0
  27. {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/collector.py +0 -0
  28. {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/config.py +0 -0
  29. {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/diff.py +0 -0
  30. {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/exceptions.py +0 -0
  31. {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/inspector.py +0 -0
  32. {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/models.py +0 -0
  33. {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/py.typed +0 -0
  34. {libcontext-0.7.0 → libcontext-0.7.1}/src/libcontext/renderer.py +0 -0
  35. {libcontext-0.7.0 → libcontext-0.7.1}/tests/__init__.py +0 -0
  36. {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_cache.py +0 -0
  37. {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_cli.py +0 -0
  38. {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_cli_mcp_parity.py +0 -0
  39. {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_collector.py +0 -0
  40. {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_config.py +0 -0
  41. {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_diff.py +0 -0
  42. {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_inspector.py +0 -0
  43. {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_models.py +0 -0
  44. {libcontext-0.7.0 → libcontext-0.7.1}/tests/test_renderer.py +0 -0
  45. {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.0
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "libcontext"
7
- version = "0.7.0"
7
+ version = "0.7.1"
8
8
  description = "Generate optimized LLM context from Python library APIs — CLI, skill, and MCP server"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -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 the full search path, including ``.pth`` expansions and
269
- site-packages.
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's ``sys.path``.
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
- script = "import sys, json; print(json.dumps(sys.path))"
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 PackageInfo, _deserialize_envelope, _serialize_envelope
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 = PackageInfo.from_dict(old_data)
241
- new_pkg = PackageInfo.from_dict(new_data)
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