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

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

24 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 ( 

23 Ambiguity, AmbiguityKind, ParsedName, Role, Token, 

24) 

25 

26 

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

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

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

30# (history: decisions.md#A2) 

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

32 dropped = set(state.dropped) 

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

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

35 if i in dropped: 

36 continue 

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

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

39 # No alphanumeric character among the SURVIVING tokens means no 

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

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

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

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

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

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

46 # so bool() is not misled.) 

47 # 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

64 # 

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

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

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

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

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

70 contentless = not any( 

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

72 if contentless: 

73 final = {} 

74 ambiguities = [] 

75 for pending in state.ambiguities: 

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

77 if i in final) 

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

79 # every referent was dropped: the ambiguity describes 

80 # nothing that survives assembly. Born-empty ambiguities 

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

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

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

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

85 continue 

86 # rules.md#A1: "a report names the reading the parse took, so 

87 # a report whose fork the rest of the parse then resolved to 

88 # NEITHER branch is withdrawn rather than carried beside a 

89 # reading it contradicts". classify's connective-or-initial 

90 # fork offers exactly two readings and its detail says which 

91 # it took ("it is read as an initial"); a generation and an 

92 # honorific are neither. So where the parse goes on to role 

93 # that letter SUFFIX or TITLE the report is false on its 

94 # face, and 'JOHN QUINCY SMITH I' carried it beside a 

95 # suffix-or-name saying the same token reads as a 

96 # generational suffix (#397 second review). 'i' is the first 

97 # word that is both a marked connective and suffix 

98 # vocabulary, so no parse before this cycle could reach the 

99 # shape. 

100 # 

101 # WITHDRAWN HERE and not emitted later, which is the narrow 

102 # reading of mechanisms.md#AMBIGUITY-AT-THE-DECISION-SITE 

103 # rather than an exception to it: "Emit at the site that 

104 # takes the branch, not where an ambiguous tag sits" governs 

105 # the EMISSION, classify still owns the fork, and what this 

106 # drops is a report whose subject the parse went on to read 

107 # as something else. Here is the one place that knows -- 

108 # roles are final and nothing downstream moves them -- and 

109 # the loop already withdraws a report whose referents did not 

110 # survive, just above. 

111 if (pending.kind is AmbiguityKind.CONJUNCTION_OR_INITIAL 

112 and any(t.role in (Role.SUFFIX, Role.TITLE) 

113 for t in materialized)): 

114 continue 

115 ambiguities.append( 

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

117 return ParsedName(original=state.original, 

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

119 ambiguities=tuple(ambiguities))