python-intl 0.5.0__tar.gz → 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-intl
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Python implementation of the Intl JavaScript API
5
5
  Author: David Danier
6
6
  Author-email: David Danier <david.danier@gmail.com>
@@ -18,8 +18,7 @@ API similar to what the [`Intl` JavaScript API](https://developer.mozilla.org/en
18
18
  provides. It is not meant to fully behave the same, but instead be close enough so
19
19
  the "same" code works on JavaScript and Python, with similar results.
20
20
 
21
- **Status:** This is very much work in progress. Currently only `Intl.DateTimeFormat`
22
- exists and this also is not complete yet.
21
+ **Status:** This is very much work in progress.
23
22
 
24
23
  **Note:** There are a lot of tests running the Python implementation against the
25
24
  JavaScript one and comparing the results. In general things should be pretty
@@ -86,6 +85,16 @@ formatter.format_range(datetime, datetime_till)
86
85
  | `DateTimeFormat.formatRangeToParts` | ✅ | `DateTimeFormat.format_range_to_parts` |
87
86
  | `DateTimeFormat.resolvedOptions` | ❌ | |
88
87
 
88
+ ### `Intl.Collator`
89
+
90
+ #### Compatibility
91
+
92
+ | Method | Status | Python name |
93
+ | ------------------ | :----: | ----------- |
94
+ | `Collator.compare` | ✅ | |
95
+
96
+ **Note:** Not all options are currently supported.
97
+
89
98
  ## Installation
90
99
 
91
100
  Be sure to be able to install `PyICU`, see the installation docs there:
@@ -5,8 +5,7 @@ API similar to what the [`Intl` JavaScript API](https://developer.mozilla.org/en
5
5
  provides. It is not meant to fully behave the same, but instead be close enough so
6
6
  the "same" code works on JavaScript and Python, with similar results.
7
7
 
8
- **Status:** This is very much work in progress. Currently only `Intl.DateTimeFormat`
9
- exists and this also is not complete yet.
8
+ **Status:** This is very much work in progress.
10
9
 
11
10
  **Note:** There are a lot of tests running the Python implementation against the
12
11
  JavaScript one and comparing the results. In general things should be pretty
@@ -73,6 +72,16 @@ formatter.format_range(datetime, datetime_till)
73
72
  | `DateTimeFormat.formatRangeToParts` | ✅ | `DateTimeFormat.format_range_to_parts` |
74
73
  | `DateTimeFormat.resolvedOptions` | ❌ | |
75
74
 
75
+ ### `Intl.Collator`
76
+
77
+ #### Compatibility
78
+
79
+ | Method | Status | Python name |
80
+ | ------------------ | :----: | ----------- |
81
+ | `Collator.compare` | ✅ | |
82
+
83
+ **Note:** Not all options are currently supported.
84
+
76
85
  ## Installation
77
86
 
78
87
  Be sure to be able to install `PyICU`, see the installation docs there:
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-intl"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  description = "Python implementation of the Intl JavaScript API"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -21,13 +21,17 @@ dev = [
21
21
  "pyright>=1.1.411",
22
22
  "pytest>=9.1.1",
23
23
  "pytest-cov>=7.1.0",
24
+ "pytest-xdist>=3.8.0",
24
25
  "ruff>=0.16.3",
25
26
  "tox>=4.60.0",
26
27
  "ty>=0.0.72",
27
28
  ]
28
29
 
29
30
  [tool.pytest.ini_options]
30
- markers = ["unit: mark a test as a unit test"]
31
+ markers = [
32
+ "unit: mark a test as a unit test",
33
+ "node: mark tests jun against the node/JS implementation",
34
+ ]
31
35
 
32
36
  [tool.ruff]
33
37
  line-length = 115
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-intl"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  description = "Python implementation of the Intl JavaScript API"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -22,6 +22,7 @@ dev = [
22
22
  "pyright>=1.1.411",
23
23
  "pytest>=9.1.1",
24
24
  "pytest-cov>=7.1.0",
25
+ "pytest-xdist>=3.8.0",
25
26
  "ruff>=0.16.3",
26
27
  "tox>=4.60.0",
27
28
  "ty>=0.0.72",
@@ -30,6 +31,7 @@ dev = [
30
31
  [tool.pytest.ini_options]
31
32
  markers = [
32
33
  "unit: mark a test as a unit test",
34
+ "node: mark tests jun against the node/JS implementation",
33
35
  ]
34
36
 
35
37
  [tool.ruff]
@@ -1,3 +1,7 @@
1
+ from .collator import (
2
+ Collator as Collator,
3
+ CollatorOptions as CollatorOptions,
4
+ )
1
5
  from .datetimeformat import (
2
6
  DateTimeFormat as DateTimeFormat,
3
7
  DateTimeFormatOptions as DateTimeFormatOptions,
@@ -0,0 +1,3 @@
1
+ from typing import Literal
2
+
3
+ type LocaleMatcherT = Literal["best fit", "lookup"]
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ from functools import cached_property
5
+ from typing import TYPE_CHECKING, Literal
6
+
7
+ import icu # type: ignore[import-untyped]
8
+
9
+ if TYPE_CHECKING:
10
+ from typing import NotRequired, TypedDict
11
+
12
+
13
+ if TYPE_CHECKING:
14
+ from ._types import LocaleMatcherT
15
+
16
+ # type UsageT = Literal["sort", "search"]
17
+ # type CollationT = Literal["emoji", "pinyin", "stroke"]
18
+ type CaseFirstT = Literal["upper", "lower", "false"]
19
+ type SensitivityT = Literal["base", "accent", "case", "variant"]
20
+
21
+ type ComparisonResultT = Literal[-1, 0, 1]
22
+
23
+ # Important: Must be the same as CollatorOptions
24
+ # (nothing is required, as this will be used to construct a
25
+ # CollatorOptions instance, so default values apply then)
26
+ class CollatorOptionsDictT(TypedDict):
27
+ locale_matcher: NotRequired[LocaleMatcherT]
28
+ # usage: NotRequired[UsageT | None]
29
+ # collation: NotRequired[CollationT | None]
30
+ numeric: NotRequired[bool]
31
+ case_first: NotRequired[CaseFirstT]
32
+ sensitivity: NotRequired[SensitivityT]
33
+ ignore_punctuation: NotRequired[bool | None]
34
+
35
+
36
+ @dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
37
+ class CollatorOptions:
38
+ locale_matcher: LocaleMatcherT = "best fit"
39
+ # usage: UsageT | None = None
40
+ # collation: CollationT | None = None
41
+ numeric: bool = False
42
+ case_first: CaseFirstT = "false"
43
+ sensitivity: SensitivityT = "variant"
44
+ ignore_punctuation: bool | None = None
45
+
46
+ def to_json(self) -> dict[str, str | int]:
47
+ return {
48
+ k: v
49
+ for k, v in (
50
+ ("localeMatcher", self.locale_matcher),
51
+ # ("usage", self.usage),
52
+ # ("collation", self.collation),
53
+ ("numeric", self.numeric),
54
+ ("caseFirst", self.case_first),
55
+ ("sensitivity", self.sensitivity),
56
+ ("ignorePunctuation", self.ignore_punctuation),
57
+ )
58
+ if v is not None
59
+ }
60
+
61
+
62
+ _COLLATOR_RESULT_TO_RESULT: dict[icu.UCollationResult, ComparisonResultT] = { # ty: ignore[unresolved-attribute]
63
+ icu.UCollationResult.LESS: -1, # ty: ignore[unresolved-attribute]
64
+ icu.UCollationResult.EQUAL: 0, # ty: ignore[unresolved-attribute]
65
+ icu.UCollationResult.GREATER: 1, # ty: ignore[unresolved-attribute]
66
+ }
67
+
68
+
69
+ class Collator:
70
+ locale: str
71
+ options: CollatorOptions
72
+
73
+ def __init__(
74
+ self,
75
+ locale: str,
76
+ options: CollatorOptions | CollatorOptionsDictT | None = None,
77
+ ) -> None:
78
+ self.locale = locale
79
+ if options is None:
80
+ self.options = CollatorOptions()
81
+ elif isinstance(options, CollatorOptions):
82
+ self.options = options
83
+ else:
84
+ self.options = CollatorOptions(**options)
85
+
86
+ @cached_property
87
+ def _icu_locale(self) -> icu.Locale: # ty: ignore[unresolved-attribute]
88
+ return icu.Locale(self.locale) # ty: ignore[unresolved-attribute]
89
+
90
+ @cached_property
91
+ def _icu_collator(self) -> icu.Collator: # ty: ignore[unresolved-attribute]
92
+ collator = icu.Collator.createInstance(self._icu_locale) # ty: ignore[unresolved-attribute]
93
+
94
+ if self.options.numeric:
95
+ collator.setAttribute(icu.UCollAttribute.NUMERIC_COLLATION, icu.UCollAttributeValue.ON) # ty: ignore[unresolved-attribute]
96
+
97
+ if (
98
+ self.options.ignore_punctuation
99
+ or (
100
+ self.options.ignore_punctuation is None
101
+ and self._icu_locale.getLanguage() == "th"
102
+ )
103
+ ):
104
+ collator.setAttribute(icu.UCollAttribute.ALTERNATE_HANDLING, icu.UCollAttributeValue.SHIFTED) # ty: ignore[unresolved-attribute]
105
+
106
+ match self.options.case_first:
107
+ case "upper":
108
+ collator.setAttribute(icu.UCollAttribute.CASE_FIRST, icu.UCollAttributeValue.UPPER_FIRST) # ty: ignore[unresolved-attribute]
109
+ case "lower":
110
+ collator.setAttribute(icu.UCollAttribute.CASE_FIRST, icu.UCollAttributeValue.LOWER_FIRST) # ty: ignore[unresolved-attribute]
111
+
112
+ collator.setAttribute(icu.UCollAttribute.NORMALIZATION_MODE, icu.UCollAttributeValue.ON) # ty: ignore[unresolved-attribute]
113
+ match self.options.sensitivity:
114
+ case "base":
115
+ collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.PRIMARY) # ty: ignore[unresolved-attribute]
116
+ case "accent":
117
+ collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.SECONDARY) # ty: ignore[unresolved-attribute]
118
+ case "case":
119
+ collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.TERTIARY) # ty: ignore[unresolved-attribute]
120
+ case "variant":
121
+ collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.QUATERNARY) # ty: ignore[unresolved-attribute]
122
+
123
+ return collator
124
+
125
+ def compare(self, string_a: str, string_b: str) -> ComparisonResultT:
126
+ return _COLLATOR_RESULT_TO_RESULT[self._icu_collator.compare(string_a, string_b)]
@@ -13,7 +13,8 @@ if TYPE_CHECKING:
13
13
 
