Skip to content

Commit 08635e0

Browse files
authored
Merge pull request #165 from derek73/test-reorg-pytest
Reorganize tests into a pytest package (and fix bugs it exposed)
2 parents dc44d85 + fe4b5f3 commit 08635e0

27 files changed

Lines changed: 2885 additions & 2657 deletions

.github/workflows/python-package.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
run: mypy
4141
- name: Run Tests
4242
run: |
43-
python tests.py
43+
pytest
4444
python -m build --sdist
4545
twine check dist/*
4646
sphinx-build -b html docs dist/docs

AGENTS.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
99
pip install --group dev
1010

1111
# Run all tests
12-
python tests.py
12+
pytest
1313

14-
# Run a single test by class/method
15-
python -m unittest tests.HumanNamePythonTests.test_utf8
14+
# Run a single test file / class / method
15+
pytest tests/test_python_api.py
16+
pytest tests/test_python_api.py::HumanNamePythonTests::test_utf8
1617

1718
# Debug how a specific name string is parsed (prints HumanName repr)
18-
python tests.py "Dr. Juan Q. Xavier de la Vega III"
19+
python -m nameparser "Dr. Juan Q. Xavier de la Vega III"
1920

2021
# Build docs
2122
sphinx-build -b html docs dist/docs
@@ -65,6 +66,6 @@ Parse flow:
6566

6667
Each named attribute (`title`, `first`, etc.) is a `@property` that joins its corresponding `_list`. Setters call `_set_list()` which runs the value through `parse_pieces()`, so assigning `hn.last = "de la Vega"` correctly re-parses prefix tokens.
6768

68-
### Tests (`tests.py`)
69+
### Tests (`tests/`)
6970

70-
All tests live in a single file. `HumanNameTestBase.m()` is a custom assert helper that prints the original name string on failure. Many test classes group cases by name format type. `TEST_NAMES` is a list of name strings that gets automatically permuted into comma-separated variants as a regression check. When adding a new parsing case, add it to the relevant test class and consider adding the base form to `TEST_NAMES`.
71+
Tests run under **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`.

CONTRIBUTING.md

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,23 @@ Install dev dependencies:
1111
Running Tests
1212
---------------
1313

14-
python tests.py
14+
pytest
1515

16-
You can also pass a name string to `tests.py` to see how it will be parsed:
16+
Run a single test file or test:
1717

18-
$ python tests.py "Secretary of State Hillary Rodham-Clinton"
18+
pytest tests/test_titles.py
19+
pytest tests/test_titles.py::TitleTestCase
20+
21+
You can also pass a name string to see how it will be parsed:
22+
23+
$ python -m nameparser "Secretary of State Hillary Rodham-Clinton"
1924
<HumanName : [
20-
Title: 'Secretary of State'
21-
First: 'Hillary'
22-
Middle: ''
23-
Last: 'Rodham-Clinton'
24-
Suffix: ''
25+
title: 'Secretary of State'
26+
first: 'Hillary'
27+
middle: ''
28+
last: 'Rodham-Clinton'
29+
suffix: ''
30+
nickname: ''
2531
]>
2632

2733
CI runs tests against Python 3.10–3.14 via GitHub Actions on every push and pull request.

MANIFEST.in

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
include AUTHORS
22
include LICENSE
33
include README.rst
4-
include tests.py
4+
recursive-include tests *.py

docs/release_log.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
Release Log
22
===========
3+
* Unreleased
4+
- Fix ``initials()`` interpolating the literal ``None`` for empty name parts when ``empty_attribute_default = None`` (e.g. ``"J. None D."``); empty parts now render as an empty string and a fully-empty result returns ``empty_attribute_default``
5+
- Add ``python -m nameparser "Name String"`` command-line helper that prints a parsed name
6+
- Reorganize the test suite from a single ``tests.py`` into a ``tests/`` pytest package
37
* 1.2.0 - June 11, 2026
48
- Drop Python 2 and Python < 3.10 support; Python 3.10–3.14 now required
59
- Add type hints and type declarations (PEP 561 ``py.typed`` marker)

docs/usage.rst

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,8 +191,9 @@ Three attributes exist for the format, `first`, `middle` and `last`.
191191
>>> CONSTANTS.initials_format = "{first} {middle}"
192192
>>> HumanName("Doe, John A. Kenneth, Jr.").initials()
193193
'J. A. K.'
194-
>>> HumanName("Doe, John A. Kenneth, Jr.", initials_format="{last}, {first}).initials()
194+
>>> HumanName("Doe, John A. Kenneth, Jr.", initials_format="{last}, {first}").initials()
195195
'D., J.'
196+
>>> CONSTANTS.initials_format = "{first} {middle} {last}"
196197

197198

198199
Furthermore, the delimiter for the string output can be set through:
@@ -201,16 +202,15 @@ Furthermore, the delimiter for the string output can be set through:
201202
.. doctest:: initials delimiter
202203

203204
>>> HumanName("Doe, John A. Kenneth, Jr.", initials_delimiter=";").initials()
204-
"J; A; K;"
205-
>>> from nameparser.config import CONSTANTS
206-
>>> CONSTANTS.initials_delimiter = "."
207-
>>> HumanName("Doe, John A. Kenneth, Jr.", initials_format="{first}{middle}{last}).initials()
208-
"J.A.K.D."
205+
'J; A; K; D;'
206+
>>> HumanName("Doe, John A. Kenneth, Jr.", initials_format="{first}{middle}{last}", initials_delimiter=".").initials()
207+
'J.A. K.D.'
209208

210209
To get a list representation of the initials, use :py:meth:`~nameparser.HumanName.initials_list`.
211210
This function is unaffected by :py:attr:`~nameparser.config.Constants.initials_format`
212211

213212
.. doctest:: list format
213+
214214
>>> HumanName("Doe, John A. Kenneth, Jr.", initials_delimiter=";").initials_list()
215-
["J", "A", "K", "D"]
215+
['J', 'A', 'K', 'D']
216216

nameparser/__main__.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Command-line debug helper: parse a name and print the result.
2+
3+
Usage:
4+
5+
python -m nameparser "Dr. Juan Q. Xavier de la Vega III"
6+
"""
7+
import logging
8+
import sys
9+
10+
from nameparser import HumanName
11+
12+
13+
def main() -> None:
14+
if len(sys.argv) <= 1:
15+
print('Usage: python -m nameparser "Name String"')
16+
raise SystemExit(1)
17+
log = logging.getLogger('HumanName')
18+
log.setLevel(logging.ERROR)
19+
log.addHandler(logging.StreamHandler())
20+
name_string = sys.argv[1]
21+
hn = HumanName(name_string)
22+
print(repr(hn))
23+
hn.capitalize()
24+
print(repr(hn))
25+
# Use comma rather than concatenation: initials() returns
26+
# empty_attribute_default (possibly None) when there are no initials.
27+
print("Initials:", hn.initials())
28+
29+
30+
if __name__ == '__main__':
31+
main()

nameparser/parser.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,17 +273,21 @@ def initials(self) -> str:
273273
middle_initials_list = [self.__process_initial__(name) for name in self.middle_list if name]
274274
last_initials_list = [self.__process_initial__(name) for name in self.last_list if name]
275275

276+
# Empty parts must render as '' (not empty_attribute_default, which may be
277+
# None) so str.format does not interpolate the literal "None" into the
278+
# output. A fully-empty result falls back to empty_attribute_default,
279+
# matching the other attribute accessors (e.g. ``first``).
276280
initials_dict = {
277281
"first": (self.initials_delimiter + " ").join(first_initials_list) + self.initials_delimiter
278-
if len(first_initials_list) else self.C.empty_attribute_default,
282+
if len(first_initials_list) else "",
279283
"middle": (self.initials_delimiter + " ").join(middle_initials_list) + self.initials_delimiter
280-
if len(middle_initials_list) else self.C.empty_attribute_default,
284+
if len(middle_initials_list) else "",
281285
"last": (self.initials_delimiter + " ").join(last_initials_list) + self.initials_delimiter
282-
if len(last_initials_list) else self.C.empty_attribute_default
286+
if len(last_initials_list) else ""
283287
}
284288

285289
_s = self.initials_format.format(**initials_dict)
286-
return self.collapse_whitespace(_s)
290+
return self.collapse_whitespace(_s) or self.C.empty_attribute_default
287291

288292
@property
289293
def has_own_config(self) -> bool:

pyproject.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ nameparser = ["py.typed"]
3939

4040
[dependency-groups]
4141
dev = [
42+
"pytest (>=8)",
4243
"dill (>=0.2.5)",
4344
"sphinx (>=8)",
4445
"mypy (>=2.1)",
@@ -54,6 +55,13 @@ module = [
5455
]
5556
ignore_missing_imports = true
5657

58+
[tool.pytest.ini_options]
59+
testpaths = ["tests"]
60+
python_classes = ["*Tests", "*TestCase"]
61+
# Fail if an xfail-marked test unexpectedly passes, so a fixed/regressed
62+
# expectation surfaces instead of silently turning into an xpass.
63+
xfail_strict = true
64+
5765
[tool.ruff.lint]
5866
extend-select = [
5967
"ANN", # flake8-annotations

0 commit comments

Comments
 (0)