fix(formd,form144): three typed fields that were silently wrong (#1192, #1193, #1195) - #1203
Open
dgunning wants to merge 2 commits into
Open
fix(formd,form144): three typed fields that were silently wrong (#1192, #1193, #1195)#1203dgunning wants to merge 2 commits into
dgunning wants to merge 2 commits into
Conversation
#1193, #1195) All three were reported by synfonia-llc on 2026-08-30, each with an offline reproduction and the source line. All three had existing tests over them, and two of those tests asserted the parser's own answer back at itself, so the suite agreed with the defect for as long as it existed. Form D ZIP codes (#1193). `child_text(address_tag, "30361")` looked for a child element named <30361> -- the sample ZIP from the docstring 40 lines above, pasted in where the tag name belongs -- so every sales-compensation recipient's zipcode was None. The identical read for related persons was always correct, which is why nothing downstream looked broken. FormD.is_new (#1192). The value parsed from <isAmendment> was assigned to is_new, which asks the opposite question, so a base D reported is_new=False and rendered as "FORMD/A" while a D/A rendered as "FORMD". The local is renamed for what it holds and inverted at the point of use, preserving None for an absent element rather than turning it into a claim. to_context() now takes the heading from submissionType, the SEC's own field, so the heading cannot contradict submission_type on the same object. Form144.nothing_to_report (#1195). The raw 'Y'/'N' flag was stored against a bool annotation, and bool("N") is True, so `if form.nothing_to_report:` read a notice reporting a sale as reporting none. Now parsed through a _yes_no_flag helper: 'Y'/'N' become True/False and anything unrecognized -- including an absent element -- stays None rather than defaulting, so "did not answer" and "answered no" stay distinguishable. This changes the field's type; callers comparing it against the string need updating, and the CHANGELOG says so. The regression tests read every expectation out of the raw XML with the standard library. Given that two existing tests pinned two of these bugs, an expectation produced by the machinery under test is worth nothing here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LN2NaNcXEuv5YvcKFntcaZ
CodeFactor flagged the stdlib `xml.etree` parse these tests used to read their expectations (the ruff `# noqa: S314` silenced ruff, not CodeFactor). `defusedxml` imports in this environment but is not declared in pyproject.toml, so reaching for it would lean on an undeclared transitive dependency. The expectations are now sliced straight out of the fixture text with two small regex helpers. That is not a workaround: the point of the file is that the expectation must not be produced by the machinery under test, and using no parser at all is the strongest available form of that. The fixtures are small, checked in and static, and the values wanted are leaf text. Same 11 tests, same assertions, unchanged behaviour under test. This also drops the `edgar.form144` import in favour of the canonical `edgar.ownership.form144`, so the file no longer emits a DeprecationWarning of its own. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LN2NaNcXEuv5YvcKFntcaZ
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.
Fixes #1192, fixes #1193, fixes #1195.
Three field-level defects reported by @synfonia-llc on 2026-08-30, each with a self-contained offline reproduction and the source line. All three are a few lines of parsing; all three produced output that looked complete and was wrong.
The defects
Form D sales-compensation ZIP codes (#1193).
SalesCompensationRecipient.from_xmlcalledchild_text(address_tag, "30361")— the sample ZIP from the docstring 40 lines above the call, pasted in where the tag name belongs. It looked for a child element named<30361>, which never exists, so every recipient'saddress.zipcodewasNone. The identical read for related persons (formd.py:372) was always correct, which is why nothing downstream ever looked broken.FormD.is_new(#1192). The value parsed from<isAmendment>was assigned straight tois_new, which asks the opposite question. A base Form D reportedis_new=Falseand rendered asFORMD/A; a Form D/A reportedis_new=Trueand rendered asFORMD.submission_typestayed correct throughout, so a single object exposed two contradictory filing identities.Form144.nothing_to_report(#1195). The raw SECY/Nflag was stored against aboolannotation.bool("N")isTrue, soif form.nothing_to_report:read a notice that does report a sale as reporting none — accession0001958244-23-000454has one prior-sale row sitting right underneath the flag. The rows were never lost; only the flag lied.The fixes
Two are direct. The other two are worth a note:
The Form D heading now comes from
<submissionType>rather than from the corrected boolean. Withis_newfixed,"/A" if not self.is_new else ""would have been correct, but it would still render/Awhen the element is absent andis_newisNone, and it would still be a derived answer capable of disagreeing withsubmission_typebeside it. Taking the SEC's own field removes the class of bug rather than this instance of it.nothing_to_reportisOptional[bool], notbool. The house idiom elsewhere ischild_text(...) == 'Y', which silently maps a missing flag toFalse. For this fieldFalsemeans "there is something to report" — an invented answer, and exactly whatXBRLS.from_filings()silently swallows unexpected parsing exceptions and returns partial results #1174/07lk.10are trying to eliminate._yes_no_flagmapsY/NtoTrue/Falseand leaves anything unrecognized, absent included, asNone.This changes
nothing_to_report's type. Callers comparing it against the string"N"need updating. Called out in the CHANGELOG.Two existing tests were pinning two of these bugs
tests/test_regd_notice_contract.py:73assertedoffering.is_new is FalseagainstD.APFund.xml, which files<submissionType>D</submissionType>with<isAmendment>false</isAmendment>— a base notice.tests/test_144_notice_contract.py:77assertedsample['nothing_to_report'] == "N".Both were written by asserting what the parser returned, so the suite agreed with the defects for as long as they existed. Both updated here, with a comment naming the filed value.
That is why every expectation in the new regression file is read out of the raw XML with
xml.etree, never from the objects under test.tests/issues/regression/test_xu3z_data_object_fields.py, 11 tests, including:_yes_no_flagtruth table, including that an unanswered flag staysNone.Verification
formd,form_d,144,offering,regd,exempt): 137 passed.ruff checkclean on all three changed files (the 8 pre-existingS110/B007findings in these modules are untouched and unrelated).Adjacent, deliberately not fixed
data/D.Shepards.xmlfiles the literal string"None"as a sales-compensation ZIP, and the parser now faithfully returns it. This module already strips that artifact forrecipientNameandrecipientCRDNumber(formd.py:129-132) but not for address fields. Extending the cleaning to addresses is a second behaviour change with its own blast radius, so it is recorded as a follow-up rather than folded in here.🤖 Generated with Claude Code
https://claude.ai/code/session_01LN2NaNcXEuv5YvcKFntcaZ