From 537739290fae02e1b48a0389ba000be16faad159 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Mon, 29 Jun 2026 14:33:57 -0700 Subject: [PATCH 1/2] 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 Co-Authored-By: Claude Opus 4.8 --- docs/release_log.rst | 1 + docs/usage.rst | 2 ++ nameparser/parser.py | 15 +++++++++++++++ tests/test_python_api.py | 8 ++++++++ 4 files changed, 26 insertions(+) diff --git a/docs/release_log.rst b/docs/release_log.rst index c603a86..4720d32 100644 --- a/docs/release_log.rst +++ b/docs/release_log.rst @@ -7,6 +7,7 @@ Release Log to 1.2.1 first (which includes a one-version compatibility shim), load and re-pickle under 1.2.1, then upgrade to 1.3.0. + - Add ``given_names`` (and ``given_names_list``) attribute as aggregate of first and middle names, mirroring ``surnames`` (closes #157) - Add ``suffix_delimiter`` to ``Constants`` and ``HumanName`` for parsing suffixes separated by arbitrary delimiters, e.g. ``"RN - CRNA"`` (#156) - Add ``initials_separator`` to ``Constants`` and ``HumanName`` to control spacing between consecutive initials within a name group (#171) - Fix ``Constants`` customizations, singleton identity, and ``TupleManager`` subclass being lost across ``pickle``/``deepcopy`` round-trips (#167, #168, #169) diff --git a/docs/usage.rst b/docs/usage.rst index 5ab1d28..f744dea 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -25,6 +25,8 @@ Requires Python 3.10+. 'III' >>> name.surnames 'Q. Xavier de la Vega' + >>> name.given_names + 'Juan Q. Xavier' >>> name.full_name = "Juan Q. Xavier Velasquez y Garcia, Jr." >>> name str: """ return " ".join(self.surnames_list) or self.C.empty_attribute_default + @property + def given_names_list(self) -> list[str]: + """ + List of first name followed by middle names. + """ + return self.first_list + self.middle_list + + @property + def given_names(self) -> str: + """ + A string of the first name followed by all middle names. + """ + return " ".join(self.given_names_list) or self.C.empty_attribute_default + # setter methods def _set_list(self, attr: str, value: str | list[str] | None) -> None: diff --git a/tests/test_python_api.py b/tests/test_python_api.py index 8602aef..a48617e 100644 --- a/tests/test_python_api.py +++ b/tests/test_python_api.py @@ -263,6 +263,14 @@ def test_surnames_attribute(self) -> None: hn = HumanName("John Edgar Casey Williams III") self.m(hn.surnames, "Edgar Casey Williams", hn) + def test_given_names_list_attribute(self) -> None: + hn = HumanName("John Edgar Casey Williams III") + self.m(hn.given_names_list, ["John", "Edgar", "Casey"], hn) + + def test_given_names_attribute(self) -> None: + hn = HumanName("John Edgar Casey Williams III") + self.m(hn.given_names, "John Edgar Casey", hn) + def test_is_prefix_with_list(self) -> None: hn = HumanName() items = ['firstname', 'lastname', 'del'] From 9479a19e3fb12560b4ee1335625a33d4ad467b33 Mon Sep 17 00:00:00 2001 From: Derek Gulbranson Date: Mon, 29 Jun 2026 14:44:27 -0700 Subject: [PATCH 2/2] 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 --- AGENTS.md | 2 ++ tests/test_python_api.py | 9 +++++++++ 2 files changed, 11 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index c19ba18..8866f86 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -126,4 +126,6 @@ Each named attribute (`title`, `first`, etc.) is a `@property` that joins its co ### Tests (`tests/`) +**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). + 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`. diff --git a/tests/test_python_api.py b/tests/test_python_api.py index a48617e..6ec18d9 100644 --- a/tests/test_python_api.py +++ b/tests/test_python_api.py @@ -271,6 +271,15 @@ def test_given_names_attribute(self) -> None: hn = HumanName("John Edgar Casey Williams III") self.m(hn.given_names, "John Edgar Casey", hn) + def test_given_names_attribute_first_only(self) -> None: + hn = HumanName("John Williams") + self.m(hn.given_names_list, ["John"], hn) + self.m(hn.given_names, "John", hn) + + def test_given_names_attribute_empty(self) -> None: + hn = HumanName("Dr. Williams") + self.m(hn.given_names, hn.C.empty_attribute_default, hn) + def test_is_prefix_with_list(self) -> None: hn = HumanName() items = ['firstname', 'lastname', 'del']