14
14
 
15
15
  if TYPE_CHECKING:
16
- type LocaleMatcherT = Literal["best fit", "lookup"]
16
+ from ._types import LocaleMatcherT
17
+
17
18
  type Hour12T = bool | None
18
19
  type HourCycleT = Literal["h11", "h12", "h23", "h24"] | None
19
20
 
@@ -136,6 +137,10 @@ _TIMEZONE_NAME_TO_JSON_MAP: dict[str | None, str] = {
136
137
  "short_generic": "shortGeneric",
137
138
  "long_generic": "longGeneric",
138
139
  }
140
+ _SOURCE_TO_JSON_MAP: dict[str, str] = {
141
+ "start_range": "startRange",
142
+ "end_range": "endRange",
143
+ }
139
144
 
140
145
 
141
146
  @dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
@@ -340,10 +345,7 @@ class DateTimeIntervalPatternPart:
340
345
  return {
341
346
  "type": _COMPONENT_TO_JSON_MAP.get(self.type, self.type),
342
347
  "value": self.value,
343
- "source": {
344
- "start_range": "startRange",
345
- "end_range": "endRange",
346
- }.get(self.source, self.source),
348
+ "source": _SOURCE_TO_JSON_MAP.get(self.source, self.source),
347
349
  }
348
350
 
349
351
 
@@ -402,10 +404,12 @@ class DateTimeFormat:
402
404
  def __init__(
403
405
  self,
404
406
  locale: str,
405
- options: DateTimeFormatOptions | DateTimeFormatOptionsDictT,
407
+ options: DateTimeFormatOptions | DateTimeFormatOptionsDictT | None = None,
406
408
  ) -> None:
407
409
  self.locale = locale
408
- if isinstance(options, DateTimeFormatOptions):
410
+ if options is None:
411
+ self.options = DateTimeFormatOptions()
412
+ elif isinstance(options, DateTimeFormatOptions):
409
413
  self.options = options
410
414
  else:
411
415
  self.options = DateTimeFormatOptions(**options)
File without changes