Conversation
|
@ariostas this one is ready to go now. |
An installed uproot without the graphed reader (any release before scikit-hep/uproot5#1720) raised AttributeError instead of skipping. Assisted-by: ClaudeCode:claude-fable-5-1
An installed uproot without the graphed reader (any release before scikit-hep/uproot5#1720) raised AttributeError instead of skipping. Assisted-by: ClaudeCode:claude-fable-5-1
|
@ariostas took care of your comments - but it looks like whatever serves the root files in the tests is 403'ing hard. I'll try kicking the tests again tomorrow. |
|
@ariostas something's really wrong with the scikit-hep hosting all the CI jobs keep timing out. |
Hmm I'll have a look |
|
@lgray should work now. Thanks for letting me know! I restarted the CI |
|
@ariostas cool - all green here now except CUDA-13 as expected. Lemme know if you have any comments :-) |
* HIST-1: hist.graphed — deferred hist filling on graphed task graphs (freeze-HIST-1) src/hist/graphed mirrors src/hist/dask over graphed-histogram: Hist/NamedHist with QuickConstruct and named-axis fills recording into the graphed IR; compute() returns a real hist.Hist. Pinned: deferred == eager twins bit for bit over graphed-numpy, graphed-awkward (ragged fills flatten), and a real uproot TTree; weighted 2D NamedHist; multi-fill; ProcessExecutor plan == compute; the whole-dataset loader never runs. Full hist suite green alongside (200 passed). CI workflow 'graphed' runs the full suite + integration on the graphed-mvp branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: hist's test deps are a PEP 735 dependency group — install with --group test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * freeze-HIST-2: respin to graphed's evaluation idiom — no compute() USER-DIRECTED (graphed-histogram freeze-M23-1): plan() + an R7 executor aggregates; hist.Hist(value)/hist.NamedHist(value) wrap results back in-memory (names/labels survive via the spec's axis-__dict__ handling); the unused _in_memory_type property is gone. Same pins; full hist suite green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci: graphed-histogram repo renamed to graphed-histogram-mvp Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * M32: import SequentialRunner from graphed_core.execution (was graphed.write) The reference runner moved to the execution contract; graphed.write no longer exposes it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(deps): consume the consolidated graphed package The 8 graphed-*-mvp prototype packages are now one pip-installable distribution `graphed`. Rewrite the merged import roots in the graphed integration test (graphed_core/graphed_numpy/graphed_awkward -> graphed.core/.numpy/.awkward); hist.graphed's own graphed_histogram / graphed_exec_local deps stay separate. CI graphed.yml installs the consolidated `graphed[awkward,numpy]` (one git URL in place of the CORE/DEBUG/FRONTEND/NUMPY/AWKWARD siblings) plus graphed-exec-local, graphed-histogram, and the uproot fork. Assisted-by: ClaudeCode:claude-opus-4.8 * test: migrate ProcessExecutor -> ProcessPoolExecutor (exec-local deprecation) graphed-exec-local deprecated ProcessExecutor on 2026-06-17; the warning fails under this suite's `filterwarnings = error`. Use the non-deprecated parent (same behaviour) in the process-executor integration test. Assisted-by: ClaudeCode:claude-opus-4.8 * chore: track graphed-exec-local -> graphed-executors + histogram repo rename CI now installs graphed-executors @ .../graphed-executors and graphed-histogram @ .../graphed-histogram; the executor importorskip targets graphed_executors.local. Assisted-by: ClaudeCode:claude-opus-4.8 * ci: install graphed-executors + graphed-histogram from PyPI (now released) Both are published on PyPI, so switch their CI env vars from git+@main to the released dists. graphed stays git+@main (active development); the uproot/hist forks stay on their branches. Assisted-by: ClaudeCode:claude-opus-4.8 * use graphed from pypi as well * chore(graphed): let mypy ignore missing graphed_histogram stubs Upstream hist's `nox -s mypy` (the "Type check" CI job, absent from the older base this fork was cut from) type-checks all of `src` — including `src/hist/graphed/` — in an env that installs only hist's own `test`/`plot` groups, not `graphed_histogram`. So `import graphed_histogram.boost` raised `import-not-found` (4 errors, 2 files). Add `graphed_histogram.*` to the existing untyped-external `ignore_missing_imports` override, alongside scipy/matplotlib/mplhep/etc. Correct whether or not the package is installed (an ignore-missing-imports override is a no-op when the module is found), so it needs no per-line `# type: ignore` that strict mode would flag unused where the dep is present. Verified: `nox -s mypy` -> "Success: no issues found in 30 source files". Assisted-by: ClaudeCode:claude-opus-4.8 * fix(graphed): pass variation_axis/unweighted through Hist.fill `hist.graphed.Hist`/`NamedHist` inherit `BaseHist.fill`, which resolves EVERY keyword through `_name_to_index` before delegating to `graphed_histogram.boost.Histogram.fill`. So graphed's two fill-MODE flags — `variation_axis=` (m52 axis mode) and `unweighted=` — were read as axis names and died: ValueError: The axis name variation_axis could not be found Add one `FillModeMixin`, mixed into both graphed hist classes ahead of the hist bases (the single point both ghb.Histogram subclasses route through). With neither flag set it delegates to `super().fill(...)` verbatim (sibling/default mode byte-identical to before); with one set it resolves the axis order itself via `_name_to_index` and calls `ghb.Histogram.fill` directly. Tests: `fill(..., variation_axis=True)` through hist.graphed now equals the low-level positional fill (fails on origin/graphed-mvp with the exact ValueError); a 2-D out-of-order kwarg fill matches the positional fill and NOT a swapped one (axis order); a no-flag missing-axis fill still raises hist's message (the flag path engages only for flags); and `unweighted=True` with a weight= factor is forwarded to graphed's refusal. All three properties verified discriminating by mutation. Assisted-by: ClaudeCode:claude-opus-5 * ci(graphed): install the graphed stack from git main, not PyPI The `graphed` integration workflow pinned graphed / graphed-executors / graphed-histogram from PyPI ("now released"), but the hist.graphed tests use m52 features that land on the repos' main branches ahead of any PyPI release: `graphed.vary`/`graphed.points` and the `variation_axis`/`unweighted` histogram fill modes. Post-merge, the released PyPI graphed had neither, so the graphed-mvp push CI failed: AttributeError: module 'graphed' has no attribute 'vary' TypeError: Histogram.fill() got an unexpected keyword argument 'unweighted' Point the three co-developed packages at git main (uproot already installs from git), matching what the local editable dev stack tests against. Repin to PyPI once a release carries m52. Assisted-by: ClaudeCode:claude-opus-4.8 * chore: drop the graphed-project machinery from the upstream PR The graphed.yml workflow and the .graphed/ tracking directory belong to the graphed-project development pipeline, not to hist; upstream CI covers the hist.graphed hooks once graphed is a test dependency. Assisted-by: ClaudeCode:claude-opus-5[1m] * ci: graphed is a test dependency on Python 3.11+ tests/test_graphed.py is guarded by importorskip, so without this the hist.graphed hooks are never exercised in CI. graphed needs Python 3.11+, so the marker keeps the 3.10 jobs (checks, mypy, minimums) unchanged; the floors keep the lowest-direct "Check minimums" resolution off the 0.0.1 releases. Assisted-by: ClaudeCode:claude-opus-5[1m] * style: ruff Satisfy the repo's ruff-check/ruff-format hooks on tests/test_graphed.py: unused PartitionedSource.read_partition arguments become _columns/_resources, two compound asserts are split, imports sorted, dead noqa dropped, reformatted. Assisted-by: ClaudeCode:claude-opus-5[1m] * docs: say what the graphed hooks do without the project's labels The hist.graphed docstring and the test-module header carried graphed-project work-item and freeze-tag labels an upstream reader cannot resolve; they now state the same behaviour in plain words. Assisted-by: ClaudeCode:claude-opus-5 * test: skip the uproot end-to-end fill when uproot lacks uproot.graphed An installed uproot without the graphed reader (any release before scikit-hep/uproot5#1720) raised AttributeError instead of skipping. Assisted-by: ClaudeCode:claude-fable-5-1 --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Mirrors uproot.dask / dask_write for the graphed task-graph system:
- src/uproot/_graphed.py: uproot.graphed(files, library="ak") returns a deferred graphed Array
(graphed-awkward backend). Metadata-only construction (TTree form via typetracer; no event data
read); compute() reads lazily via uproot, and graphed's necessary-buffer projection means only the
TBranches the analysis touches are read (the dask-awkward column-projection analogue). Plus
necessary_columns() and compute(). graphed imported lazily so importing uproot needs no graphed.
- src/uproot/writing/_graphed_write.py: graphed_write(array, dest, tree_name=...) — compute (with
projection) + write to a TTree, the dask_write analogue.
- surfaced as uproot.graphed / uproot.graphed_compute / uproot.necessary_columns / uproot.graphed_write.
- tests/test_graphed_{for_awkward,column_projection,write}.py: same structure as uproot's dask tests
(skhep_testdata + importorskip), asserting graphed reads match plain uproot bit-for-bit, projection
reads only needed branches (over-touch protection), write round-trips, library='np' -> NotImplemented.
- .github/workflows/graphed.yml: runs the graphed tests on the graphed-mvp branch (installs the
graphed-*-mvp siblings); does not collide with uproot's build-test matrix.
- gated via the graphed orchestrator (scripts/graphed_advance.py + .graphed/), milestone UPROOT.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…a python -m pytest Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ressions) Install the full uproot dev dependency-group + the graphed-*-mvp siblings and run all of tests/ (uproot + graphed) with upstream's rerun flags, instead of only the graphed test files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…er in this job) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…otd') Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ors, not a .compute() shim Drop the dask-mimicking compute()/graphed_compute convenience. A uproot-read graphed analysis is now executed the way a task graph actually runs: per-partition through graphed_exec_local's ProcessExecutor (and ThreadExecutor) with tree reduction — the path a deferred-array .compute() hides. - _graphed.py: remove compute(); add graphed_partitions(files, steps_per_file) -> [Task(Partition...)]. - writing/_graphed_write.py: inline the projection+materialize it needs (no uproot.compute dependency). - __init__: surface uproot.graphed_partitions; drop uproot.graphed_compute. - tests/graphed_uproot_analysis.py: picklable process/combine/empty reading a Partition via open_once, recording + materializing the analysis on the chunk (mirrors the M7 adl.py executor glue). - tests/test_graphed_executor.py: run via ProcessExecutor + ThreadExecutor; bit-for-bit vs single-pass; invariant to partition count; non-vacuous; n_combines. - trim the read/projection tests to construction-time (metadata-only) + projection-minimality checks. - graphed.yml: also install graphed-exec-local-mvp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… on it) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…xecutor (mirror dask_write)
graphed_write now builds a write task graph instead of materializing one array:
- each output partition is a side-effecting write task (reads its chunk via open_once, writes
{prefix-}part{N}.root, returns nothing) -> a graphed_core.Plan.
- compute=False returns the Plan (the write tasks), writing nothing; compute=True runs it through a
graphed-exec-local executor (ProcessExecutor default, executor="thread" for the pool).
- destination is a directory of part files; steps_per_file splits each TTree; prefix names the files.
Also fixes uproot.graphed for RNTuple (HasFields): use filter_field (not filter_branch) for keys and
to_akform for the form -- surfaced because uproot writes RNTuples for `file[name] = dict`.
Tests (mirror the dask_write cases): one file per partition + contents, prefix, compute=False returns
the task graph and writes nothing (then runs), compute=True via process AND thread executors,
multi-file topology, Zmumu roundtrip.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… steps) graphed_partitions now takes step_size / steps_per_file / open_files with uproot.dask's rules: - step_size is incompatible with open_files=False and mutually exclusive with steps_per_file (TypeError); - open_files=True opens each file for exact entry ranges (step_size in entries or a memory string); - open_files=False is BLIND: files are not opened here; each chunk records (step_index, n_steps) as entry_start / -entry_stop and its real range is resolved against the file's own count at read time. New shared reader uproot.read_graphed_partition(partition, columns, tree=...) resolves blind partitions; the executor helper and graphed_write's write task both use it. tests/test_graphed_blind_steps.py mirrors test_0876: the TypeError rules + every step_size/steps_per_file/ open_files combo, blind vs eager, all reduce to the single-pass histogram bit-for-bit (38 graphed tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n_resumable Wire uproot partitions into graphed's own error-harvesting machinery (graphed semantics, not a copy of dask's report tuple): a DurablePlan runs each Partition through a uproot read+histogram; a missing/broken file raises and run_resumable harvests it into the content-addressed dead-letter set (ResumeReport.dead / dead_letters) while the good partitions still reduce. - tests/graphed_uproot_report.py: module-level process/hist_add/hist_zero + build_plan (DurablePlan with OpSpec.from_ref, a small representative IR for content-addressed task_ids). - tests/test_graphed_report.py: bad file -> dead-lettered (reproducible task_id) + good partitions reduce; clean run has no dead letters; kill-then-resume skips completed (no double-count, bit-for-bit, less work); error budget stops the run. - graphed.yml: also install graphed-checkpoint-mvp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erved plan at alternate inputs - tests/test_graphed_preserve.py: build a graphed-preserve Bundle of a uproot-read analysis (embeds the events array + canonical IR). reproduce() recomputes the histogram bit-for-bit from references alone (incl. a clean reload via Bundle.open with no original file); inspect() renders without executing and a removed payload raises UnresolvedPayload; self-fingerprinting (same data -> same fingerprint, different data -> different). - Demonstrate the PRESERVED PLAN runs on alternate inputs: a DurablePlan's canonical IR is shared, so with_partitions re-targets the same analysis at a different file location and at a different number of partitions (compile once, run on N datasets) -- both reproduce the single-pass result. - graphed.yml: also install graphed-preserve-mvp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ass blind partitions, write hardening
Applies the graphed M10 remediations (superproject mvp-shortcomings.md) to the
integration branch (freeze-UPROOT-1; see .graphed/UPROOT/attempts.md):
- A.2: graphed_uproot_analysis.process no longer builds a Session and
re-records the analysis per partition — the analysis is compiled once per
worker (graphed.compile_ir, module-cached) and each partition evaluates the
reduced serialized IR (graphed.evaluate_ir). Bit-for-bit equality with the
single pass re-pinned.
- A.3: uproot.necessary_buffers + uproot.resolve_read_branches — a count-only
analysis reports {branch: OFFSETS} (where necessary_columns reports the
empty set) and is served from the jagged branch's COUNTER branch (NMuon for
Muon_Px) without reading the payload baskets.
- C.9: graphed_partitions(open_files=False) emits first-class
graphed_core.Partition.blind chunks; read_graphed_partition resolves them
(legacy negative-entry_stop sentinel still honored for pre-M10 plans). The
one frozen-era test pinning the sentinel ENCODING amended under
freeze-UPROOT-1 (same files-not-opened intent, honest representation).
- C.9: graphed_write uses the public Session.sources() accessor, rejects
multi-source arrays loudly, writes only the array's projected branches, and
ships an O(#files) base-index table instead of pickling the whole
per-partition path map into every task (part{N} naming unchanged).
New suite tests/test_graphed_m10.py (11 tests). Local: all 70 graphed tests
green; full uproot suite (xrootd-deselected) 644 passed, 1 pre-existing
environment-only dask failure also present on the base commit.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- graphed_to_parquet: ROOT in, parquet out, partition by partition — blind partitions, compiled-IR evaluation per partition, specialized on the new graphed.write base, ProcessExecutor default, disabled==enabled write consistency, multi-source rejected. The read list is the graph's SYNTACTIC source-field accesses (the buffer projection under-supplies evaluation: a zip's pz/E legs are replayed even when only .pt is consumed). - uproot.graphed(behavior=...): awkward behaviors (vector Momentum4D) record, evaluate, and project to exactly the branches a property reads; process workers accept an importable module:attr behavior reference (behavior dicts contain lambdas and do not pickle). - uproot.graphed_head: first file's leading entries only, projected branches only, through the compiled IR — witnessed by corrupting every later file. - Fusion witness: per-event (axis=1) reductions live INSIDE stages (graphed M16), with the frozen M4 SingleUse diamond pin intact; maximal fusion collapses to one stage. - Inherited-surface pins over uproot sources: record-subset getitem, axis-0 slices, the ufunc tier, structure ops. - graphed_write deliberately stays OFF the write base: its None-returning tasks and empty-range skipping are frozen pins predating it (documented; alignment = a recorded freeze bump). 19 new tests (3 files); the existing frozen set untouched; 78/78 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@ariostas a gentle ping on this one when you have time. |
There was a problem hiding this comment.
I did some skimming, but since it's a big PR, my review was mostly AI-driven. I left some comments below. This was with Fable. I'll see if a GPT model has any extra recommendations.
🤖 AI text below 🤖
Thanks for this — the projection story and the form-mapping seam are nicely done, and the suite is green and lint-clean locally. Since the goal is for this to eventually stand in for uproot.dask, I checked it against uproot.dask / dask_write behaviour and found a few correctness problems and parity gaps (inline). Three overall requests:
- Mark it experimental. The top-level API depends on a 0.0.x package whose required release predates this PR update by hours. Please say so in the
uproot.graphed/uproot.graphed_writedocstrings (API may change without a deprecation cycle), and keep the public surface to those two names (+ maybegraphed_head) until it settles. - Collapse the two read paths into one by defaulting to
TrivialFormMapping, asuproot.daskdoes. I tried it: 128/135 tests still pass, and the 7 that don't either assert unmapped-only reporting or depend on the bare-expression write fallback, which is a bug. - Trim the tests. On the ubuntu 3.11 CI job the
test_1720_*block is 42 s of a 160 s pytest run (26 % of the time for 13 % of the tests); locally it is 23 s wall but 43 s CPU because of process pools, which is what a single-core runner pays. Almost half of that is four files that exercise onlygraphed_partitions/read_graphed_partition.
| # a record graph yields named fields (the derived columns); a bare (non-record) expression | ||
| # yields a fieldless array with no branch name to write it under — fall back to the source | ||
| # columns it reads | ||
| out_rec = evaluated if evaluated.fields else chunk |
There was a problem hiding this comment.
🤖 AI text below 🤖
Bug — a bare expression writes the wrong data. graphed_write(ev.px1 * 2, dest) succeeds and writes unscaled px1: for a fieldless result this falls back to the raw input chunk. Through a form_mapping the same call crashes with AttributeError: 'PlaceholderArray' object has no attribute 'reshape', because the chunk holds placeholders for unread branches. Please raise a TypeError asking for a record (or write under an explicit default branch name) instead of substituting the inputs. test_write_of_a_projected_array_writes_only_its_branches currently pins the wrong behaviour (g.x + g.y → branches x, y).
There was a problem hiding this comment.
🤖 AI text below 🤖
Fixed (W1). A fieldless result is refused with TypeError before any file is opened, telling the user to wrap it (gak.zip({"x2": g.x * 2})); nothing substitutes the inputs any more. Witness: test_derived_record_is_written_and_a_fieldless_expression_is_refused. The test that pinned the fallback is gone with the old suite.
| with uproot.recreate( | ||
| path, **_recreate_kwargs(compression, compression_level) | ||
| ) as out: | ||
| out[tree_name] = record |
There was a problem hiding this comment.
🤖 AI text below 🤖
Output type differs from dask_write. Dict assignment writes an RNTuple here, while dask_write calls mktree and writes a TTree — under a parameter called tree_name. For a drop-in replacement I'd default to mktree and make RNTuple an explicit opt-in.
There was a problem hiding this comment.
🤖 AI text below 🤖
Fixed (W2). Each partition is written by _dask_write.ak_to_root, so the writer is mktree and the output is a TTree under tree_name, as dask_write does (test_writes_one_ttree_file_per_partition). I did not add an RNTuple opt-in: dask_write has none either, so there is no parity to keep and nobody has asked for it yet.
| if isinstance(compression, str) | ||
| else compression | ||
| ) | ||
| return {"compression": resolved} |
There was a problem hiding this comment.
🤖 AI text below 🤖
compression="ZLIB" (accepted by dask_write) and unknown names both raise a bare KeyError. ak_to_root in _dask_write.py already has this mapping with a proper error message — could the two share one helper?
There was a problem hiding this comment.
🤖 AI text below 🤖
Fixed (W3). Compression goes through the same ak_to_root lookup: "ZLIB" is accepted and an unknown name gets its error message (test_compression_names_match_dask_write).
| "process": graphed_executors.local.ProcessPoolExecutor, | ||
| "thread": graphed_executors.local.ThreadExecutor, | ||
| }[executor] | ||
| return executor # an executor class passed directly (instantiated by the caller) |
There was a problem hiding this comment.
🤖 AI text below 🤖
executor="threads" is a bare KeyError, and an executor instance fails with 'ThreadExecutor' object is not callable. A ValueError listing the accepted strings, and accepting an instance (use it as-is when it has .run), would be friendlier.
There was a problem hiding this comment.
🤖 AI text below 🤖
Fixed (W4). executor= takes "process", "thread" or "sequential" (unknown string → ValueError listing them), an executor instance used as-is, or an executor class instantiated with max_workers (test_executor_is_a_name_an_instance_or_a_class).
| max_workers=None, | ||
| compression="zlib", | ||
| compression_level=1, | ||
| ): |
There was a problem hiding this comment.
🤖 AI text below 🤖
Missing relative to dask_write: storage_options / remote destinations (os.makedirs makes this local-only, dask_write goes through url_to_fs), title, field_name, counter_name, initial_basket_capacity, resize_factor. Fine to defer, but then please list them as not-yet-supported in the docstring.
There was a problem hiding this comment.
🤖 AI text below 🤖
Fixed (W5), not deferred. graphed_write now takes title, field_name, counter_name, initial_basket_capacity, resize_factor, compression_level and storage_options, all forwarded to ak_to_root; the destination is created and addressed through fsspec.core.url_to_fs, so remote destinations work and return full URLs while local ones return plain paths (test_tree_options_reach_mktree, test_remote_destinations_go_through_fsspec).
| test = [ | ||
| "aiohttp", | ||
| "deflate", | ||
| "graphed[awkward,parquet]>=0.0.3; python_version >= '3.11'", |
There was a problem hiding this comment.
🤖 AI text below 🤖
Putting these in the main test group runs the graphed suite on every 3.11+ job. Given the cost below, would a separate test-graphed group with one dedicated CI job (like test-xrootd / test-pyodide) work for you while this is experimental?
There was a problem hiding this comment.
🤖 AI text below 🤖
Done (P1). The graphed test dependencies live in a test-graphed group (graphed[awkward,parquet]>=0.0.4, graphed-executors>=0.0.3, vector, all python_version >= '3.11'), and a single graphed-build job installs it. That job is red on this head only because the 0.0.4 floor is not on PyPI yet; graphed 0.0.4 (graphed-org/graphed#39) is being released now, after which the job gets re-run.
|
|
||
| @pytest.mark.parametrize("step_size", ["50 kB", uproot._util.unset]) | ||
| @pytest.mark.parametrize("steps_per_file", [1, 2, 5, 13, uproot._util.unset]) | ||
| @pytest.mark.parametrize("open_files", [False, True]) |
There was a problem hiding this comment.
🤖 AI text below 🤖
This is the most expensive file (9 s on CI): 20 parametrisations, the valid ones each spawning a 4-worker process pool, to cover the ~20-line range arithmetic in graphed_partitions. Asserting on the returned entry ranges directly (no executor) covers the same uproot code in milliseconds; one executor run is enough as an integration check. More generally, max_workers=4 process pools on a single-core runner mostly measure worker start-up — executor="thread" or a sequential runner would do for everything except one process-pool smoke test.
There was a problem hiding this comment.
🤖 AI text below 🤖
Done (T1). The range arithmetic is asserted directly on the returned partitions, no executor (test_steps_per_file_splits_into_contiguous_ranges, test_step_size_caps_each_chunk). Everything else runs on the thread or sequential executor; one process-pool smoke test remains. The five files run in ~5 s wall locally.
| @@ -0,0 +1,174 @@ | |||
| # BSD 3-Clause License; see https://github.com/scikit-hep/uproot5/blob/main/LICENSE | |||
There was a problem hiding this comment.
🤖 AI text below 🤖
In-process, this file, test_1720_graphed_report.py and test_1720_graphed_executor.py execute only graphed_partitions, read_graphed_partition and _partition_range from uproot; the assertions are about graphed's Bundle / DurablePlan / dead-letter / executor machinery. Those belong in graphed's own test suite — here they tie uproot CI to a 0.0.x package's behaviour. I'd drop the three files (and tests/graphed/report.py).
There was a problem hiding this comment.
🤖 AI text below 🤖
Done (T2). test_1720_graphed_preserve.py, test_1720_graphed_report.py, test_1720_graphed_executor.py and tests/graphed/report.py are deleted; graphed's own machinery is tested in graphed.
| g = uproot.graphed(src_path + ":events", library="ak") | ||
| outdir = os.path.join(tmp_path, "out") | ||
| paths = uproot.graphed_write( | ||
| g.x + g.y, outdir, steps_per_file=2, tree_name="events" |
There was a problem hiding this comment.
🤖 AI text below 🤖
This pins the fallback bug noted in _graphed_write.py: writing g.x + g.y is expected to produce branches x and y rather than the sum.
There was a problem hiding this comment.
🤖 AI text below 🤖
Done (T3). This test is gone; the behaviour it pinned is now a TypeError (see the _graphed_write.py thread).
| @@ -0,0 +1 @@ | |||
| # BSD 3-Clause License; see https://github.com/scikit-hep/uproot5/blob/main/LICENSE | |||
There was a problem hiding this comment.
🤖 AI text below 🤖
As you noted, this package shadows the real graphed when pytest runs from tests/. Renaming it (e.g. tests/graphed_helpers) removes the hazard for free. Also, 20 test files is a lot for one feature — after the cuts above, consolidating into a handful (read, form_mapping, write, projection) would make this much easier to maintain.
There was a problem hiding this comment.
🤖 AI text below 🤖
Done (T4). The package is tests/graphed_helpers and the twenty files are consolidated into test_1720_graphed_read.py, _form_mapping.py, _projection.py, _write.py and _partitions.py (54 tests).
ariostas
left a comment
There was a problem hiding this comment.
🤖 AI text below 🤖
A GPT second pass found these additional, non-duplicate issues:
-
P1: nested RNTuples get the wrong schema (
src/uproot/_graphed.py:316-341).HasFields.keys(full_paths=False)flattensrec.xtox, then_get_ttree_formadvertisesxas a top-level field even thoughRNTuple.arrays(["x"])returns it underrec. In a local{rec: {x, y}, z}RNTuple,g.xrecords but fails at execution, andgraphed_head(g.rec.x, 3)also fails. Please useto_akform(filter_name=common_keys)as_dask.py:1767-1769does. -
P1: repeated inputs overwrite output parts (
src/uproot/writing/_graphed_write.py:181-199).file_basesis a dict keyed only by(uri, tree), sographed([spec, spec])with two steps reportspart-00002.root,part-00003.roottwice and writes only one copy of the rows. The occurrence needs to be part of the partition identity/index, or duplicate inputs need to be rejected. -
P1: writes silently change axis-0 semantics with partition count (
src/uproot/writing/_graphed_write.py:80-82). The complete expression is replayed independently on each chunk. Forg.x[2:8]overrange(10), one step writes[2,3,4,5,6,7], two steps write[2,3,4,7,8,9], and three write[2,5,8,9];dask_writepreserves dataset-global slicing. Either global operations need partition-aware handling or this path must reject/document non-row-local graphs. -
P1:
graphed_headappliesnbefore the expression (src/uproot/_graphed.py:665-670). It reads the firstnsource rows and only then filters/slices. Thusgraphed_head(g.x[10:20], 5)is empty instead of[10,11,12,13,14], andhead(g[g.x % 2 == 0], 2)returns one row instead of two. A dask-like head must collect the firstnoutput rows. -
P1: mapped projection can under-supply compiled evaluation (
src/uproot/_graphed.py:632-638). The comment above_evaluation_columnscorrectly notes that compiled IR replays dead intermediate nodes, but mapped sources switch back to output-buffer projection. Withzip({"jet": g.Jet.pt, "muon": g.Muon.pt})[["jet"]], projection omitsMuon_ptwhile IR still evaluates that node; bothgraphed_headandgraphed_writefail. Mapped reads need the same syntactic-evaluation safeguard translated through the mapping. -
P2: the default process writer cannot ship common behaviors (
src/uproot/writing/_graphed_write.py:185-195). The partial serializessession.backend;vector.backends.awkward.behaviorcontains lambdas, so even the returnedcompute=Falseplan raisesPicklingErrorunderpickle.dumps. The generic Parquet writer already solves this with an importable behavior/backend reference; ROOT writing needs the same seam or should avoid serializing the backend instance. -
P2: external evaluators are dropped by
graphed_headandgraphed_write(src/uproot/_graphed.py:663-670,src/uproot/writing/_graphed_write.py:82). Agraphed.apply(...)expression materializes and runs throughaggregate_plan, but these helpers callevaluate_irwithout the session evaluators and fail with “External payload ... needs an evaluator.”aggregate_planalready wires these through_external_evaluators. -
P2: already-open TTrees are not partition-compatible (
src/uproot/_graphed.py:82-100). They are accepted by the documented dask-compatiblefilesAPI and whole-dataset materialization works, but theModel_TTreeis stored asPartition.uri;resources.open_oncethen raises because it is unhashable. Normalize it to a reusable file/tree reference or special-case open objects inopen_tree.
The existing Graphed-focused suite still passes (135 passed); these are missing edge/parity cases rather than current test failures.
|
@ariostas thanks for spending the tokens - I will do so similarly once my limits reset :-) |
…h uproot.graphed `uproot.dask` resolved files to (TTree, keys), built the base form and read a chunk inline in each of its two array paths and in `UprootReadMixin.read_tree`. Lift those into `_resolve_trees_and_keys`, `_normalize_grouped_keys`, `_base_form_of`, `_mapping_of` and a module-level `_read_tree` so a second front end reads a chunk exactly as dask does (form-mapping buffer replacements, AsGrouped keys, TBranch object paths, RNTuple forms via `to_akform`). `regularize_object_path` now delegates to `object_in_directory`, so a caller that already holds a directory (a worker re-opening a partition's file) looks the object up the same way, including the single-TTree default and `allow_missing`.
…ver output rows Review rounds 2 and 3 on this PR found `uproot.graphed` re-implementing the dask resolution and read path with its own gaps (G3 steps in file specs, G4 TBranch paths and AsGrouped, G5 None object path, G6 allow_missing on the worker path, G7/G8 a second read path, X1 nested RNTuple schema, X8 open TTree partitions). All of them go through the shared `_dask.py` helpers now: one `_GraphedTTreeSource` carries the resolved (file, object path) pairs, opens a partition's tree through `object_in_directory`, reads with `_read_tree` under `TrivialFormMapping` by default, and answers `projected_columns` through the mapping's buffer keys. Surface (R2.1, G1, G2, G9-G12, I1, E1): `uproot.graphed`, `graphed_head` and `graphed_write` are the exported names; dask-only keyword arguments are rejected by name instead of being swallowed by `uproot.open`; `decompression_executor`/`interpretation_executor` are explicit parameters; `necessary_columns` and `graphed_partitions` stay module-level for `aggregate_plan(partitions=...)`; `extras.graphed()` enforces the 0.0.4 floor. `graphed_head` (X4) collects the first n rows of the OUTPUT by growing a prefix read until the evaluated result has n rows or the tree is exhausted, so a filtered or sliced analysis no longer returns fewer rows than asked.
…'s wiring `graphed_write` (W1-W5, X2, X6, X7) now writes each partition with `_dask_write.ak_to_root`, so it takes the same tree_name/title/field_name/ counter_name/basket/compression/storage_options keywords as `dask_write`, writes a TTree via `mktree`, and surfaces compression names through the same lookup. The worker receives the backend as a class or import reference (the way `aggregate_plan` does), carries the session's External evaluators, and a fieldless array or a graph that reduces the partitioned axis is refused before any file is opened. Remote destinations are created and addressed through fsspec (`url_to_fs`); local destinations return plain paths.
…oup and CI job Replaces the twenty `test_1720_graphed_*.py` files (R2.3, T1-T4) with read/form_mapping/projection/write/partitions suites that run on the thread and sequential executors, plus one process-pool smoke with a backend import reference. The helper package moves to `tests/graphed_helpers` so pytest does not shadow the `graphed` distribution. graphed's own machinery (preserve, report, executor internals) is no longer tested here. The graphed test dependencies move to a `test-graphed` group (P1) with the 0.0.4 floor, and a `graphed-build` CI job installs that group on one platform and runs the five suites.
|
🤖 AI text below 🤖 Pushed as four commits (dd14af0, 8565546, 5aac34e, bbb884b), one per class of finding. Three of the round-3 items are defects in graphed's own partition-wise drivers, so they are fixed there: graphed-org/graphed#38 (merged), and this PR's floor moves to Every review item has its own reply: the round-2 inline comments are answered in their threads, and the round-2 overall requests and the round-3 items are answered in the quoted comments below. Three items are refuted or narrowed with evidence rather than fixed: round-3 item 5 (mapped projection — the example is not a valid awkward expression), and parts of G1 (the two executors were never dropped) and G6 (construction-time Also run locally: uproot's offline suite and the dask tests against the shared helpers (all green). |
|
🤖 AI text below 🤖
Done. Experimental notes on the module and on |
|
🤖 AI text below 🤖
Done, and further: reads share |
|
🤖 AI text below 🤖
Done. 20 files → 5 (read / form_mapping / projection / write / partitions), 54 tests, ~5 s wall locally; thread and sequential executors plus one process-pool smoke; graphed-machinery tests dropped; the graphed dependencies moved to a |
|
🤖 AI text below 🤖
Fixed as suggested: the base form comes from One thing this does not fix, because it is not this PR's: reading a nested RNTuple field still fails, identically on |
|
🤖 AI text below 🤖
Rejected explicitly. |
|
🤖 AI text below 🤖
Rejected explicitly, at the cause. A partition-wise driver evaluates the compiled IR once per chunk, so any reduction of the partitioned axis ( |
|
🤖 AI text below 🤖
Fixed. |
|
🤖 AI text below 🤖
Not an under-supply. The earlier version of this reply said "not a valid awkward expression", which was imprecise — corrected here with measurements (same file, the The zip is pass-through for projection on both engines, and the read set is the accessed members plus the list structure the broadcast needs:
So The well-formed spelling, |
|
🤖 AI text below 🤖
Fixed with the same seam as the Parquet writer. The worker receives the backend as a class or a |
|
🤖 AI text below 🤖
Fixed. Both carry the session's evaluators, through the now-public |
|
🤖 AI text below 🤖
Fixed by normalising: an open |
Moving graphed to the test-graphed group took the graphed suites out of the coverage-collecting job, so src/uproot/_graphed.py and _graphed_write.py reported as uncovered (codecov/patch 33%). The graphed job now runs with --cov and uploads its report; Codecov merges it with the main upload. Assisted-by: ClaudeCode:claude-fable-5-1
…rite paths Adds the cases the graphed job alone left uncovered — a missing tree on the known_base_form loader path, an open TTree normalised under known_base_form, steps on some but not all files, a scalar head, a partition read that opens its own file, a missing tree in graphed_partitions, nested record branch names, and the graphed-executors install hint — so src/uproot/_graphed.py and writing/_graphed_write.py are fully covered by the graphed job's own upload. Assisted-by: ClaudeCode:claude-fable-5-1
… the shared resolver Covers the last lines the review-driven refactor moved without a witness: object_in_directory's no-TTree outcomes (refused, or skipped under allow_missing) and the full_paths AsGrouped key normalisation, checked against uproot.dask on the issue-1502 file. Assisted-by: ClaudeCode:claude-fable-5-1
|
@ariostas OK - bot responses done and everything is addressed. Re-review or let me know what to do otherwise! Thanks! |
Add hooks to uproot that allow it to be used with the delayed execution engine "graphed", it has stabilized enough to where it makes sense to support the code upstream and ease installation.
🤖 AI text below 🤖
Added 2026-09-19 —
uproot.graphedtakes the same form-mapping seamuproot.daskdoes. Three parameters, each mirroring itsuproot.dasknamesake so a schema library (coffea'sNanoEventsFactoryis the archetype) can drive either arm from one mapping:form_mapping=— theImplementsFormMappingobjectuproot.dasktakes. The recorded array has the MAPPED form, the mapping info'sbehavioris registered on the backend, and the source declares its read list inTBranchnames, so projection still fetches only the branches the recorded graph touches.known_base_form=— use this form instead of opening a file to learn it; no file is opened at record time, and theTTreename comes from the object path infiles.backend=— a graphed backend instance to record on, in place of the defaultgraphed.awkward.AwkwardBackend(the route for a caller's ownArraysubclass);behavior=is refused beside it.Along the way: every partition is opened through the source, so the caller's file options reach both read and write; an empty declared read list is honoured; an RNTuple's base form is built the way a TTree's is; and
uproot.extras.graphed()registersuprootas graphed-internal, so a recorded op's provenance (and a run-time error) points at the analyst's line rather than an Uproot frame. That last call needsgraphed>=0.0.3(on PyPI since 2026-09-19), so thetestgroup floor moves to 0.0.3 andextras.graphed()tells a user on an older graphed how to upgrade. Tests:tests/test_1720_graphed_form_mapping.py,…_mapped_projection.py,…_mapped_write.py,…_mapped_edge_cases.pyover a small NanoAOD-shaped helper intests/graphed/mini_nanoaod.py;src/uproot/_graphed.pystays at 100 % line+branch under the suite.The graphed hooks ride uproot's own CI:
graphed,graphed-executorsandgraphed-histogram(all on PyPI;graphedat 0.0.3, the other two at 0.0.2 or later) are entries in thetestdependency group behindpython_version >= "3.11"(graphed requires 3.11+), so thetests/test_1720_graphed_*.pysuite runs on the 3.11+ jobs and is skipped on 3.10. The optional imports go throughuproot.extras.graphed()/uproot.extras.graphed_executors(); the suite's non-test helpers live in thetests/graphedpackage and are imported astests.graphed.<name>. The three packages are opted out of[tool.uv] exclude-newerthe way awkward already is, because the 7-day window would otherwise hide fresh releases of the three; those keys can go once the window has rolled past.The pre-commit fixes are committed by hand because pre-commit.ci cannot push to a fork owned by an organization.
pyproject.tomlkeeps the layout pre-commit.ci's cachedtoml-sort0.24 produces; a fresh local hook env pulls toml-sort 0.25.0 (2026-09-12), which reformats inline tables and would fail the same hook on pre-commit.ci. The "GPU test with CUDA 13" failure is the self-hosted runner'scuda-bindingspre-release version parse intests-cuda/, which this PR does not touch and whichmainhits too.