Skip to content

Commit f73bf88

Browse files
eemeliflodolo
andauthored
Drop support for Python < 3.9 (#200)
* style: Apply pyupgrade * Drop typing-extensions dependency as unnecessary * Advertise support & add CI tests for Python 3.13 and 3.14 --------- Co-authored-by: Francesco Lodolo <[email protected]>
1 parent bdfa9b4 commit f73bf88

File tree

18 files changed

+152
-414
lines changed

18 files changed

+152
-414
lines changed

.github/workflows/ci.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
- uses: actions/checkout@v6
1515
- uses: astral-sh/setup-uv@v7
1616
with:
17-
python-version: 3.8
17+
python-version: 3.9
1818
- uses: actions/setup-python@v6
1919
- run: uv sync --dev --all-packages
2020
- run: uv run python -m flake8
@@ -23,8 +23,8 @@ jobs:
2323
runs-on: ${{ matrix.os }}
2424
strategy:
2525
matrix:
26-
os: [ubuntu-22.04, windows-2022]
27-
python-version: [3.8, 3.9, "3.10", 3.11, 3.12, pypy3.9, pypy3.10]
26+
os: [ubuntu-latest, windows-latest, macos-latest]
27+
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
2828
steps:
2929
- uses: actions/checkout@v6
3030
- uses: astral-sh/setup-uv@v7
@@ -48,8 +48,8 @@ jobs:
4848
- uses: actions/checkout@v6
4949
- uses: astral-sh/setup-uv@v7
5050
with:
51-
python-version: 3.8
52-
- run: uv venv --python 3.8
51+
python-version: 3.12
52+
- run: uv venv
5353
- run: uv pip install ${{ matrix.fluent-syntax }}
5454
- run: uv pip install ./fluent.runtime
5555
- run: uv run python -m unittest discover -s fluent.runtime
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
from typing import Callable, Dict
1+
from typing import Callable
22

33
from .types import FluentType, fluent_date, fluent_number
44

55
NUMBER = fluent_number
66
DATETIME = fluent_date
77

88

9-
BUILTINS: Dict[str, Callable[..., FluentType]] = {
9+
BUILTINS: dict[str, Callable[..., FluentType]] = {
1010
"NUMBER": NUMBER,
1111
"DATETIME": DATETIME,
1212
}

fluent.runtime/fluent/runtime/bundle.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Tuple, Union, cast
1+
from typing import TYPE_CHECKING, Any, Callable, Literal, Union, cast
22

33
import babel
44
import babel.numbers
55
import babel.plural
66
from fluent.syntax import ast as FTL
7-
from typing_extensions import Literal
87

98
from .builtins import BUILTINS
109
from .prepare import Compiler
@@ -34,16 +33,16 @@ class FluentBundle:
3433

3534
def __init__(
3635
self,
37-
locales: List[str],
38-
functions: Union[Dict[str, Callable[..., "FluentType"]], None] = None,
36+
locales: list[str],
37+
functions: Union[dict[str, Callable[..., "FluentType"]], None] = None,
3938
use_isolating: bool = True,
4039
):
4140
self.locales = locales
4241
self._functions = {**BUILTINS, **(functions or {})}
4342
self.use_isolating = use_isolating
44-
self._messages: Dict[str, Union[FTL.Message, FTL.Term]] = {}
45-
self._terms: Dict[str, Union[FTL.Message, FTL.Term]] = {}
46-
self._compiled: Dict[str, Message] = {}
43+
self._messages: dict[str, Union[FTL.Message, FTL.Term]] = {}
44+
self._terms: dict[str, Union[FTL.Message, FTL.Term]] = {}
45+
self._compiled: dict[str, Message] = {}
4746
# The compiler is not typed, and this cast is only valid for the public API
4847
self._compiler = cast(
4948
Callable[[Union[FTL.Message, FTL.Term]], Message], Compiler()
@@ -90,8 +89,8 @@ def _lookup(self, entry_id: str, term: bool = False) -> Message:
9089
return self._compiled[compiled_id]
9190

9291
def format_pattern(
93-
self, pattern: Pattern, args: Union[Dict[str, Any], None] = None
94-
) -> Tuple[Union[str, "FluentNone"], List[Exception]]:
92+
self, pattern: Pattern, args: Union[dict[str, Any], None] = None
93+
) -> tuple[Union[str, "FluentNone"], list[Exception]]:
9594
if args is not None:
9695
fluent_args = {
9796
argname: native_to_fluent(argvalue)
@@ -100,7 +99,7 @@ def format_pattern(
10099
else:
101100
fluent_args = {}
102101

103-
errors: List[Exception] = []
102+
errors: list[Exception] = []
104103
env = ResolverEnvironment(
105104
context=self, current=CurrentEnvironment(args=fluent_args), errors=errors
106105
)

fluent.runtime/fluent/runtime/fallback.py

Lines changed: 17 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,7 @@
11
import codecs
22
import os
3-
from typing import (
4-
TYPE_CHECKING,
5-
Any,
6-
Callable,
7-
Dict,
8-
Generator,
9-
List,
10-
Type,
11-
Union,
12-
cast,
13-
)
3+
from collections.abc import Generator
4+
from typing import TYPE_CHECKING, Any, Callable, Union, cast
145

156
from fluent.syntax import FluentParser
167
from typing import NamedTuple
@@ -25,7 +16,7 @@
2516

2617
class FormattedMessage(NamedTuple):
2718
value: Union[str, None]
28-
attributes: Dict[str, str]
19+
attributes: dict[str, str]
2920

3021

3122
class FluentLocalization:
@@ -38,24 +29,24 @@ class FluentLocalization:
3829

3930
def __init__(
4031
self,
41-
locales: List[str],
42-
resource_ids: List[str],
32+
locales: list[str],
33+
resource_ids: list[str],
4334
resource_loader: "AbstractResourceLoader",
4435
use_isolating: bool = False,
45-
bundle_class: Type[FluentBundle] = FluentBundle,
46-
functions: Union[Dict[str, Callable[[Any], "FluentType"]], None] = None,
36+
bundle_class: type[FluentBundle] = FluentBundle,
37+
functions: Union[dict[str, Callable[[Any], "FluentType"]], None] = None,
4738
):
4839
self.locales = locales
4940
self.resource_ids = resource_ids
5041
self.resource_loader = resource_loader
5142
self.use_isolating = use_isolating
5243
self.bundle_class = bundle_class
5344
self.functions = functions
54-
self._bundle_cache: List[FluentBundle] = []
45+
self._bundle_cache: list[FluentBundle] = []
5546
self._bundle_it = self._iterate_bundles()
5647

5748
def format_message(
58-
self, msg_id: str, args: Union[Dict[str, Any], None] = None
49+
self, msg_id: str, args: Union[dict[str, Any], None] = None
5950
) -> FormattedMessage:
6051
bundle, msg = next((
6152
(bundle, bundle.get_message(msg_id))
@@ -82,7 +73,7 @@ def format_message(
8273
)
8374

8475
def format_value(
85-
self, msg_id: str, args: Union[Dict[str, Any], None] = None
76+
self, msg_id: str, args: Union[dict[str, Any], None] = None
8677
) -> str:
8778
bundle, msg = next((
8879
(bundle, bundle.get_message(msg_id))
@@ -96,7 +87,7 @@ def format_value(
9687
str, val
9788
) # Never FluentNone when format_pattern called externally
9889

99-
def _create_bundle(self, locales: List[str]) -> FluentBundle:
90+
def _create_bundle(self, locales: list[str]) -> FluentBundle:
10091
return self.bundle_class(
10192
locales, functions=self.functions, use_isolating=self.use_isolating
10293
)
@@ -128,8 +119,8 @@ class AbstractResourceLoader:
128119
"""
129120

130121
def resources(
131-
self, locale: str, resource_ids: List[str]
132-
) -> Generator[List["Resource"], None, None]:
122+
self, locale: str, resource_ids: list[str]
123+
) -> Generator[list["Resource"], None, None]:
133124
"""
134125
Yield lists of FluentResource objects, corresponding to
135126
each of the resource_ids.
@@ -151,18 +142,18 @@ class FluentResourceLoader(AbstractResourceLoader):
151142
different roots.
152143
"""
153144

154-
def __init__(self, roots: Union[str, List[str]]):
145+
def __init__(self, roots: Union[str, list[str]]):
155146
"""
156147
Create a resource loader. The roots may be a string for a single
157148
location on disk, or a list of strings.
158149
"""
159150
self.roots = [roots] if isinstance(roots, str) else roots
160151

161152
def resources(
162-
self, locale: str, resource_ids: List[str]
163-
) -> Generator[List["Resource"], None, None]:
153+
self, locale: str, resource_ids: list[str]
154+
) -> Generator[list["Resource"], None, None]:
164155
for root in self.roots:
165-
resources: List[Any] = []
156+
resources: list[Any] = []
166157
for resource_id in resource_ids:
167158
path = self.localize_path(os.path.join(root, resource_id), locale)
168159
if not os.path.isfile(path):

fluent.runtime/fluent/runtime/prepare.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict, List
1+
from typing import Any
22

33
from fluent.syntax import ast as FTL
44

@@ -17,7 +17,7 @@ def compile(self, node: Any) -> Any:
1717
nodename: str = type(node).__name__
1818
if not hasattr(resolver, nodename):
1919
return node
20-
kwargs: Dict[str, Any] = vars(node).copy()
20+
kwargs: dict[str, Any] = vars(node).copy()
2121
for propname, propvalue in kwargs.items():
2222
kwargs[propname] = self(propvalue)
2323
handler = getattr(self, "compile_" + nodename, self.compile_generic)
@@ -31,7 +31,7 @@ def compile_Placeable(self, _: Any, expression: Any, **kwargs: Any) -> Any:
3131
return expression
3232
return resolver.Placeable(expression=expression, **kwargs)
3333

34-
def compile_Pattern(self, _: Any, elements: List[Any], **kwargs: Any) -> Any:
34+
def compile_Pattern(self, _: Any, elements: list[Any], **kwargs: Any) -> Any:
3535
if len(elements) == 1 and isinstance(elements[0], resolver.Placeable):
3636
# Don't isolate isolated placeables
3737
return resolver.NeverIsolatingPlaceable(elements[0].expression)

fluent.runtime/fluent/runtime/resolver.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import contextlib
2-
from typing import TYPE_CHECKING, Any, Dict, Generator, List, Set, Union, cast
2+
from collections.abc import Generator
3+
from typing import TYPE_CHECKING, Any, Union, cast
34

45
import attr
56
from fluent.syntax import ast as FTL
@@ -42,7 +43,7 @@ class CurrentEnvironment:
4243
# For Messages, VariableReference nodes are interpreted as external args,
4344
# but for Terms they are the values explicitly passed using CallExpression
4445
# syntax. So we have to be able to change 'args' for this purpose.
45-
args: Dict[str, Any] = attr.ib(factory=dict)
46+
args: dict[str, Any] = attr.ib(factory=dict)
4647
# This controls whether we need to report an error if a VariableReference
4748
# refers to an arg that is not present in the args dict.
4849
error_for_missing_arg: bool = attr.ib(default=True)
@@ -51,9 +52,9 @@ class CurrentEnvironment:
5152
@attr.s
5253
class ResolverEnvironment:
5354
context: "FluentBundle" = attr.ib()
54-
errors: List[Exception] = attr.ib()
55+
errors: list[Exception] = attr.ib()
5556
part_count: int = attr.ib(default=0, init=False)
56-
active_patterns: Set[FTL.Pattern] = attr.ib(factory=set, init=False)
57+
active_patterns: set[FTL.Pattern] = attr.ib(factory=set, init=False)
5758
current: CurrentEnvironment = attr.ib(factory=CurrentEnvironment)
5859

5960
@contextlib.contextmanager
@@ -72,7 +73,7 @@ def modified(
7273
self.current = old_current
7374

7475
def modified_for_term_reference(
75-
self, args: Union[Dict[str, Any], None] = None
76+
self, args: Union[dict[str, Any], None] = None
7677
) -> Any:
7778
return self.modified(
7879
args=args if args is not None else {}, error_for_missing_arg=False
@@ -100,13 +101,13 @@ class Literal(BaseResolver):
100101
class Message(FTL.Entry, BaseResolver):
101102
id: "Identifier"
102103
value: Union["Pattern", None]
103-
attributes: Dict[str, "Pattern"]
104+
attributes: dict[str, "Pattern"]
104105

105106
def __init__(
106107
self,
107108
id: "Identifier",
108109
value: Union["Pattern", None] = None,
109-
attributes: Union[List["Attribute"], None] = None,
110+
attributes: Union[list["Attribute"], None] = None,
110111
comment: Any = None,
111112
**kwargs: Any,
112113
):
@@ -121,13 +122,13 @@ def __init__(
121122
class Term(FTL.Entry, BaseResolver):
122123
id: "Identifier"
123124
value: "Pattern"
124-
attributes: Dict[str, "Pattern"]
125+
attributes: dict[str, "Pattern"]
125126

126127
def __init__(
127128
self,
128129
id: "Identifier",
129130
value: "Pattern",
130-
attributes: Union[List["Attribute"], None] = None,
131+
attributes: Union[list["Attribute"], None] = None,
131132
comment: Any = None,
132133
**kwargs: Any,
133134
):
@@ -143,7 +144,7 @@ class Pattern(FTL.Pattern, BaseResolver):
143144
# Prevent messages with too many sub parts, for CPI DOS protection
144145
MAX_PARTS = 1000
145146

146-
elements: List[Union["TextElement", "Placeable"]] # type: ignore
147+
elements: list[Union["TextElement", "Placeable"]] # type: ignore
147148

148149
def __init__(self, *args: Any, **kwargs: Any):
149150
super().__init__(*args, **kwargs)
@@ -294,7 +295,7 @@ def __call__(self, env: ResolverEnvironment) -> Any:
294295
if isinstance(arg_val, (FluentType, str)):
295296
return arg_val
296297
env.errors.append(
297-
TypeError("Unsupported external type: {}, {}".format(name, type(arg_val)))
298+
TypeError(f"Unsupported external type: {name}, {type(arg_val)}")
298299
)
299300
return FluentNone(name)
300301

@@ -306,7 +307,7 @@ class Attribute(FTL.Attribute, BaseResolver):
306307

307308
class SelectExpression(FTL.SelectExpression, BaseResolver):
308309
selector: "InlineExpression"
309-
variants: List["Variant"] # type: ignore
310+
variants: list["Variant"] # type: ignore
310311

311312
def __call__(self, env: ResolverEnvironment) -> Union[str, FluentNone]:
312313
key = self.selector(env)
@@ -368,8 +369,8 @@ def __call__(self, env: ResolverEnvironment) -> str:
368369

369370

370371
class CallArguments(FTL.CallArguments, BaseResolver):
371-
positional: List[Union["InlineExpression", Placeable]] # type: ignore
372-
named: List["NamedArgument"] # type: ignore
372+
positional: list[Union["InlineExpression", Placeable]] # type: ignore
373+
named: list["NamedArgument"] # type: ignore
373374

374375

375376
class FunctionReference(FTL.FunctionReference, BaseResolver):
@@ -384,7 +385,7 @@ def __call__(self, env: ResolverEnvironment) -> Any:
384385
function = env.context._functions[function_name]
385386
except LookupError:
386387
env.errors.append(
387-
FluentReferenceError("Unknown function: {}".format(function_name))
388+
FluentReferenceError(f"Unknown function: {function_name}")
388389
)
389390
return FluentNone(function_name + "()")
390391

0 commit comments

Comments
 (0)