Skip to content

feat: add hooks for working with graphed-org/graphed - #1720

Open
lgray wants to merge 51 commits into
scikit-hep:mainfrom
graphed-org:graphed-mvp
Open

lgray wants to merge 51 commits into
scikit-hep:mainfrom
graphed-org:graphed-mvp

Conversation

@lgray

@lgray lgray commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

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.graphed takes the same form-mapping seam uproot.dask does. Three parameters, each mirroring its uproot.dask namesake so a schema library (coffea's NanoEventsFactory is the archetype) can drive either arm from one mapping:

  • form_mapping= — the ImplementsFormMapping object uproot.dask takes. The recorded array has the MAPPED form, the mapping info's behavior is registered on the backend, and the source declares its read list in TBranch names, 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 the TTree name comes from the object path in files.
  • backend= — a graphed backend instance to record on, in place of the default graphed.awkward.AwkwardBackend (the route for a caller's own Array subclass); 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() registers uproot as 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 needs graphed>=0.0.3 (on PyPI since 2026-09-19), so the test group floor moves to 0.0.3 and extras.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.py over a small NanoAOD-shaped helper in tests/graphed/mini_nanoaod.py; src/uproot/_graphed.py stays at 100 % line+branch under the suite.

The graphed hooks ride uproot's own CI: graphed, graphed-executors and graphed-histogram (all on PyPI; graphed at 0.0.3, the other two at 0.0.2 or later) are entries in the test dependency group behind python_version >= "3.11" (graphed requires 3.11+), so the tests/test_1720_graphed_*.py suite runs on the 3.11+ jobs and is skipped on 3.10. The optional imports go through uproot.extras.graphed() / uproot.extras.graphed_executors(); the suite's non-test helpers live in the tests/graphed package and are imported as tests.graphed.<name>. The three packages are opted out of [tool.uv] exclude-newer the 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.toml keeps the layout pre-commit.ci's cached toml-sort 0.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's cuda-bindings pre-release version parse in tests-cuda/, which this PR does not touch and which main hits too.

@lgray
lgray marked this pull request as draft September 14, 2026 14:21
@github-actions github-actions Bot added the type/feat PR title type: feat (set automatically) label Sep 14, 2026
@lgray
lgray marked this pull request as ready for review September 14, 2026 17:12
@lgray
lgray requested a review from ariostas September 14, 2026 17:12
@lgray

lgray commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

@ariostas this one is ready to go now.

lgray added a commit to graphed-org/hist-graphed-mvp that referenced this pull request Sep 14, 2026
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
lgray added a commit to graphed-org/hist-graphed-mvp that referenced this pull request Sep 14, 2026
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 ariostas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @lgray, I left a few comments. I'll take a closer look when I have some time.

Comment thread src/uproot/_graphed.py Outdated
Comment thread tests/test_1720_graphed_behaviors.py Outdated
Comment thread tests/graphed/analysis.py Outdated
@lgray

lgray commented Sep 14, 2026

Copy link
Copy Markdown
Contributor Author

@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.

@lgray

lgray commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

@ariostas something's really wrong with the scikit-hep hosting all the CI jobs keep timing out.

@ariostas

Copy link
Copy Markdown
Member

@ariostas something's really wrong with the scikit-hep hosting all the CI jobs keep timing out.

Hmm I'll have a look

@ariostas

Copy link
Copy Markdown
Member

@lgray should work now. Thanks for letting me know! I restarted the CI

@lgray

lgray commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

@ariostas cool - all green here now except CUDA-13 as expected. Lemme know if you have any comments :-)

henryiii pushed a commit to scikit-hep/hist that referenced this pull request Sep 17, 2026
* 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>
Lindsey Gray and others added 14 commits September 18, 2026 14:51
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>
@lgray

lgray commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

@ariostas a gentle ping on this one when you have time.

@ariostas ariostas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. 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_write docstrings (API may change without a deprecation cycle), and keep the public surface to those two names (+ maybe graphed_head) until it settles.
  2. Collapse the two read paths into one by defaulting to TrivialFormMapping, as uproot.dask does. 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.
  3. 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 only graphed_partitions / read_graphed_partition.

Comment thread src/uproot/writing/_graphed_write.py Outdated
# 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

Comment thread src/uproot/writing/_graphed_write.py Outdated
with uproot.recreate(
path, **_recreate_kwargs(compression, compression_level)
) as out:
out[tree_name] = record

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

Comment thread src/uproot/writing/_graphed_write.py Outdated
if isinstance(compression, str)
else compression
)
return {"compression": resolved}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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).

Comment thread src/uproot/writing/_graphed_write.py Outdated
"process": graphed_executors.local.ProcessPoolExecutor,
"thread": graphed_executors.local.ThreadExecutor,
}[executor]
return executor # an executor class passed directly (instantiated by the caller)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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,
):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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).

Comment thread pyproject.toml Outdated
test = [
"aiohttp",
"deflate",
"graphed[awkward,parquet]>=0.0.3; python_version >= '3.11'",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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?

@lgray lgray Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

Comment thread tests/test_1720_graphed_blind_steps.py Outdated

@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])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

Comment thread tests/test_1720_graphed_preserve.py Outdated
@@ -0,0 +1,174 @@
# BSD 3-Clause License; see https://github.com/scikit-hep/uproot5/blob/main/LICENSE

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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 ariostas left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI text below 🤖

A GPT second pass found these additional, non-duplicate issues:

  1. P1: nested RNTuples get the wrong schema (src/uproot/_graphed.py:316-341). HasFields.keys(full_paths=False) flattens rec.x to x, then _get_ttree_form advertises x as a top-level field even though RNTuple.arrays(["x"]) returns it under rec. In a local {rec: {x, y}, z} RNTuple, g.x records but fails at execution, and graphed_head(g.rec.x, 3) also fails. Please use to_akform(filter_name=common_keys) as _dask.py:1767-1769 does.

  2. P1: repeated inputs overwrite output parts (src/uproot/writing/_graphed_write.py:181-199). file_bases is a dict keyed only by (uri, tree), so graphed([spec, spec]) with two steps reports part-00002.root, part-00003.root twice 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.

  3. 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. For g.x[2:8] over range(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_write preserves dataset-global slicing. Either global operations need partition-aware handling or this path must reject/document non-row-local graphs.

  4. P1: graphed_head applies n before the expression (src/uproot/_graphed.py:665-670). It reads the first n source rows and only then filters/slices. Thus graphed_head(g.x[10:20], 5) is empty instead of [10,11,12,13,14], and head(g[g.x % 2 == 0], 2) returns one row instead of two. A dask-like head must collect the first n output rows.

  5. P1: mapped projection can under-supply compiled evaluation (src/uproot/_graphed.py:632-638). The comment above _evaluation_columns correctly notes that compiled IR replays dead intermediate nodes, but mapped sources switch back to output-buffer projection. With zip({"jet": g.Jet.pt, "muon": g.Muon.pt})[["jet"]], projection omits Muon_pt while IR still evaluates that node; both graphed_head and graphed_write fail. Mapped reads need the same syntactic-evaluation safeguard translated through the mapping.

  6. P2: the default process writer cannot ship common behaviors (src/uproot/writing/_graphed_write.py:185-195). The partial serializes session.backend; vector.backends.awkward.behavior contains lambdas, so even the returned compute=False plan raises PicklingError under pickle.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.

  7. P2: external evaluators are dropped by graphed_head and graphed_write (src/uproot/_graphed.py:663-670, src/uproot/writing/_graphed_write.py:82). A graphed.apply(...) expression materializes and runs through aggregate_plan, but these helpers call evaluate_ir without the session evaluators and fail with “External payload ... needs an evaluator.” aggregate_plan already wires these through _external_evaluators.

  8. P2: already-open TTrees are not partition-compatible (src/uproot/_graphed.py:82-100). They are accepted by the documented dask-compatible files API and whole-dataset materialization works, but the Model_TTree is stored as Partition.uri; resources.open_once then raises because it is unhashable. Normalize it to a reusable file/tree reference or special-case open objects in open_tree.

The existing Graphed-focused suite still passes (135 passed); these are missing edge/parity cases rather than current test failures.

@lgray

lgray commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

@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.
@lgray

lgray commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

🤖 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 graphed>=0.0.4, released as graphed-org/graphed#39. Until 0.0.4 is on PyPI the new graphed-build CI job cannot resolve the dependency; nothing else changes for the non-graphed jobs.

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 allow_missing already worked). Round-3 item 1's schema is fixed; the remaining nested-RNTuple read failure is uproot.dask's own and is described in that reply.

Also run locally: uproot's offline suite and the dask tests against the shared helpers (all green).

@lgray

lgray commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI text below 🤖

Mark it experimental. … keep the public surface to those two names (+ maybe graphed_head) until it settles.

Done. Experimental notes on the module and on uproot.graphed / uproot.graphed_write; the exported names are uproot.graphed, uproot.graphed_head, uproot.graphed_write. The rest is importable from uproot._graphed only (details in the __init__.py thread).

@lgray

lgray commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI text below 🤖

Collapse the two read paths into one by defaulting to TrivialFormMapping, as uproot.dask does.

Done, and further: reads share _dask.py's resolver, base-form construction and _read_tree with uproot.dask, so the two front ends cannot drift apart again (details in the _graphed.py L118 / L132 threads).

@lgray

lgray commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI text below 🤖

Trim the tests. … Almost half of that is four files that exercise only graphed_partitions / read_graphed_partition.

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 test-graphed group with one CI job.

@lgray

lgray commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI text below 🤖

P1: nested RNTuples get the wrong schema … Please use to_akform(filter_name=common_keys) as _dask.py:1767-1769 does.

Fixed as suggested: the base form comes from to_akform(filter_name=common_keys) through the helper uproot.dask uses, so {rec: {x, y}, z} keeps its nesting and g.rec.x records (test_a_nested_rntuple_keeps_its_nesting).

One thing this does not fix, because it is not this PR's: reading a nested RNTuple field still fails, identically on uproot.daskuproot.dask(f).rec.x.compute() raises ValueError: zip() argument 2 is longer than argument 1 from the RNTuple top-level-fields workaround in TrivialFormMappingInfo.load_buffers (the TODO there). Top-level RNTuple fields read fine on both paths.

@lgray

lgray commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI text below 🤖

P1: repeated inputs overwrite output parts … The occurrence needs to be part of the partition identity/index, or duplicate inputs need to be rejected.

Rejected explicitly. graphed.write.file_bases (graphed-org/graphed#38) raises ValueError("duplicate input …: list each file (or file, tree) once"), and graphed_write surfaces it before any file is opened (test_refusals_happen_before_anything_is_written).

@lgray

lgray commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI text below 🤖

P1: writes silently change axis-0 semantics with partition count … Either global operations need partition-aware handling or this path must reject/document non-row-local graphs.

Rejected explicitly, at the cause. A partition-wise driver evaluates the compiled IR once per chunk, so any reduction of the partitioned axis (x[2:8], x[0], gak.sum(x)) is a per-chunk partial. graphed.refuse_chunk_partials (graphed-org/graphed#38) refuses one that is a write output or that feeds another node, with GraphedError: 'slice' reduces the partitioned axis: a partitioned write has no combine step …; graphed_write calls it before opening anything (test_refusals_happen_before_anything_is_written). graphed's own to_parquet writers had the same defect and carry the same guard now. Dataset-global slicing is not emulated.

@lgray

lgray commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI text below 🤖

P1: graphed_head applies n before the expression … A dask-like head must collect the first n output rows.

Fixed. graphed_head collects the first n rows of the output, growing a prefix read until the evaluated result has n rows or the first tree is exhausted: graphed_head(g.x[3:8], 3) == [3, 4, 5], graphed_head(g[g.x % 2 == 0], 2) returns two rows (test_head_returns_the_first_rows_of_the_output).

@lgray

lgray commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI text below 🤖

P1: mapped projection can under-supply compiled evaluation … With zip({"jet": g.Jet.pt, "muon": g.Muon.pt})[["jet"]], projection omits Muon_pt while IR still evaluates that node; both graphed_head and graphed_write fail.

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 MiniNanoMapping test helper, uproot.dask vs uproot.graphed).

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:

path necessary columns compute
uproot.dask, mapped {Jet_pt, nJet, nMuon} ValueError: cannot broadcast nested list
uproot.graphed, mapped {Jet_pt, nJet, nMuon} ValueError: cannot broadcast nested list (head and write)
eager ak.zip on the read arrays ValueError: cannot broadcast nested list

So Muon_pt is omitted by design — dask-awkward projects it away too — and what fails is awkward's broadcast of jets against muons (different per-event multiplicities), identically on every path. graphed does not read a column dask would not read, and does not fail where dask succeeds.

The well-formed spelling, zip({...}, depth_limit=1)[["jet"]], projects to {Jet_pt, nJet, nMuon} and evaluates on graphed_head and graphed_write (test_unread_branches_are_placeholders_the_graph_can_pass_through). Side note from the same probe: through this mapping uproot.dask reads only {Jet_pt, nJet} for that spelling and its compute() fails with TypeError: PlaceholderArray supports only trivial slices, not int — the muon list needs its nMuon offsets even when its data is never touched; graphed declares that buffer as an offsets need and reads it.

@lgray

lgray commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI text below 🤖

P2: the default process writer cannot ship common behaviors … ROOT writing needs the same seam or should avoid serializing the backend instance.

Fixed with the same seam as the Parquet writer. The worker receives the backend as a class or a "module:attr" import reference (the aggregate_plan convention), default = the session backend's class; a behavior travels by reference (test_process_pool_with_a_behavior_backend_by_import_ref, which also pins that a lambda-bearing backend instance still cannot pickle — that is inherent, and no longer the default).

@lgray

lgray commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI text below 🤖

P2: external evaluators are dropped by graphed_head and graphed_writeaggregate_plan already wires these through _external_evaluators.

Fixed. Both carry the session's evaluators, through the now-public graphed.aggregate.external_evaluators (graphed-org/graphed#38, which also wired them into graphed's own writers, which had the same gap). test_externals_reach_the_write_workers; graphed_head(graphed.apply(f, g.px1), 2) evaluates.

@lgray

lgray commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

🤖 AI text below 🤖

P2: already-open TTrees are not partition-compatible … Normalize it to a reusable file/tree reference or special-case open objects in open_tree.

Fixed by normalising: an open TTree becomes its (file_path, object_path) pair and workers reopen it by path (test_an_open_tree_is_reopened_by_path_in_the_workers).

@lgray lgray closed this Sep 22, 2026
@lgray lgray reopened this Sep 22, 2026
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
@lgray

lgray commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

@ariostas OK - bot responses done and everything is addressed. Re-review or let me know what to do otherwise! Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type/feat PR title type: feat (set automatically)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants