Skip to content

Commit 6a21f3e

Browse files
Add CLAUDE.md
Document the build/test commands and the architecture (shared _logutils message builder, per-backend accessor state, join wrapper, docx export). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent db0b250 commit 6a21f3e

1 file changed

Lines changed: 121 additions & 0 deletions

File tree

CLAUDE.md

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## What this is
6+
7+
`raffalib` is a Python (≥3.13) helper library for data wrangling. Its headline
8+
feature is **STATA-like change logging** layered onto pandas and polars via a
9+
`.raffa` accessor/namespace, plus `.docx` export. It also bundles small,
10+
mostly-independent utilities (logging setup, pickling, progress bars, Selenium,
11+
SQLAlchemy view helpers, bibliometrics/Scopus/OpenAlex helpers).
12+
13+
Packaged with the `uv_build` backend in a **src layout** (`src/raffalib/`).
14+
15+
## Commands
16+
17+
All workflows go through `uv`:
18+
19+
```console
20+
uv sync --extra dev # install dev deps (ruff + pytest)
21+
uv run pytest # run the test suite
22+
uv run pytest tests/test_polars.py # one file
23+
uv run pytest tests/test_polars.py::test_endlog_removed_rows # one test
24+
uv run ruff check # lint
25+
uv run ruff format # format
26+
```
27+
28+
Doctests in the narrative docs are executable and run in CI (`.github/workflows/docs.yml`).
29+
Run them locally with the pandas + polars + docs extras:
30+
31+
```console
32+
uv run --extra docs --extra pandas --extra polars \
33+
sphinx-build -b doctest docs/source docs/_build/doctest
34+
```
35+
36+
CI runs in two workflows: `.github/workflows/tests.yml` (pytest, with the
37+
pandas/polars/db/web/bibliometrics extras so nothing is skipped) and
38+
`.github/workflows/docs.yml` (the doctests above).
39+
40+
## Architecture
41+
42+
### The two backends share one message builder — this is the central invariant
43+
44+
`src/raffalib/_logutils.py` is the **single source of truth for all
45+
human-readable log text** (row/column deltas, changed-cell counts, join
46+
provenance, elapsed time, the `JoinCounts` dataclass). `pandas.py` and
47+
`polars.py` only do backend-specific mechanics (state storage, cloning, value
48+
comparison) and then call into `_logutils` for the wording.
49+
50+
Consequence: the pandas and polars accessors are designed to emit **byte-identical
51+
log output**. When changing any log message, edit `_logutils.py` only — never
52+
hand-edit a string in one backend. Doing so silently breaks parity.
53+
54+
Tests and docs are tightly coupled to the exact wording: `tests/test_pandas.py`
55+
and `tests/test_polars.py` assert on literal substrings via `caplog`, and the
56+
`.rst` doctests in `docs/source/` reproduce exact output. Any message change
57+
requires updating those assertions and doctests together.
58+
59+
### How the `.raffa` accessors persist state across a pipeline
60+
61+
`startlog()` stashes the initial shape / optional clone / start time, then
62+
`endlog()` reads it back to compute the diff. The two backends store that state
63+
differently:
64+
65+
- **pandas** (`pandas.py`): registers via `@pd.api.extensions.register_*_accessor("raffa")`
66+
and appends `_initial_shape` / `_initial_df` / `_start_time` to the object's
67+
`_metadata` so they survive pandas operations. `endlog()` `del`s them when done.
68+
- **polars** (`polars.py`): polars frames have no attribute storage, so state
69+
lives in the `config_meta` namespace from the **`polars-config-meta`** package
70+
(imported for its import side effect). `endlog()` first checks the metadata
71+
exists and warns "You have to call startlog() before..." if not.
72+
73+
Both register on import: `import raffalib.pandas` / `import raffalib.polars` is
74+
what installs the `.raffa` accessor. Each module deletes any pre-existing
75+
`.raffa` accessor at import time to suppress pandas/polars override warnings.
76+
77+
`clone=True` in `startlog()` keeps a full copy so value-level changes can be
78+
detected when the shape is unchanged; `clone=False` (default) only tracks shape
79+
and emits `_logutils.CLONE_FALSE_MSG`. Null-vs-null is treated as *unchanged* in
80+
both backends (pandas masks `isna() & isna()`; polars uses `ne_missing`).
81+
82+
### The logging `join` wrapper
83+
84+
`df.raffa.join(...)` wraps `pd.DataFrame.merge` / `pl.DataFrame.join`, adding
85+
hidden `source_left` / `source_right` row-index columns to reconstruct row
86+
provenance (left-only / right-only / both, duplicate keys, dropped rows), then
87+
logging it via `_logutils.join_log`. Filtering joins (`how="semi"`/`"anti"`) are
88+
detected and logged separately via `filtering_join_log`. pandas has no native
89+
semi/anti join so it is emulated with an inner merge; pandas `"full"` is
90+
translated to merge's `"outer"`. `keep_row_index=True` keeps the source-index
91+
columns in the output (polars uses the `polars-permute` `permute` namespace to
92+
move them to the end).
93+
94+
### `.docx` export
95+
96+
`export_docx.py` holds `DocxFile`, a python-docx wrapper for page setup,
97+
styled tables, and matplotlib/plotly figures. The `DataFrame.raffa.to_docx()`
98+
methods build the table cell-by-cell on top of it. Note `DocxFile.with_table()`
99+
introspects the signatures of `__init__` and `add_table` to **route each kwarg**
100+
to the right method, raising `TypeError` on unknown keys — this is why
101+
`to_docx(..., heading_text=..., table_style=...)` "just works" with mixed
102+
document- and table-level options.
103+
104+
### `logging.py`
105+
106+
`create_logger()` is the opinionated entry point users call to actually see the
107+
output (plain `StreamHandler` or `rich.RichHandler`). It re-enables the
108+
`raffalib.pandas` / `raffalib.polars` loggers explicitly after `dictConfig`.
109+
110+
## Conventions
111+
112+
- **Optional deps are real**: only `humanize`, `jsonpickle`, `natsort`,
113+
`python-docx`, `rich`, `tqdm` are core. pandas, polars, sqlalchemy, selenium,
114+
pyalex, etc. live behind extras (see `pyproject.toml`). Tests guard their
115+
imports with `pytest.importorskip(...)` so the suite passes without every
116+
extra installed — follow that pattern for any new optional-dep test.
117+
- Every source file carries the **GPL-3.0-or-later license header**. Keep it on
118+
new files.
119+
- Docstrings use **reStructuredText field lists** (`:param:` / `:type:` /
120+
`:return:` / `:rtype:`) and are rendered by `sphinx-autoapi`.
121+
- No `[tool.ruff]` config — ruff runs on defaults.

0 commit comments

Comments
 (0)