Skip to content

FileContext.type_by_name returns object: a lookup can be an annotated builtin - #147

Merged
sathabbott merged 5 commits into
nsmith-:mainfrom
sathabbott:fix/135-type-by-name-object
Oct 5, 2026
Merged

sathabbott merged 5 commits into
nsmith-:mainfrom
sathabbott:fix/135-type-by-name-object

Conversation

@sathabbott

@sathabbott sathabbott commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

🤖 AI generated content

Fixes #135.

FileContext.type_by_name was annotated -> type[ROOTSerializable], but a lookup can return an annotated builtin. TString, TStringLong and string resolve to Annotated[bytes, ROOTString(...)] (#127), and a file's context resolves TDatime to Annotated[int, Fmt(">I")]. mypy therefore accepted .read() on the result, the TypedTKey bug #127 fixed. tests/test_read.py's basket reader still did it.

As decided on the issue (option 1, object rather than Any):

  • FileContext.type_by_name, _BootstrapContext.type_by_name and DynamicFileContext.type_by_name return object. The base class's docstring states the contract: read the result with read_value, never .read(), and narrow with isinstance(t, type) and issubclass(t, ROOTSerializable) where the class itself is needed. mypy now rejects .read() on a lookup.
  • read_value takes object, so it accepts a lookup's result unchanged.
  • tests/test_read.py's _ReadBasket reads its items with read_value.

Beyond the issue, and why:

  • TKey.read_object (without objtype) and TKey.read_from return object. They were annotated ROOTSerializable, but they return whatever the lookup reads: bytes for a TString record, int for a TDatime one. This is the same bug one level up, and TKeyList: key by (name, cycle), with get_by_name for the highest cycle #145 surfaced it once key lookups became typed. With objtype given, read_object still returns that type.
  • The Locator protocol's type variable is no longer bound to ROOTSerializable. A TKey is a locator, and its object can be a builtin, so the bound no longer holds. Fetcher.__call__ then types reader.fetch(key) as object. Callers narrow with isinstance, which every caller in the tests already does where it needs a class. Remove the DataFetcher-era methods now that locators and rootfilespec.reader exist #114 reworks the locators; this changes only the bound.

Not changed: TypedTKey.objtype stays type[ObjType]. TypedTKey.read keeps its # type: ignore[arg-type], since for a string record objtype is the alias. type_by_checksum is not implemented anywhere and is left as it is.

Tests

New tests/test_type_by_name.py:

  • test_return_type_is_object: the return annotation of FileContext.type_by_name, of both implementations and of TKey.read_from is object. All four fail on main.
  • test_lookup_of_tdatime_is_an_annotated_builtin, on root-io-spec's serialization/unframed-records.root:
    • The file's context resolves TDatime to the alias, which is not a class.
    • read_value reads the record's payload as 0x7E6CC000, the value its case.toml pins at 318, with nothing left over. reader.fetch(key) returns the same value.
    • .read() raises AttributeError at run time. The test calls it with # type: ignore[attr-defined], so CI's mypy pins the type statically: on main that ignore is unused and the line before it unreachable, and mypy fails.
  • test_lookup_of_strings: TString, TStringLong and string resolve to their aliases in the bootstrap context and in a file's.
  • test_lookup_of_a_class_narrows: TNamed narrows to the class.

Since #145 merged, test_root_string.py's two string-record reads compare reader.fetch(...) with bytes directly again. #145 had typed them object to work around TKey.read_from's old annotation.

Full suite: 577 passed, 59 skipped, 49 xfailed (main at 2a65519: 568 passed).

Breaking changes

  • FileContext.type_by_name (and its implementations) returns object. Code that calls .read() on the result fails type checking; read with serializable.read_value(t, buffer), or narrow with isinstance(t, type) and issubclass(t, ROOTSerializable) first. Run-time behaviour is unchanged.
  • TKey.read_from and TKey.read_object without objtype are typed object, so reader.fetch(key) is too. Narrow with isinstance before using the result as a particular class.
  • serializable.Locator's type variable is unbound: a locator may return a builtin.

Assisted-by: claude-code:claude-opus-5-5

… builtin

type_by_name was annotated -> type[ROOTSerializable], but TString,
TStringLong and string resolve to annotated bytes aliases (nsmith-#68), and a
file's context resolves TDatime to an annotated int. mypy therefore let
callers call .read() on the result, the TypedTKey bug nsmith-#127 fixed, and
test_read.py's basket reader still did.

Option 1 on nsmith-#135: FileContext.type_by_name and both implementations
return object, with the contract in the docstring: read the result with
read_value, which now takes object, and narrow with
isinstance(t, type) and issubclass(t, ROOTSerializable) where the class
is needed. mypy now rejects .read() on a lookup.

TKey.read_object (untyped) and TKey.read_from return object as well,
since a TString record reads as bytes; so the Locator protocol's type
variable is no longer bound to ROOTSerializable. The basket reader in
test_read.py uses read_value.

Fixes nsmith-#135

Assisted-by: claude-code:claude-opus-5-5
@codecov-commenter

codecov-commenter commented Oct 5, 2026 •

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.31%. Comparing base (2a65519) to head (db3e332).
⚠️ Report is 2 commits behind head on main.
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #147   +/-   ##
=======================================
  Coverage   93.31%   93.31%           
=======================================
  Files          37       37           
  Lines        3277     3277           
=======================================
  Hits         3058     3058           
  Misses        219      219           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread src/rootfilespec/serializable.py
@sathabbott
sathabbott requested a review from nsmith- October 5, 2026 19:38
@sathabbott

Copy link
Copy Markdown
Collaborator Author

@nsmith- this is ready for you, but I don't fully understand it.

nsmith-#145 typed two string-record reads in test_root_string.py as object,
because TKey.read_from claimed ROOTSerializable. It is typed object now,
so the reads compare directly again.

Assisted-by: claude-code:claude-opus-5-5
The comment on T_co only said why it is unbound. It is the type a
locator returns (Locator[RPage], Locator[TTree]), unbound because some
locators return a builtin. Wording from Nick's review of nsmith-#147.

Assisted-by: claude-code:claude-opus-5-5
@sathabbott
sathabbott merged commit 82d6f7f into nsmith-:main Oct 5, 2026
10 checks passed
@sathabbott
sathabbott deleted the fix/135-type-by-name-object branch October 6, 2026 00:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

FileContext.type_by_name can return an annotated builtin, not only a ROOTSerializable class

3 participants