diff --git a/pdoc/search.py b/pdoc/search.py index 63fcff7c..c197310b 100644 --- a/pdoc/search.py +++ b/pdoc/search.py @@ -46,6 +46,7 @@ from collections.abc import Callable from collections.abc import Mapping +import functools import html import json from pathlib import Path @@ -124,6 +125,16 @@ def make_index(mod: pdoc.doc.Namespace, **extra): return documents +@functools.cache +def node_executable() -> str | None: + if shutil.which("nodejs"): + return "nodejs" + elif shutil.which("node"): + return "node" + else: + return None + + def precompile_index(documents: list[dict], compile_js: Path) -> str: """ This method tries to precompile the Elasticlunr.js search index by invoking `nodejs` or `node`. @@ -136,12 +147,11 @@ def precompile_index(documents: list[dict], compile_js: Path) -> str: """ raw = json.dumps(documents) try: - if shutil.which("nodejs"): - executable = "nodejs" - else: - executable = "node" + node = node_executable() + if node is None: + raise FileNotFoundError("No such file or directory: 'node'") out = subprocess.check_output( - [executable, compile_js], + [node, compile_js], input=raw.encode(), cwd=Path(__file__).parent / "templates", stderr=subprocess.STDOUT, diff --git a/test/test__pydantic.py b/test/test__pydantic.py index 11e6e97c..40112529 100644 --- a/test/test__pydantic.py +++ b/test/test__pydantic.py @@ -12,14 +12,15 @@ def test_no_pydantic(monkeypatch): assert _pydantic.default_value(pdoc.doc.Module, "kind", "module") == "module" -def test_with_pydantic(monkeypatch): - class User(pydantic.BaseModel): - id: int - name: str = pydantic.Field(description="desc", default="Jane Doe") +class ExampleModel(pydantic.BaseModel): + id: int + name: str = pydantic.Field(description="desc", default="Jane Doe") + - assert _pydantic.is_pydantic_model(User) - assert _pydantic.get_field_docstring(User, "name") == "desc" - assert _pydantic.default_value(User, "name", None) == "Jane Doe" +def test_with_pydantic(monkeypatch): + assert _pydantic.is_pydantic_model(ExampleModel) + assert _pydantic.get_field_docstring(ExampleModel, "name") == "desc" + assert _pydantic.default_value(ExampleModel, "name", None) == "Jane Doe" assert not _pydantic.is_pydantic_model(pdoc.doc.Module) assert _pydantic.get_field_docstring(pdoc.doc.Module, "kind") is None diff --git a/test/test_search.py b/test/test_search.py index d2d42ce6..1a23ac90 100644 --- a/test/test_search.py +++ b/test/test_search.py @@ -8,6 +8,24 @@ here = Path(__file__).parent +def test_node_executable(monkeypatch): + monkeypatch.setattr( + shutil, "which", lambda x: "/usr/bin/nodejs" if x == "nodejs" else None + ) + search.node_executable.cache_clear() + assert search.node_executable() == "nodejs" + + monkeypatch.setattr( + shutil, "which", lambda x: "/usr/bin/node" if x == "node" else None + ) + search.node_executable.cache_clear() + assert search.node_executable() == "node" + + monkeypatch.setattr(shutil, "which", lambda _: None) + search.node_executable.cache_clear() + assert search.node_executable() is None + + def test_precompile_index(monkeypatch, capsys): docs = [ { @@ -18,28 +36,25 @@ def test_precompile_index(monkeypatch, capsys): "doc": "a" * 3 * 1024 * 1024, # we only warn if index size is meaningful. } ] + raw = json.dumps(docs) compile_js = here / ".." / "pdoc" / "templates" / "build-search-index.js" monkeypatch.setattr(subprocess, "check_output", lambda *_, **__: '{"foo": 42}') + monkeypatch.setattr(search, "node_executable", lambda: "nodejs") assert ( search.precompile_index(docs, compile_js) == '{"foo": 42, "_isPrebuiltIndex": true}' ) - monkeypatch.setattr(shutil, "which", lambda _: "C:\\nodejs.exe") - assert ( - search.precompile_index(docs, compile_js) - == '{"foo": 42, "_isPrebuiltIndex": true}' - ) - monkeypatch.setattr(shutil, "which", lambda _: None) - assert ( - search.precompile_index(docs, compile_js) - == '{"foo": 42, "_isPrebuiltIndex": true}' - ) + monkeypatch.setattr(search, "node_executable", lambda: None) + assert search.precompile_index(docs, compile_js) == raw def _raise(*_, **__): raise subprocess.CalledProcessError(-1, ["cmd"], b"nodejs error") + monkeypatch.setattr(search, "node_executable", lambda: "node") monkeypatch.setattr(subprocess, "check_output", _raise) - assert search.precompile_index(docs, compile_js) == json.dumps(docs) - assert "pdoc failed to precompile the search index" in capsys.readouterr().out + assert search.precompile_index(docs, compile_js) == raw + out = capsys.readouterr().out + assert "pdoc failed to precompile the search index" in out + assert "Node.js Output" in out diff --git a/test/test_snapshot.py b/test/test_snapshot.py index 77482f6c..54f58ad8 100755 --- a/test/test_snapshot.py +++ b/test/test_snapshot.py @@ -4,7 +4,6 @@ from contextlib import ExitStack import os from pathlib import Path -import shutil import sys import tempfile import warnings @@ -12,6 +11,7 @@ import pytest import pdoc.render +import pdoc.search here = Path(__file__).parent.absolute() @@ -175,6 +175,7 @@ def test_snapshots(snapshot: Snapshot, format: str, monkeypatch): Compare pdoc's rendered output against stored snapshots. """ monkeypatch.chdir(snapshot_dir) + monkeypatch.setattr(pdoc.search, "node_executable", lambda: None) if sys.version_info < snapshot.min_version: pytest.skip( f"Snapshot only works on Python {'.'.join(str(x) for x in snapshot.min_version)} and above." @@ -189,12 +190,7 @@ def test_snapshots(snapshot: Snapshot, format: str, monkeypatch): if __name__ == "__main__": warnings.simplefilter("error") - if not shutil.which("nodejs") and not shutil.which("node"): - print( - "Snapshots include precompiled search indices, " - "but this system does not have Node.js installed to render them. Aborting." - ) - sys.exit(1) + pdoc.search.node_executable = lambda: None # type: ignore os.chdir(snapshot_dir) skipped_some = False for snapshot in snapshots: diff --git a/test/testdata/demopackage_dir.html b/test/testdata/demopackage_dir.html index 3b8c0547..c6c3c9f0 100644 --- a/test/testdata/demopackage_dir.html +++ b/test/testdata/demopackage_dir.html @@ -3,7 +3,7 @@

search.js