edgar_read: list 13F holdings instead of returning None (#1337) - #1365
Merged
Merged
Conversation
_extract_13f_section tested the ThirteenF.holdings DataFrame for truth, which raises; the broad except returned None at DEBUG. Walk the first 30 rows with iterrows() instead and log extraction failures at warning. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…them (dgunning#1337) test_mcp_intent_tools.py is classed network by filename, which put the new offline tests outside the fast lane and left check_offline_audit.py with nothing to audit. They pass with outbound sockets blocked.
dgunning
approved these changes
Sep 26, 2026
dgunning
left a comment
Owner
There was a problem hiding this comment.
Thanks, this is exactly what I hoped for. The #1136 loop now lives in one place (_holding_rows), _get_fund_holdings calls it, and the section checks is not None and not .empty. I also ran it against Berkshire's latest 13F-HR (Q2 2026): it lists the holdings correctly, starting with Apple at 227,917,808 shares worth $65,950,296,923.
Two non-blocking notes for a follow-up:
_holding_rowsskips rows with no usable cell, so the header can say "30 of 211" while listing fewer lines. Real filings don't hit this.- The holdings section counts securities (Berkshire: "29 of 29"), while the summary's "Total Holdings" counts information-table lines (89). Both are right, but they read as contradictory next to each other, so it's worth labelling them. This isn't something this PR introduced.
Merging.
Merged
dgunning
added a commit
that referenced
this pull request
Sep 26, 2026
Two fixes merged since 5.59.0 earlier today. ExxonMobil's 10-K Item 7 opened on a "Table of Contents" breadcrumb again, a 5.59.0 regression from rendering section tables cell by cell (#1364). And edgar_read's 13F holdings section, which returned None for every filing, now lists the top holdings through the row reader shared with edgar_ownership (#1365, GH #1337, thanks @wolfgang-aura). 2 fragments folded into the dated section. test-fast green: 7729 passed, 12 skipped, 1 xfailed. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1337.
edgar_readreturnedholdings: Nonefor every 13F-HR, while thesummarysection of the same response reportedTotal Holdings: 211._extract_13f_section(obj, "holdings")inedgar/ai/mcp/tools/reader.pytestedobj.holdingsfor truth.ThirteenF.holdingsis a DataFrame, so that raisedValueError: The truth value of a DataFrame is ambiguous. Theexcept Exceptioncaught it atDEBUGand the function returnedNone. With the guard removed, the loop underneath would still have been wrong: iterating a DataFrame yields its column names, andgetattr(h, 'name')on a string reads nothing useful. #1136 was the same pattern in_get_fund_holdings.The section now checks
holdings is not None and not holdings.emptyand reads the first 30 rows through a new_holding_rowshelper inedgar/ai/mcp/tools/base.py. As you asked, that helper is the row loop_get_fund_holdingshas used since #1136, moved out ofownership.pyunchanged:iterrows()with_cell_text/_cell_numberoverIssuer,Cusip,SharesPrnAmountandValue._get_fund_holdingsnow calls it too, so there is one copy. A missing issuer prints asUnknown, and a missing share count or value is left off the line. The failure log moves fromdebugtowarning.Before, on the issue's 211-row fixture:
None. After:How this was tested
Fixtures: an in-memory DataFrame with the
ThirteenF.holdingscolumns, as in the issue. No network.TestExtract13FHoldingsintests/ai/test_mcp_intent_tools.py: the header, the first and 30th lines, the 30-row cap, NaN cells, andNonefor an empty or missing frame.tests/issues/regression/test_mcp_ownership_cell_coercion.py(the MCPedgar_ownership(analysis_type="fund_portfolio")always returnsholdings: []#1136 regression tests for_get_fund_holdings): 15 passed after the move.pytest tests/ai -m "not (slow or network or performance or batch)": 169 passed, 1 failed. The failure istest_install_skill_uses_symlinks: creating symlinks needs a privilege this Windows account lacks, and it fails the same way at the base commit.edgar.ai.mcp.tools, with no marker filter: 148 passed, 0 failed.Run on Windows 11 / Python 3.14.3; CI covers the rest of the matrix. Changelog fragment:
changelog.d/1337.fixed.md.AI disclosure
This change was drafted with AI assistance: Claude Opus 5.5 wrote the patch and a second Claude Opus 5.5 pass reviewed it, both running under Mailman. The second commit, which moves the row loop into
_holding_rowsafter your comment, was written by Claude Opus 5.5 in a later session and did not get a second review pass. The test results above come from the harness running the commands itself, not from either agent's account of its own work. The account filing this pull request answers for every line of it.