diff --git a/pageindex/__init__.py b/pageindex/__init__.py index 0670332..4e15919 100644 --- a/pageindex/__init__.py +++ b/pageindex/__init__.py @@ -74,3 +74,12 @@ __all__ = [ "get_document_structure", "get_page_content", ] + + +def __getattr__(name): + # Lazy so plain `import pageindex` never trips the shims' deprecation + # warnings; they fire only when the legacy attribute is actually used. + if name in ("utils", "page_index_md"): + import importlib + return importlib.import_module(f".{name}", __name__) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/pageindex/backend/cloud.py b/pageindex/backend/cloud.py index 1339a79..8aad060 100644 --- a/pageindex/backend/cloud.py +++ b/pageindex/backend/cloud.py @@ -40,8 +40,6 @@ class CloudBackend: @property def base_url(self) -> str: - # A callable is resolved per request so reassigning client.BASE_URL - # after construction takes effect, matching 0.2.x call-time semantics. return self._base_url() if callable(self._base_url) else self._base_url # ── HTTP helpers ────────────────────────────────────────────────────── diff --git a/pageindex/client.py b/pageindex/client.py index dfe4433..a51df63 100644 --- a/pageindex/client.py +++ b/pageindex/client.py @@ -74,8 +74,8 @@ class PageIndexClient: def _init_cloud(self, api_key: str): from .backend.cloud import CloudBackend from .cloud_api import LegacyCloudAPI - # Pass a callable so BASE_URL is re-read on every request — 0.2.x - # allowed reassigning client.BASE_URL after construction. + # Callable: re-read per request so post-construction BASE_URL + # reassignment (a 0.2.x pattern) still applies. base_url = lambda: self.BASE_URL self._backend = CloudBackend(api_key=api_key, base_url=base_url) self._legacy_cloud_api = LegacyCloudAPI(api_key=api_key, base_url=base_url) diff --git a/pageindex/cloud_api.py b/pageindex/cloud_api.py index ec8971f..37029e2 100644 --- a/pageindex/cloud_api.py +++ b/pageindex/cloud_api.py @@ -25,8 +25,6 @@ class LegacyCloudAPI: @property def base_url(self) -> str: - # A callable is resolved per request so reassigning client.BASE_URL - # after construction takes effect, matching 0.2.x call-time semantics. return self._base_url() if callable(self._base_url) else self._base_url @base_url.setter diff --git a/pageindex/index/page_index.py b/pageindex/index/page_index.py index a0ea4ff..d003e62 100644 --- a/pageindex/index/page_index.py +++ b/pageindex/index/page_index.py @@ -1314,7 +1314,7 @@ def page_index(doc, model=None, toc_check_page_num=None, max_page_num_each_node= "if_add_node_text": if_add_node_text, } user_opt = {k: v for k, v in user_opt.items() if v is not None} - opt = IndexConfig(**user_opt) + opt = IndexConfig.from_yaml(**user_opt) return page_index_main(doc, opt) diff --git a/pageindex/index/utils.py b/pageindex/index/utils.py index edf7ba5..4aa00a0 100644 --- a/pageindex/index/utils.py +++ b/pageindex/index/utils.py @@ -953,14 +953,14 @@ def _coerce_bool(value): class ConfigLoader: - """Legacy 0.2.x config helper. Defaults now come from IndexConfig; this - class no longer reads the packaged ``config.yaml`` (the CLI still uses it - via ``IndexConfig.from_yaml``). Prefer ``pageindex.IndexConfig``. + """Legacy 0.2.x config helper. Defaults come from ``default_path`` (or the + packaged ``config.yaml``), with IndexConfig field defaults filling any keys + the YAML omits. Prefer ``pageindex.IndexConfig`` in new code. """ def __init__(self, default_path=None): from ..config import IndexConfig - self._default_dict = IndexConfig().model_dump() + self._default_dict = IndexConfig.from_yaml(default_path).model_dump() def _validate_keys(self, user_dict): unknown_keys = set(user_dict) - set(self._default_dict) @@ -968,7 +968,7 @@ class ConfigLoader: raise ValueError(f"Unknown config keys: {unknown_keys}") def load(self, user_opt=None) -> _config: - """Merge user options over IndexConfig defaults, returning a namespace.""" + """Merge user options over the YAML defaults, returning a namespace.""" if user_opt is None: user_dict = {} elif isinstance(user_opt, _config): diff --git a/tests/test_legacy_shims.py b/tests/test_legacy_shims.py index 1c57fc4..455e188 100644 --- a/tests/test_legacy_shims.py +++ b/tests/test_legacy_shims.py @@ -62,16 +62,78 @@ def test_get_leaf_nodes_has_331_fix(): assert leaves == [{"title": "Leaf", "start_index": 1, "end_index": 2}] -def test_configloader_no_longer_needs_config_yaml(): - """ConfigLoader must build defaults from IndexConfig, not read config.yaml.""" +def test_configloader_defaults_come_from_packaged_yaml(): + """ConfigLoader must read the packaged config.yaml as its defaults, like + 0.2.x — notably if_add_doc_description ships as "no" there, while the + IndexConfig field default is True (the new-SDK default).""" from pageindex.index.utils import ConfigLoader cfg = ConfigLoader().load({"model": "gpt-5.4"}) assert cfg.model == "gpt-5.4" - assert cfg.if_add_node_summary is True # IndexConfig default + assert cfg.if_add_node_summary is True # config.yaml: "yes" + assert cfg.if_add_doc_description is False # config.yaml: "no" with pytest.raises(ValueError, match="Unknown config keys"): ConfigLoader().load({"nope": 1}) +def test_configloader_reads_custom_yaml_path(tmp_path): + """A custom default_path must be honored; keys the YAML omits fall back to + IndexConfig field defaults.""" + from pageindex.index.utils import ConfigLoader + custom = tmp_path / "my.yaml" + custom.write_text('model: "my-model"\nif_add_node_summary: "no"\n') + cfg = ConfigLoader(str(custom)).load() + assert cfg.model == "my-model" + assert cfg.if_add_node_summary is False + assert cfg.if_add_node_id is True # omitted -> IndexConfig default + + +def test_configloader_missing_custom_yaml_raises(tmp_path): + from pageindex.index.utils import ConfigLoader + with pytest.raises(FileNotFoundError): + ConfigLoader(str(tmp_path / "nope.yaml")) + + +def test_legacy_submodule_attrs_lazy_bound(): + """Shim warnings fire on first attribute use, never at package import. + Subprocess: in-process the attrs may already be bound by other tests.""" + import subprocess + import sys + code = ( + "import warnings\n" + "with warnings.catch_warnings(record=True) as w:\n" + " warnings.simplefilter('always')\n" + " import pageindex\n" + "assert not any('has moved' in str(x.message) for x in w), 'import warned'\n" + "with warnings.catch_warnings(record=True) as w:\n" + " warnings.simplefilter('always')\n" + " assert callable(pageindex.utils.print_tree)\n" + "assert any('pageindex.utils has moved' in str(x.message) for x in w)\n" + "assert callable(pageindex.page_index_md.md_to_tree)\n" + ) + result = subprocess.run([sys.executable, "-c", code], + capture_output=True, text=True, timeout=120) + assert result.returncode == 0, result.stderr + + +def test_unknown_package_attr_still_raises(): + import pageindex + with pytest.raises(AttributeError, match="no attribute 'definitely_not_real'"): + pageindex.definitely_not_real + + +def test_page_index_defaults_follow_config_yaml(monkeypatch): + """page_index() resolution order: explicit args > config.yaml > IndexConfig + field defaults (the 0.2.x contract).""" + import pageindex.index.page_index as pi + captured = {} + monkeypatch.setattr(pi, "page_index_main", + lambda doc, opt: captured.setdefault("opt", opt)) + pi.page_index("dummy.pdf", model="my-model") + opt = captured["opt"] + assert opt.model == "my-model" # explicit arg wins + assert opt.if_add_doc_description is False # config.yaml "no", not True + + def test_configloader_coerces_legacy_yes_no_strings(): """A legacy caller passing 'no' must get a real False, not a truthy string — page_index_main's `if opt.if_add_node_summary:` checks (bare