Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/nameparser/_locale.py: 53%

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

32 statements  

1"""The Locale type: a named delta over (Lexicon, Policy). 

2 

3A Locale dissolves at parser construction (parser_for, a later plan): 

4lexicon fragments union onto the base, the PolicyPatch folds via 

5apply_patch. Packs are pure data; they have no privileged capabilities. 

6 

7Layering: imports _lexicon and _policy only (enforced by 

8tests/v2/test_layering.py). 

9""" 

10from __future__ import annotations 

11 

12import dataclasses 

13import re 

14from dataclasses import dataclass 

15 

16from nameparser._lexicon import Lexicon 

17from nameparser._policy import UNSET, PolicyPatch 

18from nameparser._types import _guarded_getstate, _guarded_setstate 

19 

20 

21@dataclass(frozen=True, slots=True) 

22class Locale: 

23 """A named, shareable bundle of vocabulary and behavior for a 

24 naming tradition: a lexicon fragment plus a policy patch, applied 

25 together by :func:`nameparser.parser_for`. The packs shipped with 

26 nameparser live in :mod:`nameparser.locales`; building your own 

27 needs no registration -- construct one and pass it to 

28 ``parser_for``. See the :doc:`locale packs guide </locales>` for 

29 using, creating, and contributing packs.""" 

30 

31 #: Identifier, lowercase ``[a-z0-9_]+`` (e.g. "ru", "tr_az"). 

32 code: str 

33 #: Vocabulary ADDED to the base parser's lexicon (unioned; a pack 

34 #: never removes base vocabulary). 

35 lexicon: Lexicon 

36 #: Behavior changes folded onto the base policy (set-valued fields 

37 #: union; scalars override, later pack wins). 

38 policy: PolicyPatch = PolicyPatch() 

39 

40 # in the class body so @dataclass(slots=True) keeps them 

41 __getstate__ = _guarded_getstate 

42 __setstate__ = _guarded_setstate 

43 

44 def __post_init__(self) -> None: 

45 if not isinstance(self.code, str): 

46 raise TypeError( 

47 f"Locale.code must be a str, got {self.code!r}" 

48 ) 

49 if not self.code.strip(): 

50 raise ValueError( 

51 f"Locale.code must be a non-empty string, got {self.code!r}" 

52 ) 

53 if self.code != self.code.lower(): 

54 raise ValueError( 

55 f"Locale.code must be lowercase, got {self.code!r}" 

56 ) 

57 # Codes are registry keys (parser_for, third-party packs): every 

58 # accepted character is supported forever, so pin the charset 

59 # while relaxing later is still compatible. One separator only -- 

60 # allowing '-' too would make tr-az and tr_az distinct keys. 

61 if not re.fullmatch(r"[a-z0-9_]+", self.code): 

62 raise ValueError( 

63 f"Locale.code must match [a-z0-9_]+, got {self.code!r}" 

64 ) 

65 if not isinstance(self.lexicon, Lexicon): 

66 raise TypeError( 

67 f"Locale.lexicon must be a Lexicon, got {self.lexicon!r}" 

68 ) 

69 if not isinstance(self.policy, PolicyPatch): 

70 raise TypeError( 

71 f"Locale.policy must be a PolicyPatch, got {self.policy!r}" 

72 ) 

73 

74 def __repr__(self) -> str: 

75 # Bounded: shows the code and which Policy fields the patch sets, 

76 # never the Lexicon contents or the patched values themselves 

77 # (design rule, see nameparser._types module docstring). 

78 patched = [f.name for f in dataclasses.fields(self.policy) 

79 if getattr(self.policy, f.name) is not UNSET] 

80 suffix = f": {', '.join(patched)}" if patched else "" 

81 return f"Locale({self.code!r}{suffix})"