Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/nameparser/_pipeline/_assemble.py: 95%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

22 statements  

1"""Not a stage: converts the final ParseState into a public ParsedName. 

2 

3Consumes: tokens (all roles set), dropped, ambiguities (by index). 

4Produces: a validated ParsedName -- the constructor re-checks every 

5invariant (span order/bounds, ambiguity subset), so a pipeline bug 

6that would produce an invalid result dies HERE, not in a renderer 

7three layers away. 

8 

9Structural tokens (dropped maiden markers) are omitted, like delimiter 

10characters. A main-stream token that somehow reaches here with no role 

11takes Role.GIVEN -- parse must never raise on the *content* of a name, 

12so the fallback is deliberately boring rather than an exception. This 

13should never fire in practice (assign/group must set a role on every 

14main-stream token); it exists as a last-resort safety net against a 

15pipeline bug, not as sanctioned behavior for real input -- see 

16test_assemble_falls_back_to_given_for_unassigned_role in 

17tests/v2/pipeline/test_assemble.py. 

18""" 

19from __future__ import annotations 

20 

21from nameparser._pipeline._state import ParseState 

22from nameparser._types import Ambiguity, ParsedName, Role, Token 

23 

24 

25# rules.md#A2: "a name with no name content parses to the empty 

26# name — every field empty, false as a boolean — while ambiguities 

27# born from its punctuation survive on the empty result" 

28# (history: decisions.md#A2) 

29def assemble(state: ParseState) -> ParsedName: 

30 dropped = set(state.dropped) 

31 final: dict[int, Token] = {} 

32 for i, t in enumerate(state.tokens): 

33 if i in dropped: 

34 continue 

35 role = t.role if t.role is not None else Role.GIVEN 

36 final[i] = Token(t.text, t.span, role, t.tags) 

37 # No alphanumeric character among the SURVIVING tokens means no 

38 # name: a bare '.' or '- -' is not a person. v1 kept such input 

39 # (parse('.') -> first '.'); 2.0 empties it so bool() stays an 

40 # honest "did I get a name?" check. isalnum() is Unicode-aware, so 

41 # every real name in any script has content and only pure 

42 # punctuation/symbols empty out. (Embedded junk in a name with 

43 # content -- 'John . Smith' -- is left alone: that parse is truthy, 

44 # so bool() is not misled.) 

45 # 

46 # This read "anywhere" until #329, and the two said the same thing 

47 # for as long as nothing could take a content-bearing token away. 

48 # Dropping a maiden marker inside a delimited clause can, and 

49 # "(née —)" under Policy(maiden_delimiters=...) is the input where 

50 # the readings part: the marker is the clause's only alnum token, 

51 # so once it goes structural the em dash is all that survives and 

52 # the whole parse empties -- where 1.4.0 and pre-#329 both gave 

53 # maiden 'née —'. Deliberate, not fallout: a dropped marker is 

54 # structural like a delimiter character, and brackets plus a marker 

55 # plus a dash name no one, exactly as "(-)" already named no one. 

56 # A guard keyed on what ELSE is in the clause would be a different 

57 # rule, and would leave maiden holding marker-plus-punctuation. 

58 # Pinned by cases.py's maiden_marker_delimited_content_free and, 

59 # for the other side of it -- the same clause inside a name, where 

60 # the em dash survives as the maiden value because Jane Smith 

61 # carries the content -- ..._content_free_in_a_name beside it. 

62 # 

63 # The TOKENS go, not the diagnostics: this drops the name, and 

64 # "was the input malformed?" is the one question still worth 

65 # answering about it -- most of all here, where there is no parse 

66 # left to infer it from. parse('(') keeps its unbalanced-delimiter 

67 # report, pointing at no token because no token survived. 

68 contentless = not any( 

69 c.isalnum() for t in final.values() for c in t.text) 

70 if contentless: 

71 final = {} 

72 ambiguities = [] 

73 for pending in state.ambiguities: 

74 materialized = tuple(final[i] for i in pending.indices 

75 if i in final) 

76 if pending.indices and not materialized and not contentless: 

77 # every referent was dropped: the ambiguity describes 

78 # nothing that survives assembly. Born-empty ambiguities 

79 # (unbalanced delimiters) are token-independent and kept. 

80 # Not so when the whole name was emptied just above -- the 

81 # referents did not lose a contest, they were discarded 

82 # wholesale, and the report still describes the input. 

83 continue 

84 ambiguities.append( 

85 Ambiguity(pending.kind, pending.detail, materialized)) 

86 return ParsedName(original=state.original, 

87 tokens=tuple(final.values()), 

88 ambiguities=tuple(ambiguities))