Skip to content

Commit b279a2a

Browse files
derek73fdesoyeclaude
authored
feat: add given_names attribute (closes #157) (#180)
* feat: add given_names attribute aggregating first and middle names (closes #157) Adds given_names (and given_names_list), the first name followed by all middle names, mirroring the existing surnames attribute (middle + last). Reimplemented fresh on current master from the approach in #157, which was based on a pre-refactor codebase and had drifted. Co-Authored-By: fdesoye <fdesoye@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test: add empty-path and first-only tests for given_names; document pattern in AGENTS.md Covers the `or self.C.empty_attribute_default` branch (untested before this commit) and the no-middle boundary case. Also adds a note to AGENTS.md reminding future contributors to include an empty-path test for any new aggregate property. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: fdesoye <fdesoye@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 518e965 commit b279a2a

5 files changed

Lines changed: 37 additions & 0 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,6 @@ Each named attribute (`title`, `first`, etc.) is a `@property` that joins its co
126126

127127
### Tests (`tests/`)
128128

129+
**When adding a new aggregate property** (like `given_names` or `surnames`), always include a test for the empty path — a name that produces no value for that property — so the `or self.C.empty_attribute_default` guard is covered. The conftest dual-fixture then automatically exercises both `""` and `None` variants. Example: `HumanName("Williams")` for a given-names property (last-name-only string has no first or middle).
130+
129131
Tests run under **pytest** (via `uv run pytest`) and are split one file per concern (`tests/test_titles.py`, `tests/test_suffixes.py`, etc.). `tests/base.py` holds `HumanNameTestBase` — a plain (non-`unittest`) base whose `m()` helper is a custom assert that prints the original name string on failure (plus thin `assert*` shims so the moved test bodies are unchanged). `tests/conftest.py` defines an autouse fixture that runs **every test twice** — once with `empty_attribute_default = ''` and once with `None` — so reported counts are doubled (e.g. 11 methods → 22 results); it also snapshots/restores the scalar `CONSTANTS` config around each test to keep tests order-independent. `TEST_NAMES` (in `tests/test_variations.py`) is a list of name strings permuted into comma-separated variants as a regression check. Tests that should fail use `@pytest.mark.xfail`. When adding a parsing case, add it to the relevant `tests/test_*.py` file and consider adding the base form to `TEST_NAMES`.

docs/release_log.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Release Log
77
to 1.2.1 first (which includes a one-version compatibility shim), load and
88
re-pickle under 1.2.1, then upgrade to 1.3.0.
99

10+
- Add ``given_names`` (and ``given_names_list``) attribute as aggregate of first and middle names, mirroring ``surnames`` (closes #157)
1011
- Add ``suffix_delimiter`` to ``Constants`` and ``HumanName`` for parsing suffixes separated by arbitrary delimiters, e.g. ``"RN - CRNA"`` (#156)
1112
- Add ``initials_separator`` to ``Constants`` and ``HumanName`` to control spacing between consecutive initials within a name group (#171)
1213
- Fix ``Constants`` customizations, singleton identity, and ``TupleManager`` subclass being lost across ``pickle``/``deepcopy`` round-trips (#167, #168, #169)

docs/usage.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ Requires Python 3.10+.
2525
'III'
2626
>>> name.surnames
2727
'Q. Xavier de la Vega'
28+
>>> name.given_names
29+
'Juan Q. Xavier'
2830
>>> name.full_name = "Juan Q. Xavier Velasquez y Garcia, Jr."
2931
>>> name
3032
<HumanName : [

nameparser/parser.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ class HumanName:
4444
* :py:attr:`suffix`
4545
* :py:attr:`nickname`
4646
* :py:attr:`surnames`
47+
* :py:attr:`given_names`
4748
4849
:param str full_name: The name string to be parsed.
4950
:param constants constants:
@@ -415,6 +416,20 @@ def surnames(self) -> str:
415416
"""
416417
return " ".join(self.surnames_list) or self.C.empty_attribute_default
417418

419+
@property
420+
def given_names_list(self) -> list[str]:
421+
"""
422+
List of first name followed by middle names.
423+
"""
424+
return self.first_list + self.middle_list
425+
426+
@property
427+
def given_names(self) -> str:
428+
"""
429+
A string of the first name followed by all middle names.
430+
"""
431+
return " ".join(self.given_names_list) or self.C.empty_attribute_default
432+
418433
# setter methods
419434

420435
def _set_list(self, attr: str, value: str | list[str] | None) -> None:

tests/test_python_api.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,23 @@ def test_surnames_attribute(self) -> None:
263263
hn = HumanName("John Edgar Casey Williams III")
264264
self.m(hn.surnames, "Edgar Casey Williams", hn)
265265

266+
def test_given_names_list_attribute(self) -> None:
267+
hn = HumanName("John Edgar Casey Williams III")
268+
self.m(hn.given_names_list, ["John", "Edgar", "Casey"], hn)
269+
270+
def test_given_names_attribute(self) -> None:
271+
hn = HumanName("John Edgar Casey Williams III")
272+
self.m(hn.given_names, "John Edgar Casey", hn)
273+
274+
def test_given_names_attribute_first_only(self) -> None:
275+
hn = HumanName("John Williams")
276+
self.m(hn.given_names_list, ["John"], hn)
277+
self.m(hn.given_names, "John", hn)
278+
279+
def test_given_names_attribute_empty(self) -> None:
280+
hn = HumanName("Dr. Williams")
281+
self.m(hn.given_names, hn.C.empty_attribute_default, hn)
282+
266283
def test_is_prefix_with_list(self) -> None:
267284
hn = HumanName()
268285
items = ['firstname', 'lastname', 'del']

0 commit comments

Comments
 (0)