From 4b3815604c1afa7887191f89f4c6a70624637da8 Mon Sep 17 00:00:00 2001 From: RickZ <121943721+rickisba@users.noreply.github.com> Date: Wed, 29 Jul 2026 17:50:32 +0800 Subject: [PATCH 1/6] refactor: establish canonical cacheroute src namespace --- core/README.md | 2 +- core/runtime_compat.py | 2 +- kdn_server/domain/models.py | 2 +- pyproject.toml | 5 +++-- src/cacheroute/__init__.py | 3 +++ .../cacheroute/compat}/__init__.py | 0 .../cacheroute/compat}/runtime.py | 13 +++++++++++++ src/cacheroute/observability/__init__.py | 3 +++ src/cacheroute_compat/__init__.py | 8 ++++++++ src/cacheroute_compat/runtime.py | 4 ++++ test/kdn/README.md | 2 +- test/test_namespace_layout.py | 16 ++++++++++++++++ test/test_runtime_compat.py | 12 +++++++++++- test/test_wheel_install.py | 8 ++++---- 14 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 src/cacheroute/__init__.py rename {cacheroute_compat => src/cacheroute/compat}/__init__.py (100%) rename {cacheroute_compat => src/cacheroute/compat}/runtime.py (93%) create mode 100644 src/cacheroute/observability/__init__.py create mode 100644 src/cacheroute_compat/__init__.py create mode 100644 src/cacheroute_compat/runtime.py create mode 100644 test/test_namespace_layout.py diff --git a/core/README.md b/core/README.md index 9ea184d..bed40d2 100644 --- a/core/README.md +++ b/core/README.md @@ -28,7 +28,7 @@ Main responsibilities: ## Runtime compatibility boundary -`core/runtime_compat.py` is the compatibility-facing import path used by existing CacheRoute modules. The dependency-light implementation lives in `cacheroute_compat/runtime.py`, which keeps Runtime Profile normalization available without importing the full application graph. +`core/runtime_compat.py` is the compatibility-facing import path used by existing CacheRoute modules. The dependency-light implementation lives in `src/cacheroute/compat/runtime.py`, which keeps Runtime Profile normalization available without importing the full application graph. The deprecated `cacheroute_compat` import path is a forwarding shim and is intended for removal in a future release. Persisted and wire-level objects must use a resolved Runtime Profile (`legacy`, `v1`, or `test/mock`). The `auto` value is limited to startup resolution and must not become an operation-time fallback. The storage-neutral KDN representation of these profiles is documented in the [KDN domain vocabulary](../kdn_server/domain/README.md); serving-stack migration and startup details remain in [runtime compatibility documentation](../docs/runtime_compatibility_v1.md). diff --git a/core/runtime_compat.py b/core/runtime_compat.py index d9f9ef3..4be5a56 100644 --- a/core/runtime_compat.py +++ b/core/runtime_compat.py @@ -1,5 +1,5 @@ """Compatibility re-export for the dependency-light runtime profile helpers.""" -from cacheroute_compat.runtime import ( +from cacheroute.compat.runtime import ( RUNTIME_PROFILE_AUTO, RUNTIME_PROFILE_LEGACY, RUNTIME_PROFILE_TEST_MOCK, diff --git a/kdn_server/domain/models.py b/kdn_server/domain/models.py index b0a57fe..95bc135 100644 --- a/kdn_server/domain/models.py +++ b/kdn_server/domain/models.py @@ -9,7 +9,7 @@ from pydantic import AwareDatetime, BaseModel, ConfigDict, Field, field_validator, model_validator -from cacheroute_compat.runtime import normalize_runtime_profile +from cacheroute.compat.runtime import normalize_runtime_profile def utc_now() -> datetime: diff --git a/pyproject.toml b/pyproject.toml index 592276c..bebc6d9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "cacheroute" version = "0.1.9" description = "Compute-network-aware LLM scheduling for flexible KV cache reuse across vLLM and LMCache systems." authors = [ - { name = "xxx", email = "xxx" } + { name = "xxx" } ] readme = "README.md" requires-python = ">=3.10" @@ -37,7 +37,8 @@ dev = [ ] [tool.setuptools] +package-dir = {"" = "src"} [tool.setuptools.packages.find] -where = ["."] +where = ["src"] exclude = ["tests*", "docs*", "examples*"] diff --git a/src/cacheroute/__init__.py b/src/cacheroute/__init__.py new file mode 100644 index 0000000..744bc3e --- /dev/null +++ b/src/cacheroute/__init__.py @@ -0,0 +1,3 @@ +"""CacheRoute's dependency-light public Python namespace.""" + +__all__: list[str] = [] diff --git a/cacheroute_compat/__init__.py b/src/cacheroute/compat/__init__.py similarity index 100% rename from cacheroute_compat/__init__.py rename to src/cacheroute/compat/__init__.py diff --git a/cacheroute_compat/runtime.py b/src/cacheroute/compat/runtime.py similarity index 93% rename from cacheroute_compat/runtime.py rename to src/cacheroute/compat/runtime.py index 0b424b3..f80de14 100644 --- a/cacheroute_compat/runtime.py +++ b/src/cacheroute/compat/runtime.py @@ -123,3 +123,16 @@ def filter_supported_keys( key for key in key_set if classify_lmcache_redis_key(key) in allowed } + + +__all__ = [ + "RUNTIME_PROFILE_AUTO", + "RUNTIME_PROFILE_LEGACY", + "RUNTIME_PROFILE_TEST_MOCK", + "RUNTIME_PROFILE_V1", + "SUPPORTED_RUNTIME_PROFILES", + "classify_lmcache_redis_key", + "filter_supported_keys", + "normalize_runtime_profile", + "resolve_scan_match", +] diff --git a/src/cacheroute/observability/__init__.py b/src/cacheroute/observability/__init__.py new file mode 100644 index 0000000..ac8bf0a --- /dev/null +++ b/src/cacheroute/observability/__init__.py @@ -0,0 +1,3 @@ +"""Dependency-light namespace for CacheRoute observability helpers.""" + +__all__: list[str] = [] diff --git a/src/cacheroute_compat/__init__.py b/src/cacheroute_compat/__init__.py new file mode 100644 index 0000000..8c6ef7d --- /dev/null +++ b/src/cacheroute_compat/__init__.py @@ -0,0 +1,8 @@ +"""Deprecated forwarding package for :mod:`cacheroute.compat`. + +This import path is retained for Phase A compatibility and will be removed in +a future release. New code should import :mod:`cacheroute.compat`. +""" + +from cacheroute.compat import * # noqa: F401,F403 +from cacheroute.compat import __all__ diff --git a/src/cacheroute_compat/runtime.py b/src/cacheroute_compat/runtime.py new file mode 100644 index 0000000..f524551 --- /dev/null +++ b/src/cacheroute_compat/runtime.py @@ -0,0 +1,4 @@ +"""Deprecated forwarding module for :mod:`cacheroute.compat.runtime`.""" + +from cacheroute.compat.runtime import * # noqa: F401,F403 +from cacheroute.compat.runtime import __all__ diff --git a/test/kdn/README.md b/test/kdn/README.md index 71e95e6..1d770b3 100644 --- a/test/kdn/README.md +++ b/test/kdn/README.md @@ -86,7 +86,7 @@ python3 -m pytest -q \ test/test_instance_capability_registration.py python3 -m compileall -q \ - cacheroute_compat \ + src/cacheroute \ kdn_server/contracts \ kdn_server/domain \ kdn_server/gateway \ diff --git a/test/test_namespace_layout.py b/test/test_namespace_layout.py new file mode 100644 index 0000000..730cff7 --- /dev/null +++ b/test/test_namespace_layout.py @@ -0,0 +1,16 @@ +"""Tests for the dependency-light canonical package namespace.""" + +import subprocess +import sys + + +def test_canonical_namespace_import_is_dependency_light(): + script = """ +import sys +import cacheroute +import cacheroute.observability + +for name in ('fastapi', 'redis', 'numpy', 'torch', 'vllm', 'lmcache'): + assert name not in sys.modules, name +""" + subprocess.run([sys.executable, "-I", "-c", script], check=True) diff --git a/test/test_runtime_compat.py b/test/test_runtime_compat.py index 2d7779c..b0788c0 100644 --- a/test/test_runtime_compat.py +++ b/test/test_runtime_compat.py @@ -1,6 +1,16 @@ import pytest -from core.runtime_compat import filter_supported_keys, normalize_runtime_profile, resolve_scan_match +import cacheroute.compat.runtime as canonical_runtime +import cacheroute_compat.runtime as legacy_runtime + +filter_supported_keys = canonical_runtime.filter_supported_keys +normalize_runtime_profile = canonical_runtime.normalize_runtime_profile +resolve_scan_match = canonical_runtime.resolve_scan_match + + +def test_legacy_import_forwards_to_canonical_objects(): + assert legacy_runtime.normalize_runtime_profile is canonical_runtime.normalize_runtime_profile + assert legacy_runtime.filter_supported_keys is canonical_runtime.filter_supported_keys @pytest.mark.parametrize("value, expected", [ diff --git a/test/test_wheel_install.py b/test/test_wheel_install.py index 6e2eea7..21a41e8 100644 --- a/test/test_wheel_install.py +++ b/test/test_wheel_install.py @@ -25,10 +25,10 @@ def test_wheel_contains_dependency_light_compatibility_package(tmp_path): script = f""" import sys sys.path.insert(0, {str(installed)!r}) -from core.runtime_compat import normalize_runtime_profile -from kdn_server.domain import RuntimeProfile +from cacheroute.compat import normalize_runtime_profile +from cacheroute_compat.runtime import normalize_runtime_profile as legacy_normalize assert normalize_runtime_profile('modern') == 'v1' -assert RuntimeProfile.normalize('old') is RuntimeProfile.LEGACY -print('installed domain compatibility: passed') +assert legacy_normalize is normalize_runtime_profile +print('installed compatibility namespace: passed') """ subprocess.run([sys.executable, "-I", "-c", script], cwd=outside, check=True) From 8b4db721d21788383eb653ae15d4a258cd544554 Mon Sep 17 00:00:00 2001 From: RickZ <121943721+rickisba@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:10:32 +0800 Subject: [PATCH 2/6] docs: correct namespace inventory command --- AGENTS.md | 2 + core/README.md | 4 +- doc/package_migration_phase_a.md | 161 ++++++++++++++++++++++++++++ instance/TTFT_predictor/WORKFLOW.md | 6 +- pyproject.toml | 30 +++++- src/cacheroute_compat/__init__.py | 4 +- test/test_namespace_layout.py | 31 +++++- test/test_repository_governance.py | 82 ++++++++++++++ test/test_runtime_compat.py | 9 +- test/test_wheel_install.py | 69 +++++++++--- 10 files changed, 366 insertions(+), 32 deletions(-) create mode 100644 doc/package_migration_phase_a.md create mode 100644 test/test_repository_governance.py diff --git a/AGENTS.md b/AGENTS.md index 91a1d28..10dd1f9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -307,6 +307,8 @@ The user typically prefers **incremental, directly actionable code guidance**. ## 13. If You Add New Code +No new functional top-level directory may be added without a dedicated architecture Issue. New Python capabilities must first be placed under `src/cacheroute//` or an existing component hierarchy. Cross-component shared code must use a dependency-light, side-effect-free package under the `cacheroute` namespace. + When introducing a new helper, field, or branch: - keep naming consistent with existing repository terms, - add comments for non-obvious logic, diff --git a/core/README.md b/core/README.md index bed40d2..e0ae530 100644 --- a/core/README.md +++ b/core/README.md @@ -28,9 +28,9 @@ Main responsibilities: ## Runtime compatibility boundary -`core/runtime_compat.py` is the compatibility-facing import path used by existing CacheRoute modules. The dependency-light implementation lives in `src/cacheroute/compat/runtime.py`, which keeps Runtime Profile normalization available without importing the full application graph. The deprecated `cacheroute_compat` import path is a forwarding shim and is intended for removal in a future release. +`core/runtime_compat.py` is the compatibility-facing import path used by existing CacheRoute modules. The dependency-light implementation lives in `src/cacheroute/compat/runtime.py`, which keeps Runtime Profile normalization available without importing the full application graph. The deprecated `cacheroute_compat` import path is a forwarding shim scheduled for removal in CacheRoute 0.3.0. -Persisted and wire-level objects must use a resolved Runtime Profile (`legacy`, `v1`, or `test/mock`). The `auto` value is limited to startup resolution and must not become an operation-time fallback. The storage-neutral KDN representation of these profiles is documented in the [KDN domain vocabulary](../kdn_server/domain/README.md); serving-stack migration and startup details remain in [runtime compatibility documentation](../docs/runtime_compatibility_v1.md). +Persisted and wire-level objects must use a resolved Runtime Profile (`legacy`, `v1`, or `test/mock`). The `auto` value is limited to startup resolution and must not become an operation-time fallback. The storage-neutral KDN representation of these profiles is documented in the [KDN domain vocabulary](../kdn_server/domain/README.md); serving-stack migration and startup details remain in [runtime compatibility documentation](../doc/runtime_compatibility_v1.md). ## Configuration diff --git a/doc/package_migration_phase_a.md b/doc/package_migration_phase_a.md new file mode 100644 index 0000000..c9a6f66 --- /dev/null +++ b/doc/package_migration_phase_a.md @@ -0,0 +1,161 @@ +# Phase A package migration audit + +This audit records the repository state reviewed for Issue #157 Phase A and +separates that complete inventory from the two dependency-light packages moved +by PR #158. It is maintained alongside later namespace migration phases. + +## Scope and status + +PR #158 migrates only `cacheroute_compat` to `cacheroute.compat`, establishes +the lightweight `cacheroute` namespace, and reserves +`cacheroute.observability`. It does **not** migrate application packages or the +actual observability implementation being developed in PR #156. Consequently, +the observability acceptance criterion is not complete and neither #157 nor +#141 is closed by this phase. + +PR #156 must rebase onto PR #158 and move its `cacheroute_observability` +implementation, tests, demo, README, imports, and links into +`src/cacheroute/observability`. A permanent root `cacheroute_observability` +package must not be retained. + +## Complete top-level directory inventory + +Classification reflects directories tracked at the Phase A baseline. Generated +`.git`, `.pytest_cache`, and `__pycache__` directories are not architecture. + +| Directory | Classification | Packaged before Phase A? | Phase A action | +|---|---|---:|---| +| `.assets` | Documentation assets | No | Unchanged | +| `.github` | Repository/CI metadata | No | Unchanged | +| `UI` | Runtime UI package | Yes | Preserved at root | +| `cacheroute_compat` | Dependency-light compatibility implementation | Yes | Moved to `src/cacheroute/compat` | +| `client` | Runtime client package | Yes | Preserved at root | +| `core` | Shared application runtime package | Yes | Preserved at root | +| `data` | Packaged data helper package | Yes | Preserved at root | +| `doc` | Documentation | No | This audit added here | +| `env` | Environment, Docker, and deployment tooling | No | Unchanged | +| `instance` | Runtime Instance package | Yes | Preserved at root | +| `kdn_server` | Runtime KDN package | Yes | Preserved at root | +| `log` | Experiment log artifacts | No | Unchanged | +| `model` | Runtime model package | Yes | Preserved at root | +| `proxy` | Runtime Proxy package | Yes | Preserved at root | +| `scheduler` | Runtime Scheduler package | Yes | Preserved at root | +| `scripts` | Repository scripts | No | Unchanged | +| `src` | Canonical Python source root | No (new) | Hosts migrated packages | +| `store` | Runtime storage package | Yes | Preserved at root | +| `test` | Tests and demo launchers | Excluded | Updated validation only | +| `util` | Runtime utility package | Yes | Preserved at root | + +There are no baseline top-level `component` or `storage` Python directories; +the corresponding current concepts live in component packages and `store`. + +## Complete packaged runtime inventory + +The pre-PR namespace-aware setuptools discovery (`where = ["."]`) discovered the following +packages. The transitional explicit package list preserves every entry and adds +the four entries marked **new**: + +```text +UI, UI.client_ui, UI.client_ui.static, UI.client_ui.templates, UI.kdn_ui, +UI.proxy_ui, UI.proxy_ui.static +client, client.taskset +core +data, data.CacheRoute_dataset, data.CacheRoute_dataset.knowledge_document +doc, doc.blog, doc.integrations +env, env.config, env.docker, env.docker.cu130, env.docker.cu130.scripts +instance, instance.TPOT_predictor, instance.TTFT_predictor, +instance.TTFT_predictor.data, instance.pclient, instance.resource_agent, +instance.resource_agent.src, instance.resource_dashboard, +instance.resource_dashboard.static +kdn_server, kdn_server.KV_database, kdn_server.contracts, kdn_server.domain, +kdn_server.gateway, kdn_server.sclient, kdn_server.text_database, +kdn_server.text_database.blocks, kdn_server.util +log, log.scheduler +model +proxy, proxy.metrics, proxy.metrics.data, proxy.queue, proxy.resource, +proxy.sclient, proxy.strategy +scheduler, scheduler.knowledge, scheduler.resource, scheduler.strategy +scripts +store +test, test.kdn +util +cacheroute (new) +cacheroute.compat (new canonical implementation) +cacheroute.observability (new namespace only) +cacheroute_compat (moved deprecated shim) +``` + +Archive inspection (ignoring the `.dist-info` directory) produced these exact +top-level Python package directories: + +| Wheel | Complete top-level package list | +|---|---| +| Before PR #158 | `UI`, `cacheroute_compat`, `client`, `core`, `data`, `env`, `instance`, `kdn_server`, `model`, `proxy`, `scheduler`, `scripts`, `store`, `test`, `util` | +| After corrected PR #158 | `UI`, `cacheroute`, `cacheroute_compat`, `client`, `core`, `data`, `env`, `instance`, `kdn_server`, `model`, `proxy`, `scheduler`, `scripts`, `store`, `test`, `util` | + +The baseline wheel required the unrelated invalid placeholder author email to +be removed in its temporary archived copy before current setuptools could build +it; no package-discovery setting was changed for that inspection. + +## Reference audit + +The inventory searched all tracked repository files, including Python, TOML, +YAML, JSON, Dockerfiles, shell files, CI configuration, and Markdown. The +following categories are repository-wide; only the compatibility references +listed under “changed mapping” were modified. + +| Reference class | Inventory result | +|---|---| +| Normal compatibility imports | `core/runtime_compat.py`, `kdn_server/domain/models.py`, `test/test_runtime_compat.py`, and `test/test_wheel_install.py` | +| Dynamic/importlib references | `instance/capability_builder.py` uses distribution metadata; no dynamic string names target either migrated package | +| `python -m` commands | Operational/docs commands occur in `README.md`, `doc/`, `env/`, component READMEs, and `test/README.md`; none target the migrated package | +| Uvicorn/FastAPI module strings | `instance/TTFT_predictor/prefill_prediction_server.py`, its README/workflow, and `UI/proxy_ui/README.md`; none target the migrated package | +| Subprocess module invocations | `client/perf_client.py`, Instance dashboards, test demo launchers, resource-monitor tests, and wheel tests; only the wheel test exercises the migrated imports | +| Monkeypatch/mock target strings | Tests under `test/` and `test/kdn/`; none target the migrated implementation path | +| Docker/Compose/shell/CI | `Dockerfile`, `env/docker/cu130/`, `env/README.md`, `scripts/`, and `.github/`; no Compose file or migrated-package reference was found | +| Markdown references | `core/README.md` and `test/kdn/README.md` referenced the old implementation path and were updated; compatibility history remains explicitly marked deprecated | + +The reproducible inventory commands are: + +```bash +find . -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort +python3 -c "from setuptools import find_namespace_packages; print(*find_namespace_packages('.', exclude=['tests*','docs*','examples*']), sep='\n')" +rg -n --hidden -g '!.git/**' 'cacheroute_compat|cacheroute_observability' +rg -n --hidden -g '!.git/**' 'importlib|python(3)?[[:space:]]+-m|uvicorn|subprocess|monkeypatch|mock[.]patch' +``` + +## Changed path and entry-point mapping + +| Before | After | Policy | +|---|---|---| +| `cacheroute_compat` | `cacheroute.compat` | Canonical implementation moved | +| `cacheroute_compat.runtime` | `cacheroute.compat.runtime` | Canonical import for new code | +| None | `cacheroute` | Lightweight canonical namespace | +| None | `cacheroute.observability` | Reserved namespace, implementation pending PR #156 | + +No CLI, Uvicorn, FastAPI, dynamic-import, or `python -m` entry point changed. +The `cacheroute_compat` forwarding shim is scheduled for removal in CacheRoute +0.3.0. It contains imports only and is narrowly covered by compatibility tests. + +## Transitional packaging design + +Setuptools cannot safely discover unrelated packages from two source roots with +one declarative `find` rule. Phase A therefore uses an explicit, reviewable +package list plus targeted `package-dir` mappings for `cacheroute` and +`cacheroute_compat`. This prevents the src-layout migration from silently +dropping root packages. The final package-migration phase removes the explicit +transition and enables src-only discovery after every listed root runtime +package is migrated or intentionally deprecated through review. + +## Later phases and risks + +1. PR #156 performs the observability implementation migration described above. +2. Later #157 phases move each root runtime package without changing its public + import path, then replace the transitional explicit package list. +3. CacheRoute 0.3.0 removes `cacheroute_compat` after downstream imports migrate. +4. Root runtime package initializers currently import optional application + dependencies. Wheel smoke tests therefore install declared dependencies; + namespace isolation tests separately prove canonical imports remain light. +5. Packaging data files used by runtime modules requires review during each + component migration; Phase A deliberately preserves the prior package list + and runtime behavior rather than broadening that scope. diff --git a/instance/TTFT_predictor/WORKFLOW.md b/instance/TTFT_predictor/WORKFLOW.md index 6193410..c426495 100644 --- a/instance/TTFT_predictor/WORKFLOW.md +++ b/instance/TTFT_predictor/WORKFLOW.md @@ -15,7 +15,7 @@ python prefill_prediction_server.py ``` -入口位于 `[prefill_prediction_server.py](/d:/研/代码/PD分离推理调度代码/BurstGPT/example/scheduler/TTFT_predictor/prefill_prediction_server.py)`。 +入口位于 [`prefill_prediction_server.py`](prefill_prediction_server.py)。 启动后主流程如下: @@ -31,7 +31,7 @@ await predict_ttft(batch_size=1, prompt_length=1) ## 2. 首次初始化会触发哪些函数 -`predict_ttft(...)` 定义在 `[prefill_predictor.py](/d:/研/代码/PD分离推理调度代码/BurstGPT/example/scheduler/TTFT_predictor/prefill_predictor.py)`。 +`predict_ttft(...)` 定义在 [`prefill_predictor.py`](prefill_predictor.py)。 调用链如下: @@ -80,7 +80,7 @@ asyncio.create_task(run_warmup_in_background()) ## 4. 真实 warmup 会触发哪些函数 -`perform_detailed_warmup(...)` 位于 `[prefill_predictor.py](/d:/研/代码/PD分离推理调度代码/BurstGPT/example/scheduler/TTFT_predictor/prefill_predictor.py)`。 +`perform_detailed_warmup(...)` 位于 [`prefill_predictor.py`](prefill_predictor.py)。 它的主要流程如下: diff --git a/pyproject.toml b/pyproject.toml index bebc6d9..41827cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,8 +37,28 @@ dev = [ ] [tool.setuptools] -package-dir = {"" = "src"} - -[tool.setuptools.packages.find] -where = ["src"] -exclude = ["tests*", "docs*", "examples*"] +# Transitional Phase A package map. Root packages remain installed until their +# later reviewed migrations; the final migration phase replaces this explicit +# list with src-only discovery. +package-dir = {cacheroute = "src/cacheroute", cacheroute_compat = "src/cacheroute_compat"} +packages = [ + "cacheroute", "cacheroute.compat", "cacheroute.observability", + "cacheroute_compat", + "UI", "UI.client_ui", "UI.client_ui.static", "UI.client_ui.templates", + "UI.kdn_ui", "UI.proxy_ui", "UI.proxy_ui.static", + "client", "client.taskset", "core", "data", "data.CacheRoute_dataset", + "data.CacheRoute_dataset.knowledge_document", "doc", "doc.blog", + "doc.integrations", "env", "env.config", "env.docker", "env.docker.cu130", + "env.docker.cu130.scripts", "instance", "instance.TPOT_predictor", + "instance.TTFT_predictor", "instance.TTFT_predictor.data", "instance.pclient", + "instance.resource_agent", "instance.resource_agent.src", + "instance.resource_dashboard", "instance.resource_dashboard.static", + "kdn_server", "kdn_server.KV_database", "kdn_server.contracts", + "kdn_server.domain", "kdn_server.gateway", "kdn_server.sclient", + "kdn_server.text_database", "kdn_server.text_database.blocks", + "kdn_server.util", "log", "log.scheduler", "model", "proxy", + "proxy.metrics", "proxy.metrics.data", "proxy.queue", "proxy.resource", + "proxy.sclient", "proxy.strategy", "scheduler", "scheduler.knowledge", + "scheduler.resource", "scheduler.strategy", "scripts", "store", "test", + "test.kdn", "util", +] diff --git a/src/cacheroute_compat/__init__.py b/src/cacheroute_compat/__init__.py index 8c6ef7d..0d5e0b6 100644 --- a/src/cacheroute_compat/__init__.py +++ b/src/cacheroute_compat/__init__.py @@ -1,7 +1,7 @@ """Deprecated forwarding package for :mod:`cacheroute.compat`. -This import path is retained for Phase A compatibility and will be removed in -a future release. New code should import :mod:`cacheroute.compat`. +This import path is retained for Phase A compatibility and is scheduled for +removal in CacheRoute 0.3.0. New code should import :mod:`cacheroute.compat`. """ from cacheroute.compat import * # noqa: F401,F403 diff --git a/test/test_namespace_layout.py b/test/test_namespace_layout.py index 730cff7..6bda981 100644 --- a/test/test_namespace_layout.py +++ b/test/test_namespace_layout.py @@ -4,13 +4,34 @@ import sys -def test_canonical_namespace_import_is_dependency_light(): - script = """ +PROHIBITED_MODULES = ( + "core", "proxy", "instance", "scheduler", "kdn_server", + "fastapi", "redis", "numpy", "torch", "vllm", "lmcache", +) + + +def _run_isolated(import_statement, exercise=""): + script = f""" import sys -import cacheroute -import cacheroute.observability +{import_statement} +{exercise} -for name in ('fastapi', 'redis', 'numpy', 'torch', 'vllm', 'lmcache'): +for name in {PROHIBITED_MODULES!r}: assert name not in sys.modules, name """ subprocess.run([sys.executable, "-I", "-c", script], check=True) + + +def test_cacheroute_import_is_dependency_light(): + _run_isolated("import cacheroute") + + +def test_compat_import_is_dependency_light_and_functional(): + _run_isolated( + "import cacheroute.compat as compatibility", + "assert compatibility.normalize_runtime_profile('modern') == 'v1'", + ) + + +def test_observability_import_is_dependency_light(): + _run_isolated("import cacheroute.observability") diff --git a/test/test_repository_governance.py b/test/test_repository_governance.py new file mode 100644 index 0000000..1416260 --- /dev/null +++ b/test/test_repository_governance.py @@ -0,0 +1,82 @@ +"""Repository checks that keep the Phase A migration reviewable.""" + +import ast +from pathlib import Path +import re + + +ROOT = Path(__file__).resolve().parents[1] +ALLOWED_TOP_LEVEL_DIRECTORIES = { + ".assets", ".git", ".github", ".pytest_cache", "UI", "__pycache__", + "client", "core", "data", "doc", "env", "instance", "kdn_server", + "log", "model", "proxy", "scheduler", "scripts", "src", "store", + "test", "util", +} +LEGACY_REFERENCE_ALLOWLIST = { + Path("core/README.md"), + Path("doc/package_migration_phase_a.md"), + Path("pyproject.toml"), + Path("src/cacheroute_compat/__init__.py"), + Path("src/cacheroute_compat/runtime.py"), + Path("test/test_repository_governance.py"), + Path("test/test_runtime_compat.py"), + Path("test/test_wheel_install.py"), +} + + +def test_no_unreviewed_functional_root_directories(): + current = { + path.name for path in ROOT.iterdir() + if path.is_dir() and path.name != "build" and not path.name.endswith(".egg-info") + } + assert current <= ALLOWED_TOP_LEVEL_DIRECTORIES + + +def test_legacy_compatibility_references_are_narrowly_allowlisted(): + references = set() + for path in ROOT.rglob("*"): + if ( + not path.is_file() + or ".git" in path.parts + or "build" in path.parts + or "__pycache__" in path.parts + or any(part.endswith(".egg-info") for part in path.parts) + ): + continue + try: + if "cacheroute_compat" in path.read_text(encoding="utf-8"): + references.add(path.relative_to(ROOT)) + except UnicodeDecodeError: + continue + assert references <= LEGACY_REFERENCE_ALLOWLIST + + +def test_legacy_shim_contains_imports_only(): + for relative in ("src/cacheroute_compat/__init__.py", "src/cacheroute_compat/runtime.py"): + module = ast.parse((ROOT / relative).read_text(encoding="utf-8")) + functional = [ + node for node in module.body + if not isinstance(node, ast.ImportFrom) + and not ( + isinstance(node, ast.Expr) + and isinstance(node.value, ast.Constant) + and isinstance(node.value.value, str) + ) + ] + assert functional == [] + + +def test_local_markdown_links_resolve(): + missing = [] + link_pattern = re.compile(r"(?") + if not target or target.startswith(("#", "http://", "https://", "mailto:")): + continue + local = target.split("#", 1)[0] + if local and not (markdown.parent / local).resolve().exists(): + missing.append(f"{markdown.relative_to(ROOT)} -> {target}") + assert missing == [] diff --git a/test/test_runtime_compat.py b/test/test_runtime_compat.py index b0788c0..a2470ca 100644 --- a/test/test_runtime_compat.py +++ b/test/test_runtime_compat.py @@ -2,6 +2,7 @@ import cacheroute.compat.runtime as canonical_runtime import cacheroute_compat.runtime as legacy_runtime +import core.runtime_compat as core_runtime filter_supported_keys = canonical_runtime.filter_supported_keys normalize_runtime_profile = canonical_runtime.normalize_runtime_profile @@ -9,8 +10,12 @@ def test_legacy_import_forwards_to_canonical_objects(): - assert legacy_runtime.normalize_runtime_profile is canonical_runtime.normalize_runtime_profile - assert legacy_runtime.filter_supported_keys is canonical_runtime.filter_supported_keys + assert legacy_runtime.__all__ == canonical_runtime.__all__ + assert core_runtime.__all__ == canonical_runtime.__all__ + for name in canonical_runtime.__all__: + canonical = getattr(canonical_runtime, name) + assert getattr(legacy_runtime, name) is canonical + assert getattr(core_runtime, name) is canonical @pytest.mark.parametrize("value, expected", [ diff --git a/test/test_wheel_install.py b/test/test_wheel_install.py index 21a41e8..70c0e68 100644 --- a/test/test_wheel_install.py +++ b/test/test_wheel_install.py @@ -3,12 +3,35 @@ from pathlib import Path import subprocess import sys +import venv +import zipfile + + +EXPECTED_TOP_LEVEL_PACKAGES = { + "UI", "cacheroute", "cacheroute_compat", "client", "core", "data", + "env", "instance", "kdn_server", "model", "proxy", + "scheduler", "scripts", "store", "test", "util", +} +EXPECTED_PACKAGE_INITIALIZERS = { + "UI/__init__.py", "client/__init__.py", "core/__init__.py", "data/__init__.py", + "instance/__init__.py", "instance/TPOT_predictor/__init__.py", + "instance/TTFT_predictor/__init__.py", "instance/pclient/__init__.py", + "kdn_server/__init__.py", "kdn_server/contracts/__init__.py", + "kdn_server/domain/__init__.py", "kdn_server/gateway/__init__.py", + "model/__init__.py", "proxy/__init__.py", "proxy/metrics/__init__.py", + "proxy/queue/__init__.py", "proxy/sclient/__init__.py", + "proxy/strategy/__init__.py", "scheduler/__init__.py", + "scheduler/knowledge/__init__.py", "scheduler/resource/__init__.py", + "scheduler/strategy/__init__.py", "store/__init__.py", "util/__init__.py", + "cacheroute/__init__.py", "cacheroute/compat/__init__.py", + "cacheroute/observability/__init__.py", "cacheroute_compat/__init__.py", +} def test_wheel_contains_dependency_light_compatibility_package(tmp_path): repo = Path(__file__).resolve().parents[1] wheelhouse = tmp_path / "wheelhouse" - installed = tmp_path / "installed" + environment = tmp_path / "venv" outside = tmp_path / "outside" wheelhouse.mkdir() outside.mkdir() @@ -18,17 +41,37 @@ def test_wheel_contains_dependency_light_compatibility_package(tmp_path): check=True, ) wheel = next(wheelhouse.glob("cacheroute-*.whl")) - subprocess.run( - [sys.executable, "-m", "pip", "install", "--no-deps", "--target", str(installed), str(wheel)], - check=True, - ) - script = f""" -import sys -sys.path.insert(0, {str(installed)!r}) -from cacheroute.compat import normalize_runtime_profile -from cacheroute_compat.runtime import normalize_runtime_profile as legacy_normalize + with zipfile.ZipFile(wheel) as archive: + members = set(archive.namelist()) + packaged = { + name.partition("/")[0] + for name in archive.namelist() + if "/" in name and not name.partition("/")[0].endswith(".dist-info") + } + assert packaged == EXPECTED_TOP_LEVEL_PACKAGES + assert EXPECTED_PACKAGE_INITIALIZERS <= members + venv.EnvBuilder(with_pip=True, system_site_packages=True).create(environment) + python = environment / "bin" / "python" + subprocess.run([python, "-m", "pip", "install", "--no-deps", str(wheel)], check=True) + script = """ +import core +import proxy +import instance +import cacheroute +import cacheroute.compat +import cacheroute.compat.runtime as canonical_runtime +import cacheroute_compat.runtime as legacy_runtime +import core.runtime_compat as core_runtime +from core.runtime_compat import normalize_runtime_profile +from kdn_server.domain import RuntimeProfile + assert normalize_runtime_profile('modern') == 'v1' -assert legacy_normalize is normalize_runtime_profile -print('installed compatibility namespace: passed') +assert RuntimeProfile.normalize('old') is RuntimeProfile.LEGACY +assert canonical_runtime.__all__ == legacy_runtime.__all__ == core_runtime.__all__ +for name in canonical_runtime.__all__: + canonical = getattr(canonical_runtime, name) + assert getattr(legacy_runtime, name) is canonical + assert getattr(core_runtime, name) is canonical +print('installed public import surface: passed') """ - subprocess.run([sys.executable, "-I", "-c", script], cwd=outside, check=True) + subprocess.run([python, "-I", "-c", script], cwd=outside, check=True) From 833e6f51648510e3e7f27bc9c4c10669a42c2fc0 Mon Sep 17 00:00:00 2001 From: RickZ <121943721+rickisba@users.noreply.github.com> Date: Wed, 29 Jul 2026 19:27:59 +0800 Subject: [PATCH 3/6] test: isolate wheel and packaging validation --- doc/package_migration_phase_a.md | 62 ++++++++++++- pyproject.toml | 8 ++ test/test_repository_governance.py | 63 +++++++++++-- test/test_wheel_install.py | 139 +++++++++++++++++++++-------- 4 files changed, 223 insertions(+), 49 deletions(-) diff --git a/doc/package_migration_phase_a.md b/doc/package_migration_phase_a.md index c9a6f66..449b3b6 100644 --- a/doc/package_migration_phase_a.md +++ b/doc/package_migration_phase_a.md @@ -97,6 +97,63 @@ The baseline wheel required the unrelated invalid placeholder author email to be removed in its temporary archived copy before current setuptools could build it; no package-discovery setting was changed for that inspection. +### Normalized wheel member and package-data audit + +After excluding `.dist-info` metadata and normalizing the intentional +`cacheroute_compat/runtime.py` relocation to `cacheroute/compat/runtime.py`, the +baseline has 155 members and the corrected Phase A wheel has 197. No normalized +member was removed. The 42 added members are: + +```text +UI/client_ui/static/app.js +UI/client_ui/static/style.css +UI/client_ui/templates/index.html +UI/proxy_ui/static/app.js +UI/proxy_ui/static/index.html +UI/proxy_ui/static/style.css +cacheroute/__init__.py +cacheroute/observability/__init__.py +cacheroute_compat/__init__.py +cacheroute_compat/runtime.py +instance/TTFT_predictor/data/README.md +instance/TTFT_predictor/data/log-bs1-rtx5090-8-llama3-70b.txt +instance/TTFT_predictor/data/log-bs2-rtx5090-8-llama3-70b.txt +instance/TTFT_predictor/data/log-bs3-rtx5090-8-llama3-70b.txt +instance/TTFT_predictor/data/log-bs4-rtx5090-8-llama3-70b.txt +instance/TTFT_predictor/data/log-bs5-rtx5090-8-llama3-70b.txt +instance/TTFT_predictor/data/log-bs6-rtx5090-8-llama3-70b.txt +instance/TTFT_predictor/data/log-bs7-rtx5090-8-llama3-70b.txt +instance/TTFT_predictor/data/log-bs8-rtx5090-8-llama3-70b.txt +instance/TTFT_predictor/data/补录数据.txt +instance/TTFT_predictor/data/补录数据(小长度).txt +instance/TTFT_predictor/prompt_length_validation.log +instance/resource_dashboard/static/app.js +instance/resource_dashboard/static/index.html +instance/resource_dashboard/static/style.css +model/model_configs.yaml +proxy/metrics/data/log-bs1-rtx5090-8-llama3-70b.txt +proxy/metrics/data/log-bs2-rtx5090-8-llama3-70b.txt +proxy/metrics/data/log-bs3-rtx5090-8-llama3-70b.txt +proxy/metrics/data/log-bs4-rtx5090-8-llama3-70b.txt +proxy/metrics/data/log-bs5-rtx5090-8-llama3-70b.txt +proxy/metrics/data/log-bs6-rtx5090-8-llama3-70b.txt +proxy/metrics/data/log-bs7-rtx5090-8-llama3-70b.txt +proxy/metrics/data/log-bs8-rtx5090-8-llama3-70b.txt +proxy/metrics/data/redis_pull_table_from_image.json +proxy/metrics/redis_pull_coefficients.json +proxy/metrics/tpot_benchmark_table.json +proxy/metrics/tpot_coefficients.json +proxy/metrics/ttft_benchmark_table.json +proxy/metrics/ttft_coefficients.json +test/test_namespace_layout.py +test/test_repository_governance.py +``` + +The baseline wheel contained zero non-Python, non-metadata members. The +corrected wheel contains 36: the UI/dashboard assets, predictor data, model +configuration, and metric tables listed above. These additions make required +runtime package data explicit; they do not replace or remove baseline data. + ## Reference audit The inventory searched all tracked repository files, including Python, TOML, @@ -154,8 +211,9 @@ package is migrated or intentionally deprecated through review. import path, then replace the transitional explicit package list. 3. CacheRoute 0.3.0 removes `cacheroute_compat` after downstream imports migrate. 4. Root runtime package initializers currently import optional application - dependencies. Wheel smoke tests therefore install declared dependencies; - namespace isolation tests separately prove canonical imports remain light. + dependencies. The wheel tests use separate clean virtual environments: a + no-dependency environment for lightweight namespaces and a second environment + that installs declared dependencies before exercising the full public surface. 5. Packaging data files used by runtime modules requires review during each component migration; Phase A deliberately preserves the prior package list and runtime behavior rather than broadening that scope. diff --git a/pyproject.toml b/pyproject.toml index 41827cd..35f244b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,3 +62,11 @@ packages = [ "scheduler.resource", "scheduler.strategy", "scripts", "store", "test", "test.kdn", "util", ] + +[tool.setuptools.package-data] +"UI.client_ui" = ["static/*", "templates/*"] +"UI.proxy_ui" = ["static/*"] +"instance.resource_dashboard" = ["static/*"] +"instance.TTFT_predictor" = ["data/*.md", "data/*.txt", "*.log"] +"proxy.metrics" = ["*.json", "data/*.json", "data/*.txt"] +model = ["*.yaml"] diff --git a/test/test_repository_governance.py b/test/test_repository_governance.py index 1416260..06c4648 100644 --- a/test/test_repository_governance.py +++ b/test/test_repository_governance.py @@ -3,6 +3,10 @@ import ast from pathlib import Path import re +import subprocess +import tomllib + +from setuptools import find_namespace_packages ROOT = Path(__file__).resolve().parents[1] @@ -22,17 +26,56 @@ Path("test/test_runtime_compat.py"), Path("test/test_wheel_install.py"), } +OBSERVABILITY_REFERENCE_ALLOWLIST = { + Path("doc/package_migration_phase_a.md"), + Path("test/test_repository_governance.py"), +} +CANONICAL_PACKAGES = { + "cacheroute", "cacheroute.compat", "cacheroute.observability", + "cacheroute_compat", +} +GENERATED_PACKAGE_EXCLUDES = [ + "src", "src.*", "build", "build.*", "dist", "dist.*", "wheelhouse", + "wheelhouse.*", ".venv", ".venv.*", "*.egg-info", "*.egg-info.*", + ".pytest_cache", ".pytest_cache.*", "__pycache__", "__pycache__.*", + ".mypy_cache", ".mypy_cache.*", ".ruff_cache", ".ruff_cache.*", + "tests", "tests.*", "docs", "docs.*", +] def test_no_unreviewed_functional_root_directories(): - current = { - path.name for path in ROOT.iterdir() - if path.is_dir() and path.name != "build" and not path.name.endswith(".egg-info") + result = subprocess.run( + ["git", "ls-files", "-z"], cwd=ROOT, check=True, capture_output=True, + ) + tracked = { + Path(raw.decode()).parts[0] + for raw in result.stdout.split(b"\0") if raw and len(Path(raw.decode()).parts) > 1 } - assert current <= ALLOWED_TOP_LEVEL_DIRECTORIES + assert tracked <= ALLOWED_TOP_LEVEL_DIRECTORIES + + +def test_transitional_explicit_packages_match_root_discovery(): + configuration = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) + configured = set(configuration["tool"]["setuptools"]["packages"]) + discovered_root = set(find_namespace_packages( + where=str(ROOT), exclude=GENERATED_PACKAGE_EXCLUDES, + )) + assert CANONICAL_PACKAGES <= configured + assert configured - CANONICAL_PACKAGES == discovered_root def test_legacy_compatibility_references_are_narrowly_allowlisted(): + references = _files_containing("cacheroute_compat") + assert references <= LEGACY_REFERENCE_ALLOWLIST + + +def test_observability_legacy_references_are_narrowly_allowlisted(): + assert not (ROOT / "cacheroute_observability").exists() + references = _files_containing("cacheroute_observability") + assert references <= OBSERVABILITY_REFERENCE_ALLOWLIST + + +def _files_containing(needle): references = set() for path in ROOT.rglob("*"): if ( @@ -44,11 +87,11 @@ def test_legacy_compatibility_references_are_narrowly_allowlisted(): ): continue try: - if "cacheroute_compat" in path.read_text(encoding="utf-8"): + if needle in path.read_text(encoding="utf-8"): references.add(path.relative_to(ROOT)) except UnicodeDecodeError: continue - assert references <= LEGACY_REFERENCE_ALLOWLIST + return references def test_legacy_shim_contains_imports_only(): @@ -68,11 +111,15 @@ def test_legacy_shim_contains_imports_only(): def test_local_markdown_links_resolve(): missing = [] - link_pattern = re.compile(r"(?") if not target or target.startswith(("#", "http://", "https://", "mailto:")): continue diff --git a/test/test_wheel_install.py b/test/test_wheel_install.py index 70c0e68..687c7ca 100644 --- a/test/test_wheel_install.py +++ b/test/test_wheel_install.py @@ -1,4 +1,4 @@ -"""Verify compatibility imports from an installed wheel, not the source tree.""" +"""Verify wheel contents and imports without using the source checkout.""" from pathlib import Path import subprocess @@ -6,72 +6,133 @@ import venv import zipfile +import pytest + EXPECTED_TOP_LEVEL_PACKAGES = { "UI", "cacheroute", "cacheroute_compat", "client", "core", "data", - "env", "instance", "kdn_server", "model", "proxy", - "scheduler", "scripts", "store", "test", "util", + "env", "instance", "kdn_server", "model", "proxy", "scheduler", + "scripts", "store", "test", "util", } -EXPECTED_PACKAGE_INITIALIZERS = { - "UI/__init__.py", "client/__init__.py", "core/__init__.py", "data/__init__.py", - "instance/__init__.py", "instance/TPOT_predictor/__init__.py", - "instance/TTFT_predictor/__init__.py", "instance/pclient/__init__.py", - "kdn_server/__init__.py", "kdn_server/contracts/__init__.py", - "kdn_server/domain/__init__.py", "kdn_server/gateway/__init__.py", - "model/__init__.py", "proxy/__init__.py", "proxy/metrics/__init__.py", - "proxy/queue/__init__.py", "proxy/sclient/__init__.py", - "proxy/strategy/__init__.py", "scheduler/__init__.py", - "scheduler/knowledge/__init__.py", "scheduler/resource/__init__.py", - "scheduler/strategy/__init__.py", "store/__init__.py", "util/__init__.py", - "cacheroute/__init__.py", "cacheroute/compat/__init__.py", - "cacheroute/observability/__init__.py", "cacheroute_compat/__init__.py", +REQUIRED_PACKAGE_DATA = { + "UI/client_ui/static/app.js", + "UI/client_ui/static/style.css", + "UI/client_ui/templates/index.html", + "UI/proxy_ui/static/app.js", + "UI/proxy_ui/static/index.html", + "UI/proxy_ui/static/style.css", + "instance/resource_dashboard/static/app.js", + "instance/resource_dashboard/static/index.html", + "instance/resource_dashboard/static/style.css", + "instance/TTFT_predictor/data/README.md", + "instance/TTFT_predictor/data/log-bs1-rtx5090-8-llama3-70b.txt", + "instance/TTFT_predictor/data/补录数据.txt", + "model/model_configs.yaml", + "proxy/metrics/ttft_benchmark_table.json", + "proxy/metrics/data/redis_pull_table_from_image.json", } -def test_wheel_contains_dependency_light_compatibility_package(tmp_path): +@pytest.fixture(scope="module") +def built_wheel(tmp_path_factory): repo = Path(__file__).resolve().parents[1] - wheelhouse = tmp_path / "wheelhouse" - environment = tmp_path / "venv" - outside = tmp_path / "outside" - wheelhouse.mkdir() - outside.mkdir() - + wheelhouse = tmp_path_factory.mktemp("wheelhouse") subprocess.run( [sys.executable, "-m", "pip", "wheel", str(repo), "--no-deps", "--no-build-isolation", "-w", str(wheelhouse)], check=True, ) - wheel = next(wheelhouse.glob("cacheroute-*.whl")) - with zipfile.ZipFile(wheel) as archive: + return next(wheelhouse.glob("cacheroute-*.whl")) + + +def _create_isolated_environment(path: Path) -> Path: + venv.EnvBuilder(with_pip=True, system_site_packages=False).create(path) + return path / "bin" / "python" + + +def _run_outside_repo(python: Path, outside: Path, script: str) -> subprocess.CompletedProcess[str]: + outside.mkdir() + return subprocess.run( + [python, "-I", "-c", script], + cwd=outside, + check=True, + text=True, + capture_output=True, + ) + + +def test_wheel_preserves_packages_and_runtime_data(built_wheel): + with zipfile.ZipFile(built_wheel) as archive: members = set(archive.namelist()) packaged = { name.partition("/")[0] - for name in archive.namelist() + for name in members if "/" in name and not name.partition("/")[0].endswith(".dist-info") } assert packaged == EXPECTED_TOP_LEVEL_PACKAGES - assert EXPECTED_PACKAGE_INITIALIZERS <= members - venv.EnvBuilder(with_pip=True, system_site_packages=True).create(environment) - python = environment / "bin" / "python" - subprocess.run([python, "-m", "pip", "install", "--no-deps", str(wheel)], check=True) - script = """ -import core -import proxy -import instance + assert REQUIRED_PACKAGE_DATA <= members + + +def test_dependency_light_clean_wheel_imports(built_wheel, tmp_path): + python = _create_isolated_environment(tmp_path / "light-venv") + subprocess.run([python, "-m", "pip", "install", "--no-deps", str(built_wheel)], check=True) + result = _run_outside_repo(python, tmp_path / "outside-light", """ +from pathlib import Path +import sys import cacheroute import cacheroute.compat import cacheroute.compat.runtime as canonical_runtime +import cacheroute.observability import cacheroute_compat.runtime as legacy_runtime + +modules = (cacheroute, cacheroute.compat, canonical_runtime, + cacheroute.observability, legacy_runtime) +for module in modules: + path = Path(module.__file__).resolve() + print(f"{module.__name__}={path}") + assert path.is_relative_to(Path(sys.prefix).resolve()) +assert canonical_runtime.normalize_runtime_profile("modern") == "v1" +assert canonical_runtime.__all__ == legacy_runtime.__all__ +for name in canonical_runtime.__all__: + assert getattr(legacy_runtime, name) is getattr(canonical_runtime, name) +print("dependency-light clean-wheel imports: passed") +""") + assert "dependency-light clean-wheel imports: passed" in result.stdout + print(result.stdout, end="") + + +def test_full_public_imports_from_clean_wheel(built_wheel, tmp_path): + python = _create_isolated_environment(tmp_path / "full-venv") + # This deliberately installs declared dependencies. An unavailable package + # index must fail the test rather than leaking host packages into the venv. + subprocess.run([python, "-m", "pip", "install", str(built_wheel)], check=True) + result = _run_outside_repo(python, tmp_path / "outside-full", """ +from pathlib import Path +import sys +import core import core.runtime_compat as core_runtime +import proxy +import instance +import kdn_server +import kdn_server.domain +import cacheroute.compat.runtime as canonical_runtime +import cacheroute_compat.runtime as legacy_runtime from core.runtime_compat import normalize_runtime_profile from kdn_server.domain import RuntimeProfile -assert normalize_runtime_profile('modern') == 'v1' -assert RuntimeProfile.normalize('old') is RuntimeProfile.LEGACY +modules = (core, core_runtime, proxy, instance, kdn_server, kdn_server.domain, + canonical_runtime, legacy_runtime) +for module in modules: + path = Path(module.__file__).resolve() + print(f"{module.__name__}={path}") + assert path.is_relative_to(Path(sys.prefix).resolve()) +assert normalize_runtime_profile("modern") == "v1" +assert RuntimeProfile.normalize("old") is RuntimeProfile.LEGACY assert canonical_runtime.__all__ == legacy_runtime.__all__ == core_runtime.__all__ for name in canonical_runtime.__all__: canonical = getattr(canonical_runtime, name) assert getattr(legacy_runtime, name) is canonical assert getattr(core_runtime, name) is canonical -print('installed public import surface: passed') -""" - subprocess.run([python, "-I", "-c", script], cwd=outside, check=True) +print("full clean-wheel public imports: passed") +""") + assert "full clean-wheel public imports: passed" in result.stdout + print(result.stdout, end="") From bbcb8d338a373091d864511a38da930518cf0d05 Mon Sep 17 00:00:00 2001 From: RickZ <121943721+rickisba@users.noreply.github.com> Date: Wed, 29 Jul 2026 19:48:41 +0800 Subject: [PATCH 4/6] fix: support source checkout namespace imports --- conftest.py | 5 ++- core/config.py | 5 ++- doc/package_migration_phase_a.md | 34 +++++++++++++++-- pyproject.toml | 7 +++- scripts/validate_v1_kdn_roundtrip.py | 5 ++- test/demo_client.py | 5 ++- test/demo_instance.py | 5 ++- test/demo_kdn.py | 5 ++- test/demo_proxy.py | 5 ++- test/demo_scheduler.py | 5 ++- test/test_namespace_layout.py | 39 +++++++++++++++++++ test/test_repository_governance.py | 56 ++++++++++++++++------------ test/test_source_checkout_imports.py | 10 +++++ test/test_wheel_install.py | 13 ++++++- 14 files changed, 155 insertions(+), 44 deletions(-) create mode 100644 test/test_source_checkout_imports.py diff --git a/conftest.py b/conftest.py index f1e3ee2..ae97e63 100644 --- a/conftest.py +++ b/conftest.py @@ -6,5 +6,6 @@ ROOT = Path(__file__).resolve().parent -if str(ROOT) not in sys.path: - sys.path.insert(0, str(ROOT)) +for source_root in (ROOT, ROOT / "src"): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) diff --git a/core/config.py b/core/config.py index 6f2138e..ee2c0f1 100644 --- a/core/config.py +++ b/core/config.py @@ -9,8 +9,9 @@ from pathlib import Path ROOT_DIR = Path(__file__).resolve().parents[1] -if str(ROOT_DIR) not in sys.path: - sys.path.append(str(ROOT_DIR)) +for source_root in (ROOT_DIR, ROOT_DIR / "src"): + if str(source_root) not in sys.path: + sys.path.append(str(source_root)) # Default model settings DEFAULT_MODEL = "/workspace/llm-stack/models/LLM-Research/Meta-Llama-3-70B-Instruct" # Default main model path, used when Scheduler/Client does not specify one diff --git a/doc/package_migration_phase_a.md b/doc/package_migration_phase_a.md index 449b3b6..4426951 100644 --- a/doc/package_migration_phase_a.md +++ b/doc/package_migration_phase_a.md @@ -126,7 +126,6 @@ instance/TTFT_predictor/data/log-bs7-rtx5090-8-llama3-70b.txt instance/TTFT_predictor/data/log-bs8-rtx5090-8-llama3-70b.txt instance/TTFT_predictor/data/补录数据.txt instance/TTFT_predictor/data/补录数据(小长度).txt -instance/TTFT_predictor/prompt_length_validation.log instance/resource_dashboard/static/app.js instance/resource_dashboard/static/index.html instance/resource_dashboard/static/style.css @@ -147,12 +146,28 @@ proxy/metrics/ttft_benchmark_table.json proxy/metrics/ttft_coefficients.json test/test_namespace_layout.py test/test_repository_governance.py +test/test_source_checkout_imports.py ``` The baseline wheel contained zero non-Python, non-metadata members. The -corrected wheel contains 36: the UI/dashboard assets, predictor data, model +corrected wheel contains 35: the UI/dashboard assets, predictor data, model configuration, and metric tables listed above. These additions make required runtime package data explicit; they do not replace or remove baseline data. +The generated `instance/TTFT_predictor/prompt_length_validation.log` is +explicitly excluded. At the same source head, its removal reduced the wheel +from 583,868 bytes to 583,476 bytes. + +Package-data groups are limited to runtime inputs: + +- `UI.client_ui` and `UI.proxy_ui`: browser JavaScript, CSS, and HTML assets; +- `instance.resource_dashboard`: dashboard JavaScript, CSS, and HTML assets; +- `instance.TTFT_predictor`: checked-in calibration tables and their data note; +- `proxy.metrics`: checked-in prediction coefficients, benchmark tables, and + calibration inputs; +- `model`: the runtime model configuration YAML. + +Generated logs, test output, temporary files, and standalone documentation are +not selected by the package-data patterns. ## Reference audit @@ -165,12 +180,13 @@ listed under “changed mapping” were modified. |---|---| | Normal compatibility imports | `core/runtime_compat.py`, `kdn_server/domain/models.py`, `test/test_runtime_compat.py`, and `test/test_wheel_install.py` | | Dynamic/importlib references | `instance/capability_builder.py` uses distribution metadata; no dynamic string names target either migrated package | -| `python -m` commands | Operational/docs commands occur in `README.md`, `doc/`, `env/`, component READMEs, and `test/README.md`; none target the migrated package | +| `python -m` commands | Operational/docs commands occur in `README.md`, `doc/`, `env/`, component READMEs, and `test/README.md`; application commands follow documented installation, while pytest uses the centralized root `conftest.py` source bootstrap | | Uvicorn/FastAPI module strings | `instance/TTFT_predictor/prefill_prediction_server.py`, its README/workflow, and `UI/proxy_ui/README.md`; none target the migrated package | | Subprocess module invocations | `client/perf_client.py`, Instance dashboards, test demo launchers, resource-monitor tests, and wheel tests; only the wheel test exercises the migrated imports | | Monkeypatch/mock target strings | Tests under `test/` and `test/kdn/`; none target the migrated implementation path | | Docker/Compose/shell/CI | `Dockerfile`, `env/docker/cu130/`, `env/README.md`, `scripts/`, and `.github/`; no Compose file or migrated-package reference was found | | Markdown references | `core/README.md` and `test/kdn/README.md` referenced the old implementation path and were updated; compatibility history remains explicitly marked deprecated | +| Source checkout path bootstraps | Root `conftest.py`; `test/demo_client.py`, `test/demo_instance.py`, `test/demo_kdn.py`, `test/demo_proxy.py`, and `test/demo_scheduler.py`; `scripts/validate_v1_kdn_roundtrip.py`; and `core/config.py` add both the repository root and `src`. `test/test_demo_instance_ui.py` has a test-only root bootstrap, while `test/demo_resource_monitor_e2e.py` uses its root path for subprocess working directories. Isolated wheel tests run elsewhere and do not use any source bootstrap. | The reproducible inventory commands are: @@ -217,3 +233,15 @@ package is migrated or intentionally deprecated through review. 5. Packaging data files used by runtime modules requires review during each component migration; Phase A deliberately preserves the prior package list and runtime behavior rather than broadening that scope. + +Run the network-dependent full clean-wheel validation explicitly with: + +```bash +CACHEROUTE_RUN_NETWORK_TESTS=1 \ + python3 -m pytest -q test/test_wheel_install.py -m network +``` + +For an offline complete wheelhouse, also set +`CACHEROUTE_TEST_WHEELHOUSE=/path/to/wheelhouse`; pip then uses `--no-index` and +`--find-links`. A default skip is not release evidence: the network-marked test +must pass once against a package index or complete wheelhouse before merge. diff --git a/pyproject.toml b/pyproject.toml index 35f244b..cc7d7dd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -67,6 +67,11 @@ packages = [ "UI.client_ui" = ["static/*", "templates/*"] "UI.proxy_ui" = ["static/*"] "instance.resource_dashboard" = ["static/*"] -"instance.TTFT_predictor" = ["data/*.md", "data/*.txt", "*.log"] +"instance.TTFT_predictor" = ["data/*.md", "data/*.txt"] "proxy.metrics" = ["*.json", "data/*.json", "data/*.txt"] model = ["*.yaml"] + +[tool.pytest.ini_options] +markers = [ + "network: requires external package indexes or a complete local wheelhouse", +] diff --git a/scripts/validate_v1_kdn_roundtrip.py b/scripts/validate_v1_kdn_roundtrip.py index 40688af..28d0e7f 100644 --- a/scripts/validate_v1_kdn_roundtrip.py +++ b/scripts/validate_v1_kdn_roundtrip.py @@ -20,8 +20,9 @@ from urllib.request import Request, urlopen REPO_ROOT = Path(__file__).resolve().parents[1] -if str(REPO_ROOT) not in sys.path: - sys.path.insert(0, str(REPO_ROOT)) +for source_root in (REPO_ROOT, REPO_ROOT / "src"): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) import redis # noqa: E402 diff --git a/test/demo_client.py b/test/demo_client.py index 9fb3113..d80d221 100644 --- a/test/demo_client.py +++ b/test/demo_client.py @@ -23,8 +23,9 @@ def _ensure_project_root_on_syspath() -> None: while still importing packages such as client/ and UI/ from the project root. """ root = Path(__file__).resolve().parents[1] # test/ -> project root - if str(root) not in sys.path: - sys.path.insert(0, str(root)) + for source_root in (root, root / "src"): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) def run_cli() -> None: diff --git a/test/demo_instance.py b/test/demo_instance.py index 91fdad0..de00191 100644 --- a/test/demo_instance.py +++ b/test/demo_instance.py @@ -13,8 +13,9 @@ from pathlib import Path ROOT_DIR = Path(__file__).resolve().parents[1] -if str(ROOT_DIR) not in sys.path: - sys.path.append(str(ROOT_DIR)) +for source_root in (ROOT_DIR, ROOT_DIR / "src"): + if str(source_root) not in sys.path: + sys.path.append(str(source_root)) import importlib.util diff --git a/test/demo_kdn.py b/test/demo_kdn.py index eec591f..ac70310 100644 --- a/test/demo_kdn.py +++ b/test/demo_kdn.py @@ -4,8 +4,9 @@ from pathlib import Path ROOT_DIR = Path(__file__).resolve().parents[1] -if str(ROOT_DIR) not in sys.path: - sys.path.append(str(ROOT_DIR)) +for source_root in (ROOT_DIR, ROOT_DIR / "src"): + if str(source_root) not in sys.path: + sys.path.append(str(source_root)) from kdn_server.kdn_api import kdn from core import config diff --git a/test/demo_proxy.py b/test/demo_proxy.py index d652b5d..6afdd23 100644 --- a/test/demo_proxy.py +++ b/test/demo_proxy.py @@ -18,8 +18,9 @@ from pathlib import Path ROOT_DIR = Path(__file__).resolve().parents[1] -if str(ROOT_DIR) not in sys.path: - sys.path.append(str(ROOT_DIR)) +for source_root in (ROOT_DIR, ROOT_DIR / "src"): + if str(source_root) not in sys.path: + sys.path.append(str(source_root)) _CONFIG_SPEC = importlib.util.spec_from_file_location("cacheroute_core_config", ROOT_DIR / "core" / "config.py") if _CONFIG_SPEC is None or _CONFIG_SPEC.loader is None: diff --git a/test/demo_scheduler.py b/test/demo_scheduler.py index 414e8f0..ea9fc0f 100644 --- a/test/demo_scheduler.py +++ b/test/demo_scheduler.py @@ -15,8 +15,9 @@ # Allow direct execution from test/ while importing project packages from the # repository root, consistent with the other demo entrypoints. ROOT_DIR = Path(__file__).resolve().parents[1] -if str(ROOT_DIR) not in sys.path: - sys.path.insert(0, str(ROOT_DIR)) +for source_root in (ROOT_DIR, ROOT_DIR / "src"): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) from scheduler import scheduler from core import config diff --git a/test/test_namespace_layout.py b/test/test_namespace_layout.py index 6bda981..ed778fc 100644 --- a/test/test_namespace_layout.py +++ b/test/test_namespace_layout.py @@ -2,6 +2,7 @@ import subprocess import sys +from pathlib import Path PROHIBITED_MODULES = ( @@ -11,8 +12,11 @@ def _run_isolated(import_statement, exercise=""): + repo = Path(__file__).resolve().parents[1] script = f""" import sys +import runpy +runpy.run_path({str(repo / 'conftest.py')!r}) {import_statement} {exercise} @@ -35,3 +39,38 @@ def test_compat_import_is_dependency_light_and_functional(): def test_observability_import_is_dependency_light(): _run_isolated("import cacheroute.observability") + + +def test_demo_client_bootstrap_resolves_source_namespace(tmp_path): + repo = Path(__file__).resolve().parents[1] + script = f""" +from pathlib import Path +import runpy +namespace = runpy.run_path({str(repo / 'test' / 'demo_client.py')!r}) +namespace['_ensure_project_root_on_syspath']() +import cacheroute.compat +path = Path(cacheroute.compat.__file__).resolve() +print(f'cacheroute.compat={{path}}') +assert path.is_relative_to({str(repo / 'src')!r}) +""" + result = subprocess.run( + [sys.executable, "-I", "-S", "-c", script], + cwd=tmp_path, + check=True, + text=True, + capture_output=True, + ) + assert "src/cacheroute/compat/__init__.py" in result.stdout + + +def test_pytest_collects_source_namespace_without_editable_install(tmp_path): + repo = Path(__file__).resolve().parents[1] + result = subprocess.run( + [sys.executable, "-I", "-m", "pytest", "--collect-only", "-q", + str(repo / "test" / "test_source_checkout_imports.py")], + cwd=tmp_path, + check=True, + text=True, + capture_output=True, + ) + assert "1 test collected" in result.stdout diff --git a/test/test_repository_governance.py b/test/test_repository_governance.py index 06c4648..387d1fa 100644 --- a/test/test_repository_governance.py +++ b/test/test_repository_governance.py @@ -1,14 +1,13 @@ """Repository checks that keep the Phase A migration reviewable.""" import ast +from fnmatch import fnmatchcase +import os from pathlib import Path import re import subprocess import tomllib -from setuptools import find_namespace_packages - - ROOT = Path(__file__).resolve().parents[1] ALLOWED_TOP_LEVEL_DIRECTORIES = { ".assets", ".git", ".github", ".pytest_cache", "UI", "__pycache__", @@ -38,18 +37,16 @@ "src", "src.*", "build", "build.*", "dist", "dist.*", "wheelhouse", "wheelhouse.*", ".venv", ".venv.*", "*.egg-info", "*.egg-info.*", ".pytest_cache", ".pytest_cache.*", "__pycache__", "__pycache__.*", + "*.__pycache__", "*.__pycache__.*", ".mypy_cache", ".mypy_cache.*", ".ruff_cache", ".ruff_cache.*", "tests", "tests.*", "docs", "docs.*", ] def test_no_unreviewed_functional_root_directories(): - result = subprocess.run( - ["git", "ls-files", "-z"], cwd=ROOT, check=True, capture_output=True, - ) tracked = { - Path(raw.decode()).parts[0] - for raw in result.stdout.split(b"\0") if raw and len(Path(raw.decode()).parts) > 1 + path.relative_to(ROOT).parts[0] + for path in _tracked_files() if len(path.relative_to(ROOT).parts) > 1 } assert tracked <= ALLOWED_TOP_LEVEL_DIRECTORIES @@ -57,13 +54,27 @@ def test_no_unreviewed_functional_root_directories(): def test_transitional_explicit_packages_match_root_discovery(): configuration = tomllib.loads((ROOT / "pyproject.toml").read_text(encoding="utf-8")) configured = set(configuration["tool"]["setuptools"]["packages"]) - discovered_root = set(find_namespace_packages( - where=str(ROOT), exclude=GENERATED_PACKAGE_EXCLUDES, - )) + discovered_root = _discover_root_namespace_packages() assert CANONICAL_PACKAGES <= configured assert configured - CANONICAL_PACKAGES == discovered_root +def _discover_root_namespace_packages(): + packages = set() + for directory, child_directories, _files in os.walk(ROOT): + relative = Path(directory).relative_to(ROOT) + if relative == Path("."): + child_directories[:] = [name for name in child_directories if not name.startswith(".")] + continue + package = ".".join(relative.parts) + if any(fnmatchcase(package, pattern) for pattern in GENERATED_PACKAGE_EXCLUDES): + child_directories[:] = [] + continue + if not any(part.startswith(".") for part in relative.parts): + packages.add(package) + return packages + + def test_legacy_compatibility_references_are_narrowly_allowlisted(): references = _files_containing("cacheroute_compat") assert references <= LEGACY_REFERENCE_ALLOWLIST @@ -77,15 +88,7 @@ def test_observability_legacy_references_are_narrowly_allowlisted(): def _files_containing(needle): references = set() - for path in ROOT.rglob("*"): - if ( - not path.is_file() - or ".git" in path.parts - or "build" in path.parts - or "__pycache__" in path.parts - or any(part.endswith(".egg-info") for part in path.parts) - ): - continue + for path in _tracked_files(): try: if needle in path.read_text(encoding="utf-8"): references.add(path.relative_to(ROOT)) @@ -94,6 +97,15 @@ def _files_containing(needle): return references +def _tracked_files(): + result = subprocess.run( + ["git", "ls-files", "-z"], cwd=ROOT, check=True, capture_output=True, + ) + return tuple( + ROOT / raw.decode() for raw in result.stdout.split(b"\0") if raw + ) + + def test_legacy_shim_contains_imports_only(): for relative in ("src/cacheroute_compat/__init__.py", "src/cacheroute_compat/runtime.py"): module = ast.parse((ROOT / relative).read_text(encoding="utf-8")) @@ -114,9 +126,7 @@ def test_local_markdown_links_resolve(): inline_pattern = re.compile(r"!?\[[^]]*]\(([^)]+)\)") reference_pattern = re.compile(r"^\s*\[[^]]+]\s*:\s*(\S+)", re.MULTILINE) fence_pattern = re.compile(r"^\s*```.*?^\s*```\s*$", re.MULTILINE | re.DOTALL) - for markdown in ROOT.rglob("*.md"): - if ".git" in markdown.parts: - continue + for markdown in (path for path in _tracked_files() if path.suffix == ".md"): text = fence_pattern.sub("", markdown.read_text(encoding="utf-8")) targets = inline_pattern.findall(text) + reference_pattern.findall(text) for raw_target in targets: diff --git a/test/test_source_checkout_imports.py b/test/test_source_checkout_imports.py new file mode 100644 index 0000000..f824b37 --- /dev/null +++ b/test/test_source_checkout_imports.py @@ -0,0 +1,10 @@ +"""Source-checkout import sentinel loaded through the root pytest configuration.""" + +from pathlib import Path + +import cacheroute.compat + + +def test_canonical_compatibility_resolves_from_src(): + repo = Path(__file__).resolve().parents[1] + assert Path(cacheroute.compat.__file__).resolve().is_relative_to(repo / "src") diff --git a/test/test_wheel_install.py b/test/test_wheel_install.py index 687c7ca..f944775 100644 --- a/test/test_wheel_install.py +++ b/test/test_wheel_install.py @@ -1,6 +1,7 @@ """Verify wheel contents and imports without using the source checkout.""" from pathlib import Path +import os import subprocess import sys import venv @@ -70,6 +71,7 @@ def test_wheel_preserves_packages_and_runtime_data(built_wheel): } assert packaged == EXPECTED_TOP_LEVEL_PACKAGES assert REQUIRED_PACKAGE_DATA <= members + assert "instance/TTFT_predictor/prompt_length_validation.log" not in members def test_dependency_light_clean_wheel_imports(built_wheel, tmp_path): @@ -100,11 +102,20 @@ def test_dependency_light_clean_wheel_imports(built_wheel, tmp_path): print(result.stdout, end="") +@pytest.mark.network +@pytest.mark.skipif( + os.getenv("CACHEROUTE_RUN_NETWORK_TESTS") != "1", + reason="set CACHEROUTE_RUN_NETWORK_TESTS=1 to install declared dependencies in a clean venv", +) def test_full_public_imports_from_clean_wheel(built_wheel, tmp_path): python = _create_isolated_environment(tmp_path / "full-venv") # This deliberately installs declared dependencies. An unavailable package # index must fail the test rather than leaking host packages into the venv. - subprocess.run([python, "-m", "pip", "install", str(built_wheel)], check=True) + install = [python, "-m", "pip", "install"] + wheelhouse = os.getenv("CACHEROUTE_TEST_WHEELHOUSE") + if wheelhouse: + install.extend(["--no-index", "--find-links", wheelhouse]) + subprocess.run([*install, str(built_wheel)], check=True) result = _run_outside_repo(python, tmp_path / "outside-full", """ from pathlib import Path import sys From 6f41050db56b48b72d3c2ac974c81c24119b4e12 Mon Sep 17 00:00:00 2001 From: RickZ <121943721+rickisba@users.noreply.github.com> Date: Wed, 29 Jul 2026 21:23:07 +0800 Subject: [PATCH 5/6] fix: keep source bootstraps out of config modules --- client/client.py | 8 ++++++ client/kv_timing_sender.py | 7 ++--- core/config.py | 4 --- doc/package_migration_phase_a.md | 4 +-- kdn_server/kdn_register_cli.py | 10 ++++++- scheduler/scheduler_cli.py | 8 ++++++ store/knowledge_build.py | 9 ++++--- test/test_namespace_layout.py | 13 ---------- test/test_repository_governance.py | 27 +++++++++++++++++++ test/test_source_checkout_imports.py | 39 ++++++++++++++++++++++++++-- test/test_wheel_install.py | 12 ++++++++- util/kdn_build_kv.py | 7 +++++ 12 files changed, 118 insertions(+), 30 deletions(-) diff --git a/client/client.py b/client/client.py index 54f218c..195c5aa 100644 --- a/client/client.py +++ b/client/client.py @@ -17,10 +17,18 @@ import json import logging import shlex +import sys from dataclasses import dataclass +from pathlib import Path from typing import Any, Dict, List, Optional from urllib.parse import urlparse +# Support direct execution from a source checkout before project imports. +_REPO_ROOT = Path(__file__).resolve().parents[1] +for source_root in (_REPO_ROOT, _REPO_ROOT / "src"): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) + from core.config import REQUIRED_FIELDS, ALLOWED_OPTION_FIELDS import requests diff --git a/client/kv_timing_sender.py b/client/kv_timing_sender.py index 3e3ef4d..247ebb9 100644 --- a/client/kv_timing_sender.py +++ b/client/kv_timing_sender.py @@ -21,10 +21,11 @@ import httpx -# Allow repository modules to be imported when running `python client/kv_timing_sender.py`. +# Support direct execution from a source checkout before project imports. _REPO_ROOT = Path(__file__).resolve().parents[1] -if str(_REPO_ROOT) not in sys.path: - sys.path.insert(0, str(_REPO_ROOT)) +for source_root in (_REPO_ROOT, _REPO_ROOT / "src"): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) from proxy.metrics.queue_predictor import queue_predictor from core.tokenizer_registry import estimate_tokens, TokenizerRegistry diff --git a/core/config.py b/core/config.py index ee2c0f1..7251846 100644 --- a/core/config.py +++ b/core/config.py @@ -5,13 +5,9 @@ - Centralizes all default values - Provides a clear configuration reference for deployment """ -import sys from pathlib import Path ROOT_DIR = Path(__file__).resolve().parents[1] -for source_root in (ROOT_DIR, ROOT_DIR / "src"): - if str(source_root) not in sys.path: - sys.path.append(str(source_root)) # Default model settings DEFAULT_MODEL = "/workspace/llm-stack/models/LLM-Research/Meta-Llama-3-70B-Instruct" # Default main model path, used when Scheduler/Client does not specify one diff --git a/doc/package_migration_phase_a.md b/doc/package_migration_phase_a.md index 4426951..343440c 100644 --- a/doc/package_migration_phase_a.md +++ b/doc/package_migration_phase_a.md @@ -155,7 +155,7 @@ configuration, and metric tables listed above. These additions make required runtime package data explicit; they do not replace or remove baseline data. The generated `instance/TTFT_predictor/prompt_length_validation.log` is explicitly excluded. At the same source head, its removal reduced the wheel -from 583,868 bytes to 583,476 bytes. +from 585,197 bytes to 584,806 bytes. Package-data groups are limited to runtime inputs: @@ -186,7 +186,7 @@ listed under “changed mapping” were modified. | Monkeypatch/mock target strings | Tests under `test/` and `test/kdn/`; none target the migrated implementation path | | Docker/Compose/shell/CI | `Dockerfile`, `env/docker/cu130/`, `env/README.md`, `scripts/`, and `.github/`; no Compose file or migrated-package reference was found | | Markdown references | `core/README.md` and `test/kdn/README.md` referenced the old implementation path and were updated; compatibility history remains explicitly marked deprecated | -| Source checkout path bootstraps | Root `conftest.py`; `test/demo_client.py`, `test/demo_instance.py`, `test/demo_kdn.py`, `test/demo_proxy.py`, and `test/demo_scheduler.py`; `scripts/validate_v1_kdn_roundtrip.py`; and `core/config.py` add both the repository root and `src`. `test/test_demo_instance_ui.py` has a test-only root bootstrap, while `test/demo_resource_monitor_e2e.py` uses its root path for subprocess working directories. Isolated wheel tests run elsewhere and do not use any source bootstrap. | +| Source checkout path bootstraps | Root `conftest.py`; standalone demos in `test/`; `client/client.py`, `client/kv_timing_sender.py`, `kdn_server/kdn_register_cli.py`, `scheduler/scheduler_cli.py`, `scripts/validate_v1_kdn_roundtrip.py`, `store/knowledge_build.py`, and `util/kdn_build_kv.py` add both the repository root and `src` before project imports. `test/test_demo_instance_ui.py` has a test-only root bootstrap, while `test/demo_resource_monitor_e2e.py` uses its root path for subprocess working directories. Ordinary library modules, including `core/config.py`, do not mutate `sys.path`. Isolated wheel tests run elsewhere and do not use any source bootstrap. | The reproducible inventory commands are: diff --git a/kdn_server/kdn_register_cli.py b/kdn_server/kdn_register_cli.py index ae61d9d..59c2b85 100644 --- a/kdn_server/kdn_register_cli.py +++ b/kdn_server/kdn_register_cli.py @@ -2,7 +2,16 @@ import argparse import os import shlex +import sys +from pathlib import Path import requests + +# Support direct execution from a source checkout before project imports. +_REPO_ROOT = Path(__file__).resolve().parents[1] +for source_root in (_REPO_ROOT, _REPO_ROOT / "src"): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) + from core import config """ Unified KDN registration/build tool for Text + KV. @@ -543,4 +552,3 @@ def main(): if __name__ == "__main__": main() - diff --git a/scheduler/scheduler_cli.py b/scheduler/scheduler_cli.py index 88d4880..e01ad34 100644 --- a/scheduler/scheduler_cli.py +++ b/scheduler/scheduler_cli.py @@ -2,11 +2,19 @@ import argparse import os import shlex +import sys import time import requests +from pathlib import Path from urllib.parse import urlparse, urlunparse +# Support direct execution from a source checkout before project imports. +_REPO_ROOT = Path(__file__).resolve().parents[1] +for source_root in (_REPO_ROOT, _REPO_ROOT / "src"): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) + from core import config """ diff --git a/store/knowledge_build.py b/store/knowledge_build.py index 4488ba7..994f471 100644 --- a/store/knowledge_build.py +++ b/store/knowledge_build.py @@ -4,14 +4,15 @@ import sys from pathlib import Path from typing import List, Dict, Any, Optional -from core.config import DEFAULT_EMBED_MODEL import yaml ROOT_DIR = Path(__file__).resolve().parents[1] -if str(ROOT_DIR) not in sys.path: - sys.path.append(str(ROOT_DIR)) +for source_root in (ROOT_DIR, ROOT_DIR / "src"): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) KNOWLEDGE_YAML_PATH = ROOT_DIR / "data" +from core.config import DEFAULT_EMBED_MODEL from model import EmbeddingEngine def build_knowledge_yaml( @@ -108,4 +109,4 @@ def build_knowledge_yaml( source_path= str(KNOWLEDGE_YAML_PATH / "raw_data.yaml"), output_path=str(KNOWLEDGE_YAML_PATH / "knowledge_base.yaml"), model_name="intfloat/multilingual-e5-large-instruct", - ) \ No newline at end of file + ) diff --git a/test/test_namespace_layout.py b/test/test_namespace_layout.py index ed778fc..866920a 100644 --- a/test/test_namespace_layout.py +++ b/test/test_namespace_layout.py @@ -61,16 +61,3 @@ def test_demo_client_bootstrap_resolves_source_namespace(tmp_path): capture_output=True, ) assert "src/cacheroute/compat/__init__.py" in result.stdout - - -def test_pytest_collects_source_namespace_without_editable_install(tmp_path): - repo = Path(__file__).resolve().parents[1] - result = subprocess.run( - [sys.executable, "-I", "-m", "pytest", "--collect-only", "-q", - str(repo / "test" / "test_source_checkout_imports.py")], - cwd=tmp_path, - check=True, - text=True, - capture_output=True, - ) - assert "1 test collected" in result.stdout diff --git a/test/test_repository_governance.py b/test/test_repository_governance.py index 387d1fa..d79d522 100644 --- a/test/test_repository_governance.py +++ b/test/test_repository_governance.py @@ -33,6 +33,20 @@ "cacheroute", "cacheroute.compat", "cacheroute.observability", "cacheroute_compat", } +SOURCE_BOOTSTRAP_ENTRYPOINTS = { + Path("client/client.py"), + Path("client/kv_timing_sender.py"), + Path("kdn_server/kdn_register_cli.py"), + Path("scheduler/scheduler_cli.py"), + Path("scripts/validate_v1_kdn_roundtrip.py"), + Path("store/knowledge_build.py"), + Path("test/demo_client.py"), + Path("test/demo_instance.py"), + Path("test/demo_kdn.py"), + Path("test/demo_proxy.py"), + Path("test/demo_scheduler.py"), + Path("util/kdn_build_kv.py"), +} GENERATED_PACKAGE_EXCLUDES = [ "src", "src.*", "build", "build.*", "dist", "dist.*", "wheelhouse", "wheelhouse.*", ".venv", ".venv.*", "*.egg-info", "*.egg-info.*", @@ -121,6 +135,19 @@ def test_legacy_shim_contains_imports_only(): assert functional == [] +def test_source_bootstraps_stay_at_test_and_entrypoint_boundaries(): + config_tree = ast.parse((ROOT / "core/config.py").read_text(encoding="utf-8")) + assert not any(isinstance(node, (ast.Import, ast.ImportFrom)) and any( + alias.name == "sys" for alias in node.names + ) for node in config_tree.body) + assert "sys.path" not in (ROOT / "core/config.py").read_text(encoding="utf-8") + + for relative in SOURCE_BOOTSTRAP_ENTRYPOINTS: + text = (ROOT / relative).read_text(encoding="utf-8") + assert ' / "src"' in text + assert "sys.path" in text + + def test_local_markdown_links_resolve(): missing = [] inline_pattern = re.compile(r"!?\[[^]]*]\(([^)]+)\)") diff --git a/test/test_source_checkout_imports.py b/test/test_source_checkout_imports.py index f824b37..42c7c8d 100644 --- a/test/test_source_checkout_imports.py +++ b/test/test_source_checkout_imports.py @@ -1,10 +1,45 @@ -"""Source-checkout import sentinel loaded through the root pytest configuration.""" +"""Exercise application imports from outside an uninstalled source checkout.""" +import os from pathlib import Path +import subprocess +import sys import cacheroute.compat +from core.runtime_compat import normalize_runtime_profile +from kdn_server.domain import RuntimeProfile +import kdn_server.kv_builder -def test_canonical_compatibility_resolves_from_src(): +def _assert_source_imports(): repo = Path(__file__).resolve().parents[1] assert Path(cacheroute.compat.__file__).resolve().is_relative_to(repo / "src") + assert normalize_runtime_profile("modern") == "v1" + assert RuntimeProfile.normalize("old") is RuntimeProfile.LEGACY + assert Path(kdn_server.kv_builder.__file__).resolve().is_relative_to(repo) + + +def test_application_imports_from_outside_source_checkout(tmp_path): + if os.getenv("CACHEROUTE_SOURCE_CHECKOUT_CHILD") == "1": + _assert_source_imports() + print(f"cacheroute.compat={Path(cacheroute.compat.__file__).resolve()}") + print(f"kdn_server.kv_builder={Path(kdn_server.kv_builder.__file__).resolve()}") + return + + repo = Path(__file__).resolve().parents[1] + environment = os.environ.copy() + environment["CACHEROUTE_SOURCE_CHECKOUT_CHILD"] = "1" + environment.pop("PYTHONPATH", None) + result = subprocess.run( + [sys.executable, "-I", "-m", "pytest", "-q", "-s", str(Path(__file__).resolve())], + cwd=tmp_path, + env=environment, + check=True, + text=True, + capture_output=True, + ) + assert "1 passed" in result.stdout + assert "src/cacheroute/compat/__init__.py" in result.stdout + assert "kdn_server/kv_builder.py" in result.stdout + print(result.stdout, end="") + _assert_source_imports() diff --git a/test/test_wheel_install.py b/test/test_wheel_install.py index f944775..233077b 100644 --- a/test/test_wheel_install.py +++ b/test/test_wheel_install.py @@ -2,6 +2,7 @@ from pathlib import Path import os +import shutil import subprocess import sys import venv @@ -38,8 +39,17 @@ def built_wheel(tmp_path_factory): repo = Path(__file__).resolve().parents[1] wheelhouse = tmp_path_factory.mktemp("wheelhouse") + source = tmp_path_factory.mktemp("wheel-source") / "CacheRoute" + shutil.copytree( + repo, + source, + ignore=shutil.ignore_patterns( + ".git", "build", "dist", "*.egg-info", "__pycache__", + ".pytest_cache", ".mypy_cache", ".ruff_cache", ".venv", "wheelhouse", + ), + ) subprocess.run( - [sys.executable, "-m", "pip", "wheel", str(repo), "--no-deps", "--no-build-isolation", "-w", str(wheelhouse)], + [sys.executable, "-m", "pip", "wheel", str(source), "--no-deps", "--no-build-isolation", "-w", str(wheelhouse)], check=True, ) return next(wheelhouse.glob("cacheroute-*.whl")) diff --git a/util/kdn_build_kv.py b/util/kdn_build_kv.py index f83d138..7e7078e 100644 --- a/util/kdn_build_kv.py +++ b/util/kdn_build_kv.py @@ -1,7 +1,14 @@ #!/usr/bin/env python3 import argparse +import sys from pathlib import Path +# Support direct execution from a source checkout before project imports. +REPO_ROOT = Path(__file__).resolve().parents[1] +for source_root in (REPO_ROOT, REPO_ROOT / "src"): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) + from kdn_server.kv_builder import KVBuildConfig, KVCacheBuilder From d35d8fec06c7bc8ce106aa15ee7b1edd12e82ade Mon Sep 17 00:00:00 2001 From: RickZ <121943721+rickisba@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:35:09 +0800 Subject: [PATCH 6/6] fix: guard source bootstraps on direct execution --- client/client.py | 17 +++++--- client/kv_timing_sender.py | 17 +++++--- doc/package_migration_phase_a.md | 4 +- kdn_server/kdn_register_cli.py | 15 ++++--- scheduler/scheduler_cli.py | 15 ++++--- scripts/validate_v1_kdn_roundtrip.py | 14 +++++-- store/knowledge_build.py | 16 +++++-- test/test_namespace_layout.py | 24 +++++++++++ test/test_repository_governance.py | 62 ++++++++++++++++++++++++++-- test/test_source_checkout_imports.py | 17 +++++++- test/test_wheel_install.py | 5 +++ util/kdn_build_kv.py | 15 +++++-- 12 files changed, 182 insertions(+), 39 deletions(-) diff --git a/client/client.py b/client/client.py index 195c5aa..ff7904e 100644 --- a/client/client.py +++ b/client/client.py @@ -23,11 +23,16 @@ from typing import Any, Dict, List, Optional from urllib.parse import urlparse -# Support direct execution from a source checkout before project imports. -_REPO_ROOT = Path(__file__).resolve().parents[1] -for source_root in (_REPO_ROOT, _REPO_ROOT / "src"): - if str(source_root) not in sys.path: - sys.path.insert(0, str(source_root)) +def _bootstrap_source_checkout() -> None: + """Expose repository packages only for direct source-file execution.""" + repo_root = Path(__file__).resolve().parents[1] + for source_root in (repo_root, repo_root / "src"): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) + + +if __package__ in (None, ""): + _bootstrap_source_checkout() from core.config import REQUIRED_FIELDS, ALLOWED_OPTION_FIELDS @@ -455,4 +460,4 @@ def run_repl() -> None: pretty_print_response(resp, parsed.body) if __name__ == "__main__": - run_repl() \ No newline at end of file + run_repl() diff --git a/client/kv_timing_sender.py b/client/kv_timing_sender.py index 247ebb9..0d6de52 100644 --- a/client/kv_timing_sender.py +++ b/client/kv_timing_sender.py @@ -21,11 +21,16 @@ import httpx -# Support direct execution from a source checkout before project imports. -_REPO_ROOT = Path(__file__).resolve().parents[1] -for source_root in (_REPO_ROOT, _REPO_ROOT / "src"): - if str(source_root) not in sys.path: - sys.path.insert(0, str(source_root)) +def _bootstrap_source_checkout() -> None: + """Expose repository packages only for direct source-file execution.""" + repo_root = Path(__file__).resolve().parents[1] + for source_root in (repo_root, repo_root / "src"): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) + + +if __package__ in (None, ""): + _bootstrap_source_checkout() from proxy.metrics.queue_predictor import queue_predictor from core.tokenizer_registry import estimate_tokens, TokenizerRegistry @@ -651,4 +656,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/doc/package_migration_phase_a.md b/doc/package_migration_phase_a.md index 343440c..ffcaf16 100644 --- a/doc/package_migration_phase_a.md +++ b/doc/package_migration_phase_a.md @@ -155,7 +155,7 @@ configuration, and metric tables listed above. These additions make required runtime package data explicit; they do not replace or remove baseline data. The generated `instance/TTFT_predictor/prompt_length_validation.log` is explicitly excluded. At the same source head, its removal reduced the wheel -from 585,197 bytes to 584,806 bytes. +from 586,283 bytes to 585,892 bytes. Package-data groups are limited to runtime inputs: @@ -186,7 +186,7 @@ listed under “changed mapping” were modified. | Monkeypatch/mock target strings | Tests under `test/` and `test/kdn/`; none target the migrated implementation path | | Docker/Compose/shell/CI | `Dockerfile`, `env/docker/cu130/`, `env/README.md`, `scripts/`, and `.github/`; no Compose file or migrated-package reference was found | | Markdown references | `core/README.md` and `test/kdn/README.md` referenced the old implementation path and were updated; compatibility history remains explicitly marked deprecated | -| Source checkout path bootstraps | Root `conftest.py`; standalone demos in `test/`; `client/client.py`, `client/kv_timing_sender.py`, `kdn_server/kdn_register_cli.py`, `scheduler/scheduler_cli.py`, `scripts/validate_v1_kdn_roundtrip.py`, `store/knowledge_build.py`, and `util/kdn_build_kv.py` add both the repository root and `src` before project imports. `test/test_demo_instance_ui.py` has a test-only root bootstrap, while `test/demo_resource_monitor_e2e.py` uses its root path for subprocess working directories. Ordinary library modules, including `core/config.py`, do not mutate `sys.path`. Isolated wheel tests run elsewhere and do not use any source bootstrap. | +| Source checkout path bootstraps | Root `conftest.py` and dedicated demos in `test/` bootstrap their explicit test/entrypoint boundary. Dual-purpose `client/client.py`, `client/kv_timing_sender.py`, `kdn_server/kdn_register_cli.py`, `scheduler/scheduler_cli.py`, `scripts/validate_v1_kdn_roundtrip.py`, `store/knowledge_build.py`, and `util/kdn_build_kv.py` call a local bootstrap only when `__package__ in (None, "")`; normal package imports do not mutate `sys.path`. `test/test_demo_instance_ui.py` has a test-only root bootstrap, while `test/demo_resource_monitor_e2e.py` uses its root path for subprocess working directories. Ordinary library modules, including `core/config.py`, remain side-effect free. Isolated wheel tests run elsewhere and do not use any source bootstrap. | The reproducible inventory commands are: diff --git a/kdn_server/kdn_register_cli.py b/kdn_server/kdn_register_cli.py index 59c2b85..11da1b1 100644 --- a/kdn_server/kdn_register_cli.py +++ b/kdn_server/kdn_register_cli.py @@ -6,11 +6,16 @@ from pathlib import Path import requests -# Support direct execution from a source checkout before project imports. -_REPO_ROOT = Path(__file__).resolve().parents[1] -for source_root in (_REPO_ROOT, _REPO_ROOT / "src"): - if str(source_root) not in sys.path: - sys.path.insert(0, str(source_root)) +def _bootstrap_source_checkout() -> None: + """Expose repository packages only for direct source-file execution.""" + repo_root = Path(__file__).resolve().parents[1] + for source_root in (repo_root, repo_root / "src"): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) + + +if __package__ in (None, ""): + _bootstrap_source_checkout() from core import config """ diff --git a/scheduler/scheduler_cli.py b/scheduler/scheduler_cli.py index e01ad34..b10e7f8 100644 --- a/scheduler/scheduler_cli.py +++ b/scheduler/scheduler_cli.py @@ -9,11 +9,16 @@ from urllib.parse import urlparse, urlunparse -# Support direct execution from a source checkout before project imports. -_REPO_ROOT = Path(__file__).resolve().parents[1] -for source_root in (_REPO_ROOT, _REPO_ROOT / "src"): - if str(source_root) not in sys.path: - sys.path.insert(0, str(source_root)) +def _bootstrap_source_checkout() -> None: + """Expose repository packages only for direct source-file execution.""" + repo_root = Path(__file__).resolve().parents[1] + for source_root in (repo_root, repo_root / "src"): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) + + +if __package__ in (None, ""): + _bootstrap_source_checkout() from core import config diff --git a/scripts/validate_v1_kdn_roundtrip.py b/scripts/validate_v1_kdn_roundtrip.py index 28d0e7f..224735f 100644 --- a/scripts/validate_v1_kdn_roundtrip.py +++ b/scripts/validate_v1_kdn_roundtrip.py @@ -20,9 +20,17 @@ from urllib.request import Request, urlopen REPO_ROOT = Path(__file__).resolve().parents[1] -for source_root in (REPO_ROOT, REPO_ROOT / "src"): - if str(source_root) not in sys.path: - sys.path.insert(0, str(source_root)) + + +def _bootstrap_source_checkout() -> None: + """Expose repository packages only for direct source-file execution.""" + for source_root in (REPO_ROOT, REPO_ROOT / "src"): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) + + +if __package__ in (None, ""): + _bootstrap_source_checkout() import redis # noqa: E402 diff --git a/store/knowledge_build.py b/store/knowledge_build.py index 994f471..3cba1d0 100644 --- a/store/knowledge_build.py +++ b/store/knowledge_build.py @@ -7,9 +7,19 @@ import yaml ROOT_DIR = Path(__file__).resolve().parents[1] -for source_root in (ROOT_DIR, ROOT_DIR / "src"): - if str(source_root) not in sys.path: - sys.path.insert(0, str(source_root)) + + +def _bootstrap_source_checkout() -> None: + """Expose repository packages only for direct source-file execution.""" + for source_root in (ROOT_DIR, ROOT_DIR / "src"): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) + + +if __package__ in (None, ""): + _bootstrap_source_checkout() + + KNOWLEDGE_YAML_PATH = ROOT_DIR / "data" from core.config import DEFAULT_EMBED_MODEL diff --git a/test/test_namespace_layout.py b/test/test_namespace_layout.py index 866920a..5ce448a 100644 --- a/test/test_namespace_layout.py +++ b/test/test_namespace_layout.py @@ -61,3 +61,27 @@ def test_demo_client_bootstrap_resolves_source_namespace(tmp_path): capture_output=True, ) assert "src/cacheroute/compat/__init__.py" in result.stdout + + +def test_normal_package_imports_do_not_mutate_sys_path(tmp_path): + repo = Path(__file__).resolve().parents[1] + script = f""" +import sys +import runpy +runpy.run_path({str(repo / 'conftest.py')!r}) +before = tuple(sys.path) +import client.client +assert tuple(sys.path) == before +import store.knowledge_build +assert tuple(sys.path) == before +assert not any(path.endswith('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/site-packages/src') for path in sys.path) +print('normal import sys.path unchanged') +""" + result = subprocess.run( + [sys.executable, "-I", "-c", script], + cwd=tmp_path, + check=True, + text=True, + capture_output=True, + ) + assert result.stdout.strip() == "normal import sys.path unchanged" diff --git a/test/test_repository_governance.py b/test/test_repository_governance.py index d79d522..c5dfd90 100644 --- a/test/test_repository_governance.py +++ b/test/test_repository_governance.py @@ -47,6 +47,22 @@ Path("test/demo_scheduler.py"), Path("util/kdn_build_kv.py"), } +GUARDED_SOURCE_BOOTSTRAP_MODULES = { + Path("client/client.py"), + Path("client/kv_timing_sender.py"), + Path("kdn_server/kdn_register_cli.py"), + Path("scheduler/scheduler_cli.py"), + Path("scripts/validate_v1_kdn_roundtrip.py"), + Path("store/knowledge_build.py"), + Path("util/kdn_build_kv.py"), +} +SYS_PATH_ALLOWLIST = SOURCE_BOOTSTRAP_ENTRYPOINTS | { + Path("conftest.py"), + Path("test/test_demo_instance_ui.py"), + Path("test/test_namespace_layout.py"), + Path("test/test_repository_governance.py"), + Path("test/test_wheel_install.py"), +} GENERATED_PACKAGE_EXCLUDES = [ "src", "src.*", "build", "build.*", "dist", "dist.*", "wheelhouse", "wheelhouse.*", ".venv", ".venv.*", "*.egg-info", "*.egg-info.*", @@ -142,10 +158,48 @@ def test_source_bootstraps_stay_at_test_and_entrypoint_boundaries(): ) for node in config_tree.body) assert "sys.path" not in (ROOT / "core/config.py").read_text(encoding="utf-8") - for relative in SOURCE_BOOTSTRAP_ENTRYPOINTS: - text = (ROOT / relative).read_text(encoding="utf-8") - assert ' / "src"' in text - assert "sys.path" in text + files_with_sys_path = { + path.relative_to(ROOT) for path in _tracked_files() + if path.suffix == ".py" and "sys.path" in path.read_text(encoding="utf-8") + } + assert files_with_sys_path <= SYS_PATH_ALLOWLIST + + project_roots = {"core", "kdn_server", "proxy", "instance", "scheduler", "cacheroute"} + for relative in GUARDED_SOURCE_BOOTSTRAP_MODULES: + tree = ast.parse((ROOT / relative).read_text(encoding="utf-8")) + bootstrap_index = next( + index for index, node in enumerate(tree.body) + if isinstance(node, ast.FunctionDef) and node.name == "_bootstrap_source_checkout" + ) + guard_index = next( + index for index, node in enumerate(tree.body) + if isinstance(node, ast.If) + and isinstance(node.test, ast.Compare) + and isinstance(node.test.left, ast.Name) + and node.test.left.id == "__package__" + and any( + isinstance(child, ast.Call) + and isinstance(child.func, ast.Name) + and child.func.id == "_bootstrap_source_checkout" + for child in ast.walk(node) + ) + ) + project_import_indexes = [ + index for index, node in enumerate(tree.body) + if isinstance(node, ast.ImportFrom) and (node.module or "").split(".")[0] in project_roots + or isinstance(node, ast.Import) and any( + alias.name.split(".")[0] in project_roots for alias in node.names + ) + ] + assert bootstrap_index < guard_index < min(project_import_indexes) + function = tree.body[bootstrap_index] + assert any( + isinstance(node, ast.Attribute) + and isinstance(node.value, ast.Name) + and node.value.id == "sys" + and node.attr == "path" + for node in ast.walk(function) + ) def test_local_markdown_links_resolve(): diff --git a/test/test_source_checkout_imports.py b/test/test_source_checkout_imports.py index 42c7c8d..205c7f5 100644 --- a/test/test_source_checkout_imports.py +++ b/test/test_source_checkout_imports.py @@ -38,8 +38,23 @@ def test_application_imports_from_outside_source_checkout(tmp_path): text=True, capture_output=True, ) - assert "1 passed" in result.stdout + assert "2 passed" in result.stdout assert "src/cacheroute/compat/__init__.py" in result.stdout assert "kdn_server/kv_builder.py" in result.stdout print(result.stdout, end="") _assert_source_imports() + + +def test_direct_kdn_builder_entrypoint_bootstraps_source_checkout(tmp_path): + repo = Path(__file__).resolve().parents[1] + environment = os.environ.copy() + environment.pop("PYTHONPATH", None) + result = subprocess.run( + [sys.executable, "-I", str(repo / "util" / "kdn_build_kv.py"), "--help"], + cwd=tmp_path, + env=environment, + check=True, + text=True, + capture_output=True, + ) + assert "--kv-root" in result.stdout diff --git a/test/test_wheel_install.py b/test/test_wheel_install.py index 233077b..2f161e4 100644 --- a/test/test_wheel_install.py +++ b/test/test_wheel_install.py @@ -146,6 +146,11 @@ def test_full_public_imports_from_clean_wheel(built_wheel, tmp_path): path = Path(module.__file__).resolve() print(f"{module.__name__}={path}") assert path.is_relative_to(Path(sys.prefix).resolve()) +before_imports = tuple(sys.path) +import client.client +assert tuple(sys.path) == before_imports +import store.knowledge_build +assert tuple(sys.path) == before_imports assert normalize_runtime_profile("modern") == "v1" assert RuntimeProfile.normalize("old") is RuntimeProfile.LEGACY assert canonical_runtime.__all__ == legacy_runtime.__all__ == core_runtime.__all__ diff --git a/util/kdn_build_kv.py b/util/kdn_build_kv.py index 7e7078e..1c82591 100644 --- a/util/kdn_build_kv.py +++ b/util/kdn_build_kv.py @@ -3,11 +3,18 @@ import sys from pathlib import Path -# Support direct execution from a source checkout before project imports. REPO_ROOT = Path(__file__).resolve().parents[1] -for source_root in (REPO_ROOT, REPO_ROOT / "src"): - if str(source_root) not in sys.path: - sys.path.insert(0, str(source_root)) + + +def _bootstrap_source_checkout() -> None: + """Expose repository packages only for direct source-file execution.""" + for source_root in (REPO_ROOT, REPO_ROOT / "src"): + if str(source_root) not in sys.path: + sys.path.insert(0, str(source_root)) + + +if __package__ in (None, ""): + _bootstrap_source_checkout() from kdn_server.kv_builder import KVBuildConfig, KVCacheBuilder