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
4 changes: 4 additions & 0 deletions docs/release_log.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
Release Log
===========
* 1.4.0 - Unreleased

- Fix the ``"Lastname, Firstname"`` comma format not being recognized when the input uses the Arabic comma ``،`` (U+060C, the standard comma in Arabic/Persian/Urdu text) or the fullwidth CJK comma ```` (U+FF0C) instead of the ASCII comma: both variants now also split the format and no longer leak into the parsed output (closes #265)

* 1.3.1 - July 11, 2026

- Fix invisible Unicode bidirectional control characters (LRM/RLM/ALM, the embedding/override marks, and the isolates U+2066–U+2069) surviving parsing and sticking to ``first``/``last``/etc., so a copy-pasted right-to-left name silently failed equality and dedup. They are now stripped in preprocessing like emoji; disable via ``CONSTANTS.regexes.bidi = False`` (closes #266)
Expand Down
5 changes: 3 additions & 2 deletions nameparser/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -590,8 +590,9 @@ class Constants:
``None`` (no additional splitting beyond the standard comma split).

Note: setting this to ``","`` or ``", "`` has no additional effect —
the full name is already split on bare commas first, and each resulting
part is stripped of surrounding whitespace before this step runs.
the full name is already split on comma characters first (including the
Arabic ``،`` and fullwidth ``,`` variants), and each resulting part is
stripped of surrounding whitespace before this step runs.

The delimiter is only applied to parts once they've been identified as
a suffix group, so it never leaks into a first- or middle-name part. For
Expand Down
4 changes: 4 additions & 0 deletions nameparser/config/regexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
"bidi": re_bidi,
"phd": re.compile(r'\s(ph\.?\s+d\.?)', re.I),
"space_before_comma": re.compile(r'\s+,'),
# ASCII comma plus its Arabic (U+060C) and fullwidth CJK (U+FF0C)
# counterparts, used to split "Last, First" format and to strip a
# trailing comma before parsing (#265).
"commas": re.compile(r'[,،,]'),
"east_slavic_patronymic": re.compile(
r'(ovich|ovna|evich|evna|ichna|ilyich|kuzmich|lukich|fomich|fokich)$',
re.I,
Expand Down
12 changes: 9 additions & 3 deletions nameparser/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -923,7 +923,7 @@ def _apply_full_name(self, value: str | bytes, *, stacklevel: int) -> None:
def collapse_whitespace(self, string: str) -> str:
# collapse multiple spaces into single space
string = self.C.regexes.spaces.sub(" ", string.strip())
if string.endswith(","):
if string and self.C.regexes.commas.fullmatch(string[-1]):
string = string[:-1]
return string

Expand Down Expand Up @@ -1195,8 +1195,14 @@ def parse_full_name(self) -> None:

self._full_name = self.collapse_whitespace(self._full_name)

# break up full_name by commas
parts = [x.strip() for x in self._full_name.split(",")]
# break up full_name by commas. A missing "commas" key in a custom
# regexes dict falls back to RegexTupleManager's EMPTY_REGEX, whose
# .split() matches between every character rather than not
# splitting at all -- guard against that so a custom regexes dict
# that omits "commas" disables the comma split instead of shattering
# the name into single characters.
commas = self.C.regexes.commas
parts = [x.strip() for x in (commas.split(self._full_name) if commas.pattern else [self._full_name])]
self._had_comma = len(parts) > 1

log.debug("full_name: %s", self._full_name)
Expand Down
47 changes: 47 additions & 0 deletions tests/test_comma_variants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from nameparser import HumanName
from nameparser.config import Constants
from nameparser.config.regexes import REGEXES

from tests.base import HumanNameTestBase


class HumanNameCommaVariantsTests(HumanNameTestBase):
"""Non-ASCII comma characters should split "Last, First" the same as ',' (#265)."""

def test_arabic_comma_splits_lastname_format(self) -> None:
hn = HumanName("سلمان، محمد")
self.m(hn.first, "محمد", hn)
self.m(hn.last, "سلمان", hn)

def test_fullwidth_comma_splits_lastname_format(self) -> None:
hn = HumanName("Smith,John")
self.m(hn.first, "John", hn)
self.m(hn.last, "Smith", hn)

def test_arabic_comma_does_not_pollute_output(self) -> None:
hn = HumanName("سلمان، محمد")
self.assertNotIn("،", hn.last)
self.assertNotIn("،", str(hn))

def test_trailing_arabic_comma_stripped(self) -> None:
# matches ASCII behavior: a single word with a trailing comma has
# nothing after the comma, so it's a bare name, not "Last,"
hn = HumanName("سلمان،")
self.m(hn.first, "سلمان", hn)

def test_custom_regexes_without_commas_key_does_not_shatter_name(self) -> None:
# A custom regexes dict that omits "commas" entirely must not fall
# back to RegexTupleManager's EMPTY_REGEX default for splitting --
# re.compile('').split(...) matches between every character, which
# explodes any name into single-char pieces instead of leaving it
# unsplit (the EMPTY_REGEX convention elsewhere in this codebase
# means "feature disabled", not "split on every character").
# With comma splitting disabled, "Smith, John" is tokenized like any
# other no-comma input (word tokenizing drops the punctuation),
# yielding a plain first/last pair -- not the inverted "Last, First"
# reading, and definitely not single-character pieces.
custom = {k: v for k, v in REGEXES.items() if k != 'commas'}
c = Constants(regexes=custom)
hn = HumanName("Smith, John", constants=c)
self.m(hn.first, "Smith", hn)
self.m(hn.last, "John", hn)
Loading