Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
1 change: 1 addition & 0 deletions docs/release_log.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
<HumanName : [
Expand Down
15 changes: 15 additions & 0 deletions nameparser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class HumanName:
* :py:attr:`suffix`
* :py:attr:`nickname`
* :py:attr:`surnames`
* :py:attr:`given_names`

:param str full_name: The name string to be parsed.
:param constants constants:
Expand Down Expand Up @@ -415,6 +416,20 @@ def surnames(self) -> 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:
Expand Down
17 changes: 17 additions & 0 deletions tests/test_python_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,23 @@ 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_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']
Expand Down