Skip to content

Fix bugs hidden in untested parser paths; remove vestigial unparsable#216

Merged
derek73 merged 3 commits into
masterfrom
fix/parser-coverage-gaps
Jul 5, 2026
Merged

Fix bugs hidden in untested parser paths; remove vestigial unparsable#216
derek73 merged 3 commits into
masterfrom
fix/parser-coverage-gaps

Conversation

@derek73

@derek73 derek73 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

What changed

A coverage audit of the 11 uncovered lines in parser.py (98% at the time) found three real bugs, several lines of dead code, and a few untested-but-live paths. This PR fixes all of them; parser.py is now at 100% statement coverage (1298 tests, up from 1282).

Bug fixes

  • __hash__ violated the hash/eq contract: __eq__ compares lowercased strings but __hash__ didn't lowercase, so HumanName("John Smith") == HumanName("john smith") was True while their hashes differed — {hn1, hn2} had size 2. Now hashes the lowercased string.
  • initials() malformed output / crash: a name part with no initialable words (e.g. prefix-only middle name "de la" in "Vega, Juan de la") produced "J. . V." — and raised TypeError when empty_attribute_default is None. Such parts are now dropped; the duplicated per-group comprehensions in initials()/initials_list() were deduplicated into a _initials_lists() helper.
  • Trailing suffix silently dropped after an empty comma segment: "Doe, John,, Jr." lost the Jr. because the old if parts[2]: guard skipped all remaining segments when the first was empty. Empty segments are now skipped individually.

Removals (breaking)

  • unparsable attribute removed: the if len(self) < 0: guard meant to set it was unreachable (__len__ can't be negative), so it has reported False for every parsed name — including empty input — since the earliest releases.
  • __ne__ removed: Python 3 derives != from __eq__ automatically.
  • __process_initial__ renamed to _process_initial: double-underscore-both-sides names are reserved for Python special methods. Subclasses overriding the old name must rename their override.
  • Two unreachable if not nxt: branches removed from the parse loops: the final piece always hits the are_suffixes(pieces[i+1:]) branch first because all() over the empty tail is vacuously True. Comments now document that behavior at both sites.

Notes for review

  • Every behavior change had a failing test written first (the hash contract, the initials edge case, the dropped-suffix regression, and the unparsable removal pin); documenting tests were added for !=, __setitem__ KeyError, repr, and the string_format=None fallback in __str__.
  • Nine stale assertFalse(hn.unparsable) assertions were removed from existing tests.
  • All entries added to the 1.3.0 release log. Verified: pytest (1298 passed), mypy, ruff, sphinx doctests (186), README doctests.
  • The remaining 2 partial branches are inherent, not gaps: the suffix-comma loop always exits via break (same vacuous-truth reason), and _process_initial's list guard can't be false for str.split() output.

🤖 Generated with Claude Code

derek73 and others added 2 commits July 4, 2026 19:55
Auditing the 11 uncovered lines in parser.py (98% coverage) turned up
three real bugs, dead code, and a few untested-but-live paths:

- Fix __hash__ to lowercase like __eq__ does, so equal HumanName
  instances hash equal and behave correctly in sets and dicts
- Fix initials() emitting a stray empty initial ("J. . V.") — or
  raising TypeError when empty_attribute_default is None — for name
  parts with no initialable words (e.g. prefix-only middle "de la");
  deduplicate the per-group comprehensions into _initials_lists()
- Fix a trailing suffix being silently dropped after an empty comma
  segment, e.g. "Doe, John,, Jr." losing the "Jr."
- Remove the unparsable attribute: the len(self) < 0 guard meant to
  set it was unreachable, so it has reported False for every parsed
  name since the earliest releases
- Remove __ne__ (Python 3 derives != from __eq__) and the two
  unreachable "if not nxt" branches in the parse loops, which the
  vacuously-true are_suffixes() check always preempts
- Rename __process_initial__ to _process_initial: dunder-both-sides
  names are reserved for Python special methods

Adds dunder-contract, initials, and suffix regression tests; parser.py
is now at 100% statement coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t empty parse

- Correct release log: the unparsable guard broke in 2013 (v0.2.9), not
  in the earliest releases (2011-2013 releases had a working attribute)
- Pin multiple empty comma segments ("Doe, John,, Jr.,, III") so a
  break-instead-of-continue regression can't pass the single-empty test
- Pin hash interop with plain strings ("john smith" in {HumanName(...)}),
  which depends on hashing str(self).lower() specifically
- Tighten the "final piece" comments in parse_full_name and document
  are_suffixes()'s vacuous-truth contract that the piece loops rely on
- Document in usage.rst that empty/garbage input parses to an all-empty
  name, checked via len(name) == 0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@derek73 derek73 self-assigned this Jul 5, 2026
@derek73 derek73 added the bug label Jul 5, 2026
@derek73 derek73 added this to the v1.3.0 milestone Jul 5, 2026
…tead

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@derek73 derek73 merged commit e51d5b8 into master Jul 5, 2026
8 checks passed
@derek73 derek73 deleted the fix/parser-coverage-gaps branch July 5, 2026 03:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant