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.
- {python_intl-0.5.0 → python_intl-0.7.1}/PKG-INFO +32 -3
- {python_intl-0.5.0 → python_intl-0.7.1}/README.md +31 -2
- {python_intl-0.5.0 → python_intl-0.7.1}/pyproject.toml +6 -2
- {python_intl-0.5.0 → python_intl-0.7.1}/pyproject.toml.orig +3 -1
- {python_intl-0.5.0 → python_intl-0.7.1}/python_intl/__init__.py +4 -0
- python_intl-0.7.1/python_intl/_types.py +3 -0
- python_intl-0.7.1/python_intl/collator.py +142 -0
- {python_intl-0.5.0 → python_intl-0.7.1}/python_intl/datetimeformat.py +11 -7
- {python_intl-0.5.0 → python_intl-0.7.1}/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-intl
|
|
3
|
-
Version: 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.
|
|
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.
|
|
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.
|
|
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 = [
|
|
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.
|
|
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]
|
|
@@ -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
|
-
|
|
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
|
|
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
|