python-intl 0.5.0__tar.gz → 0.7.1__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.7.1
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,36 @@ 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
+ #### Example usage
91
+
92
+ ```python
93
+ import python_intl as Intl
94
+
95
+ collator = Intl.Collator("de-DE", {"numeric": True})
96
+ collator.compare("10", "9")
97
+ # Result = 1, which means 9 comes before 10
98
+
99
+ # You can also use sorted, although this is not available in JavaScript
100
+ collator.sorted(["10", "9"])
101
+ # Result = ['9', '10']
102
+ ```
103
+
104
+ #### Compatibility
105
+
106
+ | Method | Status | Python name |
107
+ | ------------------ | :----: | ----------- |
108
+ | `Collator.compare` | ✅ | |
109
+
110
+ **Note:** Not all options are currently supported.
111
+
112
+ #### Additional methods
113
+
114
+ * `Collator.sorted`: Somewhat like `sorted`, but uses the collator for comparison. If you want
115
+ to sort complex datastructures you can provide a `key` parameter (like with `sorted`) to
116
+ get a comparable string value (for example by returning an attribute).
117
+
89
118
  ## Installation
90
119
 
91
120
  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,36 @@ 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
+ #### Example usage
78
+
79
+ ```python
80
+ import python_intl as Intl
81
+
82
+ collator = Intl.Collator("de-DE", {"numeric": True})
83
+ collator.compare("10", "9")
84
+ # Result = 1, which means 9 comes before 10
85
+
86
+ # You can also use sorted, although this is not available in JavaScript
87
+ collator.sorted(["10", "9"])
88
+ # Result = ['9', '10']
89
+ ```
90
+
91
+ #### Compatibility
92
+
93
+ | Method | Status | Python name |
94
+ | ------------------ | :----: | ----------- |
95
+ | `Collator.compare` | ✅ | |
96
+
97
+ **Note:** Not all options are currently supported.
98
+
99
+ #### Additional methods
100
+
101
+ * `Collator.sorted`: Somewhat like `sorted`, but uses the collator for comparison. If you want
102
+ to sort complex datastructures you can provide a `key` parameter (like with `sorted`) to
103
+ get a comparable string value (for example by returning an attribute).
104
+
76
105
  ## Installation
77
106
 
78
107
  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.7.1"
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.7.1"
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,142 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import functools
5
+ from collections.abc import Callable, Iterable
6
+ from functools import cached_property
7
+ from typing import TYPE_CHECKING, Literal, overload
8
+
9
+ import icu # type: ignore[import-untyped]
10
+
11
+ if TYPE_CHECKING:
12
+ from typing import NotRequired, TypedDict
13
+
14
+
15
+ if TYPE_CHECKING:
16
+ from ._types import LocaleMatcherT
17
+
18
+ # type UsageT = Literal["sort", "search"]
19
+ # type CollationT = Literal["emoji", "pinyin", "stroke"]
20
+ type CaseFirstT = Literal["upper", "lower", "false"]
21
+ type SensitivityT = Literal["base", "accent", "case", "variant"]
22
+
23
+ type ComparisonResultT = Literal[-1, 0, 1]
24
+
25
+ # Important: Must be the same as CollatorOptions
26
+ # (nothing is required, as this will be used to construct a
27
+ # CollatorOptions instance, so default values apply then)
28
+ class CollatorOptionsDictT(TypedDict):
29
+ locale_matcher: NotRequired[LocaleMatcherT]
30
+ # usage: NotRequired[UsageT | None]
31
+ # collation: NotRequired[CollationT | None]
32
+ numeric: NotRequired[bool]
33
+ case_first: NotRequired[CaseFirstT]
34
+ sensitivity: NotRequired[SensitivityT]
35
+ ignore_punctuation: NotRequired[bool | None]
36
+
37
+
38
+ @dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
39
+ class CollatorOptions:
40
+ locale_matcher: LocaleMatcherT = "best fit"
41
+ # usage: UsageT | None = None
42
+ # collation: CollationT | None = None
43
+ numeric: bool = False
44
+ case_first: CaseFirstT = "false"
45
+ sensitivity: SensitivityT = "variant"
46
+ ignore_punctuation: bool | None = None
47
+
48
+ def to_json(self) -> dict[str, str | int]:
49
+ return {
50
+ k: v
51
+ for k, v in (
52
+ ("localeMatcher", self.locale_matcher),
53
+ # ("usage", self.usage),
54
+ # ("collation", self.collation),
55
+ ("numeric", self.numeric),
56
+ ("caseFirst", self.case_first),
57
+ ("sensitivity", self.sensitivity),
58
+ ("ignorePunctuation", self.ignore_punctuation),
59
+ )
60
+ if v is not None
61
+ }
62
+
63
+
64
+ _COLLATOR_RESULT_TO_RESULT: dict[icu.UCollationResult, ComparisonResultT] = { # ty: ignore[unresolved-attribute]
65
+ icu.UCollationResult.LESS: -1, # ty: ignore[unresolved-attribute]
66
+ icu.UCollationResult.EQUAL: 0, # ty: ignore[unresolved-attribute]
67
+ icu.UCollationResult.GREATER: 1, # ty: ignore[unresolved-attribute]
68
+ }
69
+
70
+
71
+ class Collator:
72
+ locale: str
73
+ options: CollatorOptions
74
+
75
+ def __init__(
76
+ self,
77
+ locale: str,
78
+ options: CollatorOptions | CollatorOptionsDictT | None = None,
79
+ ) -> None:
80
+ self.locale = locale
81
+ if options is None:
82
+ self.options = CollatorOptions()
83
+ elif isinstance(options, CollatorOptions):
84
+ self.options = options
85
+ else:
86
+ self.options = CollatorOptions(**options)
87
+
88
+ @cached_property
89
+ def _icu_locale(self) -> icu.Locale: # ty: ignore[unresolved-attribute]
90
+ return icu.Locale(self.locale) # ty: ignore[unresolved-attribute]
91
+
92
+ @cached_property
93
+ def _icu_collator(self) -> icu.Collator: # ty: ignore[unresolved-attribute]
94
+ collator = icu.Collator.createInstance(self._icu_locale) # ty: ignore[unresolved-attribute]
95
+
96
+ if self.options.numeric:
97
+ collator.setAttribute(icu.UCollAttribute.NUMERIC_COLLATION, icu.UCollAttributeValue.ON) # ty: ignore[unresolved-attribute]
98
+
99
+ if (
100
+ self.options.ignore_punctuation
101
+ or (
102
+ self.options.ignore_punctuation is None
103
+ and self._icu_locale.getLanguage() == "th"
104
+ )
105
+ ):
106
+ collator.setAttribute(icu.UCollAttribute.ALTERNATE_HANDLING, icu.UCollAttributeValue.SHIFTED) # ty: ignore[unresolved-attribute]
107
+
108
+ match self.options.case_first:
109
+ case "upper":
110
+ collator.setAttribute(icu.UCollAttribute.CASE_FIRST, icu.UCollAttributeValue.UPPER_FIRST) # ty: ignore[unresolved-attribute]
111
+ case "lower":
112
+ collator.setAttribute(icu.UCollAttribute.CASE_FIRST, icu.UCollAttributeValue.LOWER_FIRST) # ty: ignore[unresolved-attribute]
113
+
114
+ collator.setAttribute(icu.UCollAttribute.NORMALIZATION_MODE, icu.UCollAttributeValue.ON) # ty: ignore[unresolved-attribute]
115
+ match self.options.sensitivity:
116
+ case "base":
117
+ collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.PRIMARY) # ty: ignore[unresolved-attribute]
118
+ case "accent":
119
+ collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.SECONDARY) # ty: ignore[unresolved-attribute]
120
+ case "case":
121
+ collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.TERTIARY) # ty: ignore[unresolved-attribute]
122
+ case "variant":
123
+ collator.setAttribute(icu.UCollAttribute.STRENGTH, icu.UCollAttributeValue.QUATERNARY) # ty: ignore[unresolved-attribute]
124
+
125
+ return collator
126
+
127
+ def compare(self, string_a: str, string_b: str) -> ComparisonResultT:
128
+ return _COLLATOR_RESULT_TO_RESULT[self._icu_collator.compare(string_a, string_b)]
129
+
130
+ @overload
131
+ def sorted(self, items: Iterable[str], /, key: None = None) -> Iterable[str]: ...
132
+ @overload
133
+ def sorted[T](self, items: Iterable[T], /, key: Callable[[T], str]) -> Iterable[T]: ...
134
+ def sorted(self, items, /, key = None):
135
+ return sorted(
136
+ items,
137
+ key=functools.cmp_to_key(
138
+ (lambda a, b: self.compare(key(a), key(b)))
139
+ if key
140
+ else self.compare,
141
+ ),
142
+ )
@@ -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