Skip to content

#560: cover custom in the parity oracle; drop the query_file monkeypatch - #602

Merged
jepegit merged 3 commits into
masterfrom
issue-560-parse-pec-custom
Jul 21, 2026
Merged

#560: cover custom in the parity oracle; drop the query_file monkeypatch#602
jepegit merged 3 commits into
masterfrom
issue-560-parse-pec-custom

Conversation

@jepegit

@jepegit jepegit commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Follow-up to #601. Part of #560. Tests only — no production code changes.

custom needed no port

Its configuration arrives from the user's YAML instrument file at runtime, and declarations_from_configuration reads a ModelParameters instance as happily as a module — so the two-stage entry points from #601 already carry it end to end. Verified before writing anything; the only change is adding it as a parity case so that stays true rather than accidental.

Four parity cases now (maccor model one, neware model one, maccor model two, custom), and the unported-post-processor exception list is still empty.

What it uncovered in the registry

The oracle used to wrap query_file so one cellpy.get() yielded both frames. For custom the capture came back empty — not a test bug. The registry imports loader modules under their bare name, so the class it instantiates is a different object from the one at the package path:

type(loader).__module__       # -> "custom"
m.DataLoader.__module__       # -> "cellpy.readers.instruments.custom"
type(loader) is m.DataLoader  # -> False

Two distinct classes from one source file. Consequences beyond this test: isinstance/issubclass against the package-path class fail for registry-created loaders, class-level state exists twice, and monkeypatching the package-path class silently does nothing — which is exactly how this surfaced.

Not fixed here, to keep the branch scoped. Worth its own issue.

The oracle got simpler

parse() and declarations() exist now, so the oracle drives the vendor stage through the public entry points instead of patching. That removes the import-order sensitivity and exercises what the switchover will actually use.

The "both frames from one run" property that wrapping provided is covered by tests/test_loader_two_stage.py::test_the_two_stages_agree_with_the_legacy_frame — that check is what stops this module from verifying a path nothing uses.

Verification

Full suite under PYTHONUTF8=1 (the Linux-proxy mode that caught the encoding bug in #601): 1189 passed, 0 failed.

Next on #560

pec_csv is the genuinely hard one left: it extends BaseLoader directly — no AutoLoader, no config_params, no normal_headers_renaming_dict — and matches headers dynamically through _HEADER_ALIASES. There is nothing to derive declarations from, so it needs either a hand-written declaration set or a refactor expressing its aliases as a configuration. A design choice rather than a mechanical port.

Then arbin_res (ODBC, so CI can only check it against the committed fixture), tier 2, and the switchover.

🤖 Generated with Claude Code

…patch

`custom` needs no port. Its configuration arrives from the user's YAML
instrument file at runtime, and `declarations_from_configuration` reads a
ModelParameters instance as happily as a module - so the two-stage entry points
added in the previous PR already carry it end to end. Adding it as a parity case
is what keeps that true rather than accidental.

Getting it there exposed something in the registry. The oracle used to wrap
`query_file` so one `cellpy.get()` yielded both frames; for `custom` the capture
came back empty. The cause is not the test: the registry imports loader modules
under their **bare** name, so the class it instantiates (`custom.DataLoader`) is
a different class object from `cellpy.readers.instruments.custom.DataLoader`.
Patching the latter silently did nothing.

    type(loader).__module__            -> "custom"
    m.DataLoader.__module__            -> "cellpy.readers.instruments.custom"
    type(loader) is m.DataLoader       -> False

That has consequences past this test - isinstance checks against the package-path
class fail for registry-created loaders, and any class-level state exists twice.
Filed separately rather than fixed here, to keep this branch scoped.

The oracle no longer patches anything: `parse()` and `declarations()` exist now,
so it drives the vendor stage through the public entry points. That removes the
import-order sensitivity as well, and exercises what the switchover will use.
The same-run property the wrapping gave up is covered by
test_loader_two_stage.py, which checks parse() agrees with what loader() parses.

Two wrong guesses on the way to that diagnosis (patch list too narrow; then
import order), both fixed by inspecting the class identity instead of reasoning
about it.

Full suite under PYTHONUTF8=1: 1189 passed, 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 20, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

Five of the eight shipped configuration-driven dialects have no vendor file in
the repository, so the parity oracle could not reach them. "The exception list
is empty" therefore said less than it sounded like: the covered dialects agree,
not every dialect.

The bugs this arc has actually found are all configuration-level - a wrong reset
granularity, duration strings cast to null, a column mapped to the wrong native
name, a unit template left unresolved. None of them needs real vendor data to
reproduce; they need a file spelled the way the configuration says. So each
fixture is generated from the configuration's own declarations: column names,
separator, decimal mark, skiprows, state keys.

Coverage goes from three of eight configuration-driven dialects to six:
maccor_txt_three, neware_txt_zero and neware_txt_two now have value parity
against the legacy path, all with zero mismatches. maccor_txt_three exercises
European decimal commas, which nothing did before.

Mutation-tested rather than assumed green: disabling the PER_STEP derivation
makes neware_txt_zero fail with a 0.5 mAh diff on both capacity columns.

What this does not do is exercise vendor quirks - the odd row, the interrupted
run, the stray 0xFF byte. Those still need real files. This closes the
systematic half of the gap, not the empirical half, and the module docstring
says so.

Two findings while building it, both filed separately rather than fixed here:

- Files with fewer than roughly 150 data rows fail to load with a bare
  IndexError when the delimiter is auto-detected (20 and 100 rows fail; 150 and
  300 load). A short vendor file is a normal thing - a truncated run, a quick
  export - and today it fails with an error naming neither the file nor the
  cause. It is also why these fixtures have to be 300 rows.
- maccor_txt_zero and batmo_bdf_bdf remain uncovered: the former has empty
  formatters and no post-processors and looks like a template rather than a
  loadable dialect; the latter needs its own generator shape.

Full suite under PYTHONUTF8=1: 1195 passed, 0 failed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jepegit
jepegit merged commit 79be0d5 into master Jul 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant