diff --git a/README.md b/README.md index 654edf5..c7f4332 100644 --- a/README.md +++ b/README.md @@ -9,15 +9,15 @@ with zudb.connect("social.zu1") as conn: conn.execute("INSERT (p:person {uid: 1, name: 'ada'})") conn.execute("INSERT (p:person {uid: $uid, name: $name})", {"uid": 2, "name": "grace"}) - for name, uid in conn.execute("MATCH (p:person) RETURN p.name AS name, p.uid AS uid"): - print(name, uid) + people = conn.execute("MATCH (p:person) RETURN p.name AS name, p.uid AS uid") + print(people.to_pandas()) ``` ``` -pip install zudb +pip install "zudb[pandas]" ``` -No compiler, no `pkg-config`, no postinstall script. One wheel per platform with the engine inside it. +No compiler, no `pkg-config`, no postinstall script. One wheel per platform with the engine inside it. `pip install zudb` on its own brings nothing else at all; the extra above is pandas, which the last line of the snippet asks for and which a result hands its columns to over Arrow. A test in this repository runs that snippet exactly as it is printed, in a directory of its own, because a quickstart is the most read and least compiled code a project has. ## What this is @@ -38,6 +38,8 @@ The interesting parts: A statement writes one row at a time, which is the wrong shape for loading data and cannot make a rel table at all. `load` is the other shape: a table's columns whole, the edges between them whole, one file written once. ```python +import zudb + zudb.load( "social.zu1", nodes="person", diff --git a/tests/test_readme.py b/tests/test_readme.py new file mode 100644 index 0000000..bc8c515 --- /dev/null +++ b/tests/test_readme.py @@ -0,0 +1,86 @@ +"""The README against a Python that runs it. + +A quickstart is the most read and least executed code a client has. It +is copied by hand out of a page, and it goes wrong quietly, a rename or +a renamed keyword at a time, until somebody's first five minutes are +spent on a traceback. So the blocks that are whole programs are run +here, as printed, character for character. + +A block is a whole program when it starts with `import zudb`, which is +the rule the README follows: a block that stands on its own carries its +import, and a block that shows one call in the middle of a session does +not. Each program runs in an interpreter of its own with a temporary +directory as its working directory, because the file it writes is the +one a reader would find beside them afterwards. +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import pytest + +README = Path(__file__).resolve().parent.parent / "README.md" + + +def blocks(language: str) -> list[str]: + """Every fenced block of one language, in the order they appear.""" + found: list[str] = [] + current: list[str] | None = None + for line in README.read_text(encoding="utf-8").splitlines(): + if current is None: + if line.rstrip() == f"```{language}": + current = [] + elif line.rstrip() == "```": + found.append("\n".join(current) + "\n") + current = None + else: + current.append(line) + assert current is None, "a fenced block the README never closes" + return found + + +def programs() -> list[str]: + """The blocks that are whole programs.""" + return [block for block in blocks("python") if block.startswith("import zudb")] + + +def run(program: str, where: Path) -> subprocess.CompletedProcess[str]: + """A program, in an interpreter of its own, in `where`.""" + return subprocess.run( + [sys.executable, "-c", program], + cwd=where, + capture_output=True, + text=True, + timeout=120, + ) + + +def test_the_readme_prints_programs_and_not_fragments() -> None: + """The rule above holds: the page has both kinds and knows which.""" + assert len(programs()) == 2, "the README's whole programs" + assert len(blocks("python")) > len(programs()), "and its fragments" + + +def test_the_sixty_second_snippet_runs_as_printed(tmp_path: Path) -> None: + """The first block: connect, write two rows, read them as a frame.""" + snippet = programs()[0] + assert "zudb.connect" in snippet and "to_pandas" in snippet + done = run(snippet, tmp_path) + assert done.returncode == 0, done.stderr + # The frame pandas prints, whatever pandas decides to pad it with. + printed = done.stdout.split() + assert printed[:2] == ["name", "uid"] + assert "ada" in printed and "grace" in printed + # A reader runs it in the directory they are standing in, and the + # database is there when it finishes. + assert (tmp_path / "social.zu1").is_file() + + +@pytest.mark.parametrize("index", range(len(programs()))) +def test_every_whole_program_in_the_readme_runs(index: int, tmp_path: Path) -> None: + """Including the ones no other test looks at the output of.""" + done = run(programs()[index], tmp_path) + assert done.returncode == 0, done.stderr