Skip to content

Commit 67d8e5b

Browse files
derek73claude
andcommitted
fix: add initials_separator; fix or-defaulting for format/delimiter kwargs (closes #152)
- Add initials_separator = " " to Constants: controls the joiner between consecutive initials within a name group, distinct from initials_delimiter which is the trailing character after each individual initial - Add initials_separator kwarg to HumanName.__init__ - Fix or-defaulting to is-not-None for string_format, initials_format, and initials_delimiter kwargs so empty string '' is accepted as a valid value - Use initials_separator in __process_initial__ and initials() in place of hardcoded " " - Document initials_separator in usage.rst with examples Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7686567 commit 67d8e5b

4 files changed

Lines changed: 118 additions & 19 deletions

File tree

docs/usage.rst

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,25 @@ Furthermore, the delimiter for the string output can be set through:
203203

204204
>>> HumanName("Doe, John A. Kenneth, Jr.", initials_delimiter=";").initials()
205205
'J; A; K; D;'
206-
>>> HumanName("Doe, John A. Kenneth, Jr.", initials_format="{first}{middle}{last}", initials_delimiter=".").initials()
207-
'J.A. K.D.'
206+
207+
The separator between consecutive initials *within* a name group (e.g. two middle
208+
names) is controlled by :py:attr:`~nameparser.config.Constants.initials_separator`,
209+
which defaults to ``" "``. Setting it to ``""`` removes that space.
210+
211+
``initials_delimiter``, ``initials_separator``, and ``initials_format`` work together:
212+
213+
- ``initials_delimiter`` — appended *after* each individual initial (default ``"."``)
214+
- ``initials_separator`` — placed *between* consecutive initials in the same group (default ``" "``)
215+
- ``initials_format`` — controls how the first, middle, and last groups are arranged
216+
217+
For example, to produce compact period-separated initials with no spaces:
218+
219+
.. doctest:: initials separator
220+
221+
>>> HumanName("Doe, John A. Kenneth, Jr.", initials_separator="", initials_format="{first}{middle}{last}").initials()
222+
'J.A.K.D.'
223+
>>> HumanName("Doe, John A. Kenneth, Jr.", initials_delimiter="", initials_separator="", initials_format="{first}{middle}{last}").initials()
224+
'JAKD'
208225

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

nameparser/config/__init__.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,17 @@ class Constants:
215215
Will be used to add a delimiter between each initial.
216216
"""
217217

218+
initials_separator = " "
219+
"""
220+
The default separator placed between consecutive initials within a name
221+
group (first, middle, or last). Distinct from ``initials_delimiter``,
222+
which is the trailing character after each individual initial.
223+
224+
With defaults ``initials_delimiter="."`` and ``initials_separator=" "``,
225+
``initials()`` produces ``"J. A. D."``. Setting ``initials_separator=""``
226+
with ``initials_delimiter="."`` produces ``"J.A.D."``.
227+
"""
228+
218229
empty_attribute_default = ''
219230
"""
220231
Default return value for empty attributes.

nameparser/parser.py

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def __init__(
9494
string_format: str | None = None,
9595
initials_format: str | None = None,
9696
initials_delimiter: str | None = None,
97+
initials_separator: str | None = None,
9798
first: str | list[str] | None = None,
9899
middle: str | list[str] | None = None,
99100
last: str | list[str] | None = None,
@@ -106,9 +107,10 @@ def __init__(
106107
self.C = Constants()
107108

108109
self.encoding = encoding
109-
self.string_format = string_format or self.C.string_format
110-
self.initials_format = initials_format or self.C.initials_format
111-
self.initials_delimiter = initials_delimiter or self.C.initials_delimiter
110+
self.string_format = string_format if string_format is not None else self.C.string_format
111+
self.initials_format = initials_format if initials_format is not None else self.C.initials_format
112+
self.initials_delimiter = initials_delimiter if initials_delimiter is not None else self.C.initials_delimiter
113+
self.initials_separator = initials_separator if initials_separator is not None else self.C.initials_separator
112114
if (first or middle or last or title or suffix or nickname):
113115
self.first = first
114116
self.middle = middle
@@ -241,7 +243,7 @@ def __process_initial__(self, name_part: str, firstname: bool = False) -> str:
241243
if not (self.is_prefix(part) or self.is_conjunction(part)) or firstname:
242244
initials.append(part[0])
243245
if len(initials) > 0:
244-
return " ".join(initials)
246+
return self.C.initials_separator.join(initials)
245247
else:
246248
return self.C.empty_attribute_default
247249

@@ -265,19 +267,25 @@ def initials_list(self) -> list[str]:
265267

266268
def initials(self) -> str:
267269
"""
268-
Return period-delimited initials of the first, middle and optionally last name.
270+
Return formatted initials for the name, controlled by
271+
``initials_format``, ``initials_delimiter``, and ``initials_separator``.
269272
270-
:param bool include_last_name: Include the last name as part of the initials
271-
:rtype: str
273+
``initials_delimiter`` is appended after each individual initial.
274+
``initials_separator`` is placed between consecutive initials within
275+
a name group (first, middle, or last). Both can be set as
276+
``Constants`` attributes or as ``HumanName`` constructor kwargs.
272277
273-
.. doctest::
278+
.. doctest::
274279
275-
>>> name = HumanName("Sir Bob Andrew Dole")
276-
>>> name.initials()
277-
"B. A. D."
278-
>>> name = HumanName("Sir Bob Andrew Dole", initials_format="{first} {middle}")
279-
>>> name.initials()
280-
"B. A."
280+
>>> name = HumanName("Sir Bob Andrew Dole")
281+
>>> name.initials()
282+
"B. A. D."
283+
>>> name = HumanName("Sir Bob Andrew Dole", initials_format="{first} {middle}")
284+
>>> name.initials()
285+
"B. A."
286+
>>> name = HumanName("Doe, John A.", initials_delimiter="", initials_separator="")
287+
>>> name.initials()
288+
"J A D"
281289
"""
282290

283291
first_initials_list = [self.__process_initial__(name, True) for name in self.first_list if name]
@@ -289,11 +297,11 @@ def initials(self) -> str:
289297
# output. A fully-empty result falls back to empty_attribute_default,
290298
# matching the other attribute accessors (e.g. ``first``).
291299
initials_dict = {
292-
"first": (self.initials_delimiter + " ").join(first_initials_list) + self.initials_delimiter
300+
"first": (self.initials_delimiter + self.initials_separator).join(first_initials_list) + self.initials_delimiter
293301
if len(first_initials_list) else "",
294-
"middle": (self.initials_delimiter + " ").join(middle_initials_list) + self.initials_delimiter
302+
"middle": (self.initials_delimiter + self.initials_separator).join(middle_initials_list) + self.initials_delimiter
295303
if len(middle_initials_list) else "",
296-
"last": (self.initials_delimiter + " ").join(last_initials_list) + self.initials_delimiter
304+
"last": (self.initials_delimiter + self.initials_separator).join(last_initials_list) + self.initials_delimiter
297305
if len(last_initials_list) else ""
298306
}
299307

tests/test_initials.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ def test_initials_delimiter_constants(self) -> None:
7474
self.m(hn.initials(), "J; A; K; D;", hn)
7575
CONSTANTS.initials_delimiter = _orig
7676

77+
def test_initials_separator_default_on_constants(self) -> None:
78+
from nameparser.config import CONSTANTS
79+
self.assertEqual(CONSTANTS.initials_separator, " ")
80+
7781
def test_initials_list(self) -> None:
7882
hn = HumanName("Andrew Boris Petersen")
7983
self.m(hn.initials_list(), ["A", "B", "P"], hn)
@@ -90,6 +94,30 @@ def test_initials_with_prefix(self) -> None:
9094
hn = HumanName("Alex van Johnson")
9195
self.m(hn.initials_list(), ["A", "J"], hn)
9296

97+
def test_initials_delimiter_empty_string_kwarg(self) -> None:
98+
# Regression: initials_delimiter='' was silently ignored due to `or` defaulting
99+
hn = HumanName("Doe, John A.", initials_delimiter="")
100+
self.m(hn.initials(), "J A D", hn)
101+
102+
def test_initials_format_empty_string_kwarg(self) -> None:
103+
# Regression: initials_format='' was silently ignored due to `or` defaulting
104+
hn = HumanName("Doe, John A.")
105+
hn2 = HumanName("Doe, John A.", initials_format="")
106+
assert hn.initials() != hn2.initials()
107+
# When format is empty string, result should be either "" or empty_attribute_default
108+
result = hn2.initials()
109+
assert result == "" or result == hn2.C.empty_attribute_default
110+
111+
def test_initials_separator_kwarg(self) -> None:
112+
# initials_separator="" with initials_format="{first}{middle}{last}" gives
113+
# period-separated initials with no spaces — a common academic citation style
114+
hn = HumanName(
115+
"Doe, John A. Kenneth",
116+
initials_separator="",
117+
initials_format="{first}{middle}{last}",
118+
)
119+
self.m(hn.initials(), "J.A.K.D.", hn)
120+
93121
def test_constructor_first(self) -> None:
94122
hn = HumanName(first="TheName")
95123
self.assertFalse(hn.unparsable)
@@ -126,3 +154,38 @@ def test_constructor_multiple(self) -> None:
126154
self.m(hn.first, "TheName", hn)
127155
self.m(hn.last, "lastname", hn)
128156
self.m(hn.title, "mytitle", hn)
157+
158+
def test_initials_separator_multiword_name_part(self) -> None:
159+
# __process_initial__ splits on spaces internally for multi-word tokens;
160+
# initials_separator must flow through there too.
161+
hn = HumanName("", constants=None)
162+
hn.C.initials_separator = ""
163+
# Directly exercise __process_initial__ with a two-word part
164+
result = hn.__process_initial__("Van Berg", firstname=True)
165+
self.assertEqual(result, "VB")
166+
167+
def test_initials_separator_empty_multi_part_middle(self) -> None:
168+
# Full workflow from issue #152: empty delimiter + separator + compact format
169+
# gives fully concatenated initials with no spaces or punctuation.
170+
# Spaces between groups come from initials_format, so that must also be set.
171+
hn = HumanName(
172+
"Doe, John A. Kenneth",
173+
initials_delimiter="",
174+
initials_separator="",
175+
initials_format="{first}{middle}{last}",
176+
)
177+
self.m(hn.initials(), "JAKD", hn)
178+
179+
def test_initials_separator_constants_multi_part_middle(self) -> None:
180+
from nameparser.config import CONSTANTS
181+
_orig_d = CONSTANTS.initials_delimiter
182+
_orig_s = CONSTANTS.initials_separator
183+
_orig_f = CONSTANTS.initials_format
184+
CONSTANTS.initials_delimiter = ""
185+
CONSTANTS.initials_separator = ""
186+
CONSTANTS.initials_format = "{first}{middle}{last}"
187+
hn = HumanName("Doe, John A. Kenneth")
188+
self.m(hn.initials(), "JAKD", hn)
189+
CONSTANTS.initials_delimiter = _orig_d
190+
CONSTANTS.initials_separator = _orig_s
191+
CONSTANTS.initials_format = _orig_f

0 commit comments

Comments
 (0)