diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1313529b..1530ce0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,7 +26,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8, 3.9, "3.10", 3.11, 3.12, pypy3.9, pypy3.10] + python-version: [3.9, "3.10", 3.11, 3.12, pypy3.9, pypy3.10] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 @@ -43,19 +43,22 @@ jobs: # Test compatibility with the oldest Python version we claim to support, # and for fluent.runtime's compatibility with a range of fluent.syntax versions. compatibility: - runs-on: ubuntu-20.04 # https://github.com/actions/setup-python/issues/544 + runs-on: ubuntu-latest strategy: matrix: fluent-syntax: - - ./fluent.syntax - fluent.syntax==0.19.0 - fluent.syntax==0.18.1 six - fluent.syntax==0.17.0 six steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: - python-version: 3.6 + python-version: 3.9 + cache: pip + cache-dependency-path: | + fluent.syntax/setup.py + fluent.runtime/setup.py - run: python -m pip install ${{ matrix.fluent-syntax }} - run: python -m pip install ./fluent.runtime - run: python -m unittest discover -s fluent.runtime diff --git a/fluent.runtime/fluent/runtime/builtins.py b/fluent.runtime/fluent/runtime/builtins.py index cb16f9d8..781e6a88 100644 --- a/fluent.runtime/fluent/runtime/builtins.py +++ b/fluent.runtime/fluent/runtime/builtins.py @@ -1,4 +1,4 @@ -from typing import Any, Callable, Dict +from typing import Any, Callable from .types import FluentType, fluent_date, fluent_number @@ -6,7 +6,7 @@ DATETIME = fluent_date -BUILTINS: Dict[str, Callable[[Any], FluentType]] = { +BUILTINS: dict[str, Callable[[Any], FluentType]] = { "NUMBER": NUMBER, "DATETIME": DATETIME, } diff --git a/fluent.runtime/fluent/runtime/bundle.py b/fluent.runtime/fluent/runtime/bundle.py index 6a0f254d..d9e53807 100644 --- a/fluent.runtime/fluent/runtime/bundle.py +++ b/fluent.runtime/fluent/runtime/bundle.py @@ -1,10 +1,9 @@ -from typing import TYPE_CHECKING, Any, Callable, Dict, List, Tuple, Union, cast +from typing import TYPE_CHECKING, Any, Callable, Literal, Union, cast import babel import babel.numbers import babel.plural from fluent.syntax import ast as FTL -from typing_extensions import Literal from .builtins import BUILTINS from .prepare import Compiler @@ -34,16 +33,16 @@ class FluentBundle: def __init__( self, - locales: List[str], - functions: Union[Dict[str, Callable[[Any], "FluentType"]], None] = None, + locales: list[str], + functions: Union[dict[str, Callable[[Any], "FluentType"]], None] = None, use_isolating: bool = True, ): self.locales = locales self._functions = {**BUILTINS, **(functions or {})} self.use_isolating = use_isolating - self._messages: Dict[str, Union[FTL.Message, FTL.Term]] = {} - self._terms: Dict[str, Union[FTL.Message, FTL.Term]] = {} - self._compiled: Dict[str, Message] = {} + self._messages: dict[str, Union[FTL.Message, FTL.Term]] = {} + self._terms: dict[str, Union[FTL.Message, FTL.Term]] = {} + self._compiled: dict[str, Message] = {} # The compiler is not typed, and this cast is only valid for the public API self._compiler = cast( Callable[[Union[FTL.Message, FTL.Term]], Message], Compiler() @@ -90,8 +89,8 @@ def _lookup(self, entry_id: str, term: bool = False) -> Message: return self._compiled[compiled_id] def format_pattern( - self, pattern: Pattern, args: Union[Dict[str, Any], None] = None - ) -> Tuple[Union[str, "FluentNone"], List[Exception]]: + self, pattern: Pattern, args: Union[dict[str, Any], None] = None + ) -> tuple[Union[str, "FluentNone"], list[Exception]]: if args is not None: fluent_args = { argname: native_to_fluent(argvalue) @@ -100,7 +99,7 @@ def format_pattern( else: fluent_args = {} - errors: List[Exception] = [] + errors: list[Exception] = [] env = ResolverEnvironment( context=self, current=CurrentEnvironment(args=fluent_args), errors=errors ) diff --git a/fluent.runtime/fluent/runtime/fallback.py b/fluent.runtime/fluent/runtime/fallback.py index f39bbf82..c9bacece 100644 --- a/fluent.runtime/fluent/runtime/fallback.py +++ b/fluent.runtime/fluent/runtime/fallback.py @@ -1,16 +1,7 @@ import codecs import os -from typing import ( - TYPE_CHECKING, - Any, - Callable, - Dict, - Generator, - List, - Type, - Union, - cast, -) +from collections.abc import Generator +from typing import TYPE_CHECKING, Any, Callable, Union, cast from fluent.syntax import FluentParser @@ -32,12 +23,12 @@ class FluentLocalization: def __init__( self, - locales: List[str], - resource_ids: List[str], + locales: list[str], + resource_ids: list[str], resource_loader: "AbstractResourceLoader", use_isolating: bool = False, - bundle_class: Type[FluentBundle] = FluentBundle, - functions: Union[Dict[str, Callable[[Any], "FluentType"]], None] = None, + bundle_class: type[FluentBundle] = FluentBundle, + functions: Union[dict[str, Callable[[Any], "FluentType"]], None] = None, ): self.locales = locales self.resource_ids = resource_ids @@ -45,11 +36,11 @@ def __init__( self.use_isolating = use_isolating self.bundle_class = bundle_class self.functions = functions - self._bundle_cache: List[FluentBundle] = [] + self._bundle_cache: list[FluentBundle] = [] self._bundle_it = self._iterate_bundles() def format_value( - self, msg_id: str, args: Union[Dict[str, Any], None] = None + self, msg_id: str, args: Union[dict[str, Any], None] = None ) -> str: for bundle in self._bundles(): if not bundle.has_message(msg_id): @@ -63,7 +54,7 @@ def format_value( ) # Never FluentNone when format_pattern called externally return msg_id - def _create_bundle(self, locales: List[str]) -> FluentBundle: + def _create_bundle(self, locales: list[str]) -> FluentBundle: return self.bundle_class( locales, functions=self.functions, use_isolating=self.use_isolating ) @@ -95,8 +86,8 @@ class AbstractResourceLoader: """ def resources( - self, locale: str, resource_ids: List[str] - ) -> Generator[List["Resource"], None, None]: + self, locale: str, resource_ids: list[str] + ) -> Generator[list["Resource"], None, None]: """ Yield lists of FluentResource objects, corresponding to each of the resource_ids. @@ -118,7 +109,7 @@ class FluentResourceLoader(AbstractResourceLoader): different roots. """ - def __init__(self, roots: Union[str, List[str]]): + def __init__(self, roots: Union[str, list[str]]): """ Create a resource loader. The roots may be a string for a single location on disk, or a list of strings. @@ -126,10 +117,10 @@ def __init__(self, roots: Union[str, List[str]]): self.roots = [roots] if isinstance(roots, str) else roots def resources( - self, locale: str, resource_ids: List[str] - ) -> Generator[List["Resource"], None, None]: + self, locale: str, resource_ids: list[str] + ) -> Generator[list["Resource"], None, None]: for root in self.roots: - resources: List[Any] = [] + resources: list[Any] = [] for resource_id in resource_ids: path = self.localize_path(os.path.join(root, resource_id), locale) if not os.path.isfile(path): diff --git a/fluent.runtime/fluent/runtime/prepare.py b/fluent.runtime/fluent/runtime/prepare.py index 418b33ba..b14aea0b 100644 --- a/fluent.runtime/fluent/runtime/prepare.py +++ b/fluent.runtime/fluent/runtime/prepare.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List +from typing import Any from fluent.syntax import ast as FTL @@ -17,7 +17,7 @@ def compile(self, node: Any) -> Any: nodename: str = type(node).__name__ if not hasattr(resolver, nodename): return node - kwargs: Dict[str, Any] = vars(node).copy() + kwargs: dict[str, Any] = vars(node).copy() for propname, propvalue in kwargs.items(): kwargs[propname] = self(propvalue) handler = getattr(self, "compile_" + nodename, self.compile_generic) @@ -31,7 +31,7 @@ def compile_Placeable(self, _: Any, expression: Any, **kwargs: Any) -> Any: return expression return resolver.Placeable(expression=expression, **kwargs) - def compile_Pattern(self, _: Any, elements: List[Any], **kwargs: Any) -> Any: + def compile_Pattern(self, _: Any, elements: list[Any], **kwargs: Any) -> Any: if len(elements) == 1 and isinstance(elements[0], resolver.Placeable): # Don't isolate isolated placeables return resolver.NeverIsolatingPlaceable(elements[0].expression) diff --git a/fluent.runtime/fluent/runtime/resolver.py b/fluent.runtime/fluent/runtime/resolver.py index 1961a960..b42b14c3 100644 --- a/fluent.runtime/fluent/runtime/resolver.py +++ b/fluent.runtime/fluent/runtime/resolver.py @@ -1,5 +1,6 @@ import contextlib -from typing import TYPE_CHECKING, Any, Dict, Generator, List, Set, Union, cast +from collections.abc import Generator +from typing import TYPE_CHECKING, Any, Union, cast import attr from fluent.syntax import ast as FTL @@ -42,7 +43,7 @@ class CurrentEnvironment: # For Messages, VariableReference nodes are interpreted as external args, # but for Terms they are the values explicitly passed using CallExpression # syntax. So we have to be able to change 'args' for this purpose. - args: Dict[str, Any] = attr.ib(factory=dict) + args: dict[str, Any] = attr.ib(factory=dict) # This controls whether we need to report an error if a VariableReference # refers to an arg that is not present in the args dict. error_for_missing_arg: bool = attr.ib(default=True) @@ -51,9 +52,9 @@ class CurrentEnvironment: @attr.s class ResolverEnvironment: context: "FluentBundle" = attr.ib() - errors: List[Exception] = attr.ib() + errors: list[Exception] = attr.ib() part_count: int = attr.ib(default=0, init=False) - active_patterns: Set[FTL.Pattern] = attr.ib(factory=set, init=False) + active_patterns: set[FTL.Pattern] = attr.ib(factory=set, init=False) current: CurrentEnvironment = attr.ib(factory=CurrentEnvironment) @contextlib.contextmanager @@ -72,7 +73,7 @@ def modified( self.current = old_current def modified_for_term_reference( - self, args: Union[Dict[str, Any], None] = None + self, args: Union[dict[str, Any], None] = None ) -> Any: return self.modified( args=args if args is not None else {}, error_for_missing_arg=False @@ -100,13 +101,13 @@ class Literal(BaseResolver): class Message(FTL.Entry, BaseResolver): id: "Identifier" value: Union["Pattern", None] - attributes: Dict[str, "Pattern"] + attributes: dict[str, "Pattern"] def __init__( self, id: "Identifier", value: Union["Pattern", None] = None, - attributes: Union[List["Attribute"], None] = None, + attributes: Union[list["Attribute"], None] = None, comment: Any = None, **kwargs: Any, ): @@ -121,13 +122,13 @@ def __init__( class Term(FTL.Entry, BaseResolver): id: "Identifier" value: "Pattern" - attributes: Dict[str, "Pattern"] + attributes: dict[str, "Pattern"] def __init__( self, id: "Identifier", value: "Pattern", - attributes: Union[List["Attribute"], None] = None, + attributes: Union[list["Attribute"], None] = None, comment: Any = None, **kwargs: Any, ): @@ -143,7 +144,7 @@ class Pattern(FTL.Pattern, BaseResolver): # Prevent messages with too many sub parts, for CPI DOS protection MAX_PARTS = 1000 - elements: List[Union["TextElement", "Placeable"]] # type: ignore + elements: list[Union["TextElement", "Placeable"]] # type: ignore def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) @@ -294,7 +295,7 @@ def __call__(self, env: ResolverEnvironment) -> Any: if isinstance(arg_val, (FluentType, str)): return arg_val env.errors.append( - TypeError("Unsupported external type: {}, {}".format(name, type(arg_val))) + TypeError(f"Unsupported external type: {name}, {type(arg_val)}") ) return FluentNone(name) @@ -306,7 +307,7 @@ class Attribute(FTL.Attribute, BaseResolver): class SelectExpression(FTL.SelectExpression, BaseResolver): selector: "InlineExpression" - variants: List["Variant"] # type: ignore + variants: list["Variant"] # type: ignore def __call__(self, env: ResolverEnvironment) -> Union[str, FluentNone]: key = self.selector(env) @@ -368,8 +369,8 @@ def __call__(self, env: ResolverEnvironment) -> str: class CallArguments(FTL.CallArguments, BaseResolver): - positional: List[Union["InlineExpression", Placeable]] # type: ignore - named: List["NamedArgument"] # type: ignore + positional: list[Union["InlineExpression", Placeable]] # type: ignore + named: list["NamedArgument"] # type: ignore class FunctionReference(FTL.FunctionReference, BaseResolver): @@ -384,7 +385,7 @@ def __call__(self, env: ResolverEnvironment) -> Any: function = env.context._functions[function_name] except LookupError: env.errors.append( - FluentReferenceError("Unknown function: {}".format(function_name)) + FluentReferenceError(f"Unknown function: {function_name}") ) return FluentNone(function_name + "()") diff --git a/fluent.runtime/fluent/runtime/types.py b/fluent.runtime/fluent/runtime/types.py index 697a18b7..c6717194 100644 --- a/fluent.runtime/fluent/runtime/types.py +++ b/fluent.runtime/fluent/runtime/types.py @@ -1,14 +1,13 @@ import warnings from datetime import date, datetime from decimal import Decimal -from typing import Any, Dict, Type, TypeVar, Union, cast +from typing import Any, Literal, TypeVar, Union, cast import attr import pytz from babel import Locale from babel.dates import format_date, format_time, get_datetime_format, get_timezone from babel.numbers import NumberPattern, parse_pattern -from typing_extensions import Literal FORMAT_STYLE_DECIMAL = "decimal" FORMAT_STYLE_CURRENCY = "currency" @@ -106,7 +105,7 @@ def __new__( return self._init(value, kwargs) def _init( - self, value: Union[int, float, Decimal, "FluentNumber"], kwargs: Dict[str, Any] + self, value: Union[int, float, Decimal, "FluentNumber"], kwargs: dict[str, Any] ) -> "FluentNumber": self.options = merge_options( NumberFormatOptions, @@ -211,7 +210,7 @@ def replacer(s: str) -> str: def merge_options( - options_class: Type[Options], base: Union[Options, None], kwargs: Dict[str, Any] + options_class: type[Options], base: Union[Options, None], kwargs: dict[str, Any] ) -> Options: """ Given an 'options_class', an optional 'base' object to copy from, @@ -346,7 +345,7 @@ class FluentDateType(FluentType): # So we leave those alone, and implement another `_init_options` # which is called from other constructors. def _init_options( - self, dt_obj: Union[date, datetime], kwargs: Dict[str, Any] + self, dt_obj: Union[date, datetime], kwargs: dict[str, Any] ) -> None: if "timeStyle" in kwargs and not isinstance(self, datetime): raise TypeError( @@ -437,6 +436,4 @@ def fluent_date( elif isinstance(dt, FluentNone): return dt else: - raise TypeError( - "Can't use fluent_date with object {} of type {}".format(dt, type(dt)) - ) + raise TypeError(f"Can't use fluent_date with object {dt} of type {type(dt)}") diff --git a/fluent.runtime/setup.py b/fluent.runtime/setup.py index 8a32f929..c66b513a 100755 --- a/fluent.runtime/setup.py +++ b/fluent.runtime/setup.py @@ -6,7 +6,6 @@ with open(path.join(this_directory, "README.rst"), "rb") as f: long_description = f.read().decode("utf-8") - setup( name="fluent.runtime", description="Localization library for expressive translations.", @@ -21,22 +20,21 @@ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3 :: Only", ], packages=["fluent.runtime"], package_data={"fluent.runtime": ["py.typed"]}, # These should also be duplicated in tox.ini and /.github/workflows/fluent.runtime.yml - python_requires=">=3.6", + python_requires=">=3.9", install_requires=[ "fluent.syntax>=0.17,<0.20", "attrs", "babel", "pytz", - "typing-extensions>=3.7,<5", ], test_suite="tests", ) diff --git a/fluent.runtime/tools/benchmarks/fluent_benchmark.py b/fluent.runtime/tools/benchmarks/fluent_benchmark.py index d80de407..83f42912 100644 --- a/fluent.runtime/tools/benchmarks/fluent_benchmark.py +++ b/fluent.runtime/tools/benchmarks/fluent_benchmark.py @@ -1,7 +1,6 @@ #!/usr/bin/env python # This should be run using pytest -from __future__ import unicode_literals import sys @@ -48,7 +47,7 @@ def fluent_template(bundle): ) -class TestBenchmark(object): +class TestBenchmark: def test_template(self, fluent_bundle, benchmark): benchmark(lambda: fluent_template(fluent_bundle)) diff --git a/fluent.runtime/tox.ini b/fluent.runtime/tox.ini index c532b65f..ed254d68 100644 --- a/fluent.runtime/tox.ini +++ b/fluent.runtime/tox.ini @@ -1,7 +1,7 @@ # This config is for local testing. # It should be correspond to .github/workflows/fluent.runtime.yml [tox] -envlist = {py36,py37,py38,py39,pypy3}-syntax, py3-syntax0.17, latest +envlist = {py39,py310,py311,py312,pypy3}-syntax, py3-syntax0.17, latest skipsdist=True [testenv] @@ -12,7 +12,6 @@ deps = attrs==19.1.0 babel==2.7.0 pytz==2019.2 - typing-extensions~=3.7 syntax: . commands = python -m unittest diff --git a/fluent.syntax/fluent/syntax/ast.py b/fluent.syntax/fluent/syntax/ast.py index c47244e5..c13ee1c7 100644 --- a/fluent.syntax/fluent/syntax/ast.py +++ b/fluent.syntax/fluent/syntax/ast.py @@ -1,10 +1,10 @@ import json import re import sys -from typing import Any, Callable, Dict, List, TypeVar, Union, cast +from typing import Any, Callable, TypeVar, Union, cast Node = TypeVar("Node", bound="BaseNode") -ToJsonFn = Callable[[Dict[str, Any]], Any] +ToJsonFn = Callable[[dict[str, Any]], Any] def to_json(value: Any, fn: Union[ToJsonFn, None] = None) -> Any: @@ -29,7 +29,7 @@ def from_json(value: Any) -> Any: return value -def scalars_equal(node1: Any, node2: Any, ignored_fields: List[str]) -> bool: +def scalars_equal(node1: Any, node2: Any, ignored_fields: list[str]) -> bool: """Compare two nodes which are not lists.""" if type(node1) is not type(node2): @@ -66,7 +66,7 @@ def visit(value: Any) -> Any: **{name: visit(value) for name, value in vars(self).items()} ) - def equals(self, other: "BaseNode", ignored_fields: List[str] = ["span"]) -> bool: + def equals(self, other: "BaseNode", ignored_fields: list[str] = ["span"]) -> bool: """Compare two nodes. Nodes are deeply compared on a field by field basis. If possible, False @@ -126,7 +126,7 @@ def add_span(self, start: int, end: int) -> None: class Resource(SyntaxNode): - def __init__(self, body: Union[List["EntryType"], None] = None, **kwargs: Any): + def __init__(self, body: Union[list["EntryType"], None] = None, **kwargs: Any): super().__init__(**kwargs) self.body = body or [] @@ -140,7 +140,7 @@ def __init__( self, id: "Identifier", value: Union["Pattern", None] = None, - attributes: Union[List["Attribute"], None] = None, + attributes: Union[list["Attribute"], None] = None, comment: Union["Comment", None] = None, **kwargs: Any ): @@ -156,7 +156,7 @@ def __init__( self, id: "Identifier", value: "Pattern", - attributes: Union[List["Attribute"], None] = None, + attributes: Union[list["Attribute"], None] = None, comment: Union["Comment", None] = None, **kwargs: Any ): @@ -169,7 +169,7 @@ def __init__( class Pattern(SyntaxNode): def __init__( - self, elements: List[Union["TextElement", "Placeable"]], **kwargs: Any + self, elements: list[Union["TextElement", "Placeable"]], **kwargs: Any ): super().__init__(**kwargs) self.elements = elements @@ -206,12 +206,12 @@ def __init__(self, value: str, **kwargs: Any): super().__init__(**kwargs) self.value = value - def parse(self) -> Dict[str, Any]: + def parse(self) -> dict[str, Any]: return {"value": self.value} class StringLiteral(Literal): - def parse(self) -> Dict[str, str]: + def parse(self) -> dict[str, str]: def from_escape_sequence(matchobj: Any) -> str: c, codepoint4, codepoint6 = matchobj.groups() if c: @@ -233,7 +233,7 @@ def from_escape_sequence(matchobj: Any) -> str: class NumberLiteral(Literal): - def parse(self) -> Dict[str, Union[float, int]]: + def parse(self) -> dict[str, Union[float, int]]: value = float(self.value) decimal_position = self.value.find(".") precision = 0 @@ -283,7 +283,7 @@ def __init__(self, id: "Identifier", arguments: "CallArguments", **kwargs: Any): class SelectExpression(Expression): def __init__( - self, selector: "InlineExpression", variants: List["Variant"], **kwargs: Any + self, selector: "InlineExpression", variants: list["Variant"], **kwargs: Any ): super().__init__(**kwargs) self.selector = selector @@ -293,8 +293,8 @@ def __init__( class CallArguments(SyntaxNode): def __init__( self, - positional: Union[List[Union["InlineExpression", Placeable]], None] = None, - named: Union[List["NamedArgument"], None] = None, + positional: Union[list[Union["InlineExpression", Placeable]], None] = None, + named: Union[list["NamedArgument"], None] = None, **kwargs: Any ): super().__init__(**kwargs) @@ -366,7 +366,7 @@ class Junk(SyntaxNode): def __init__( self, content: Union[str, None] = None, - annotations: Union[List["Annotation"], None] = None, + annotations: Union[list["Annotation"], None] = None, **kwargs: Any ): super().__init__(**kwargs) @@ -388,7 +388,7 @@ class Annotation(SyntaxNode): def __init__( self, code: str, - arguments: Union[List[Any], None] = None, + arguments: Union[list[Any], None] = None, message: Union[str, None] = None, **kwargs: Any ): diff --git a/fluent.syntax/fluent/syntax/errors.py b/fluent.syntax/fluent/syntax/errors.py index b6a59f5b..baa98113 100644 --- a/fluent.syntax/fluent/syntax/errors.py +++ b/fluent.syntax/fluent/syntax/errors.py @@ -1,4 +1,4 @@ -from typing import Tuple, Union +from typing import Union class ParseError(Exception): @@ -8,15 +8,15 @@ def __init__(self, code: str, *args: Union[str, None]): self.message = get_error_message(code, args) -def get_error_message(code: str, args: Tuple[Union[str, None], ...]) -> str: +def get_error_message(code: str, args: tuple[Union[str, None], ...]) -> str: if code == "E00001": return "Generic error" if code == "E0002": return "Expected an entry start" if code == "E0003": - return 'Expected token: "{}"'.format(args[0]) + return f'Expected token: "{args[0]}"' if code == "E0004": - return 'Expected a character from range: "{}"'.format(args[0]) + return f'Expected a character from range: "{args[0]}"' if code == "E0005": msg = 'Expected message "{}" to have a value or attributes' return msg.format(args[0]) @@ -58,9 +58,9 @@ def get_error_message(code: str, args: Tuple[Union[str, None], ...]) -> str: if code == "E0024": return "Cannot access variants of a message." if code == "E0025": - return "Unknown escape sequence: \\{}.".format(args[0]) + return f"Unknown escape sequence: \\{args[0]}." if code == "E0026": - return "Invalid Unicode escape sequence: {}.".format(args[0]) + return f"Invalid Unicode escape sequence: {args[0]}." if code == "E0027": return "Unbalanced closing brace in TextElement." if code == "E0028": diff --git a/fluent.syntax/fluent/syntax/parser.py b/fluent.syntax/fluent/syntax/parser.py index 7b41f314..aefe15db 100644 --- a/fluent.syntax/fluent/syntax/parser.py +++ b/fluent.syntax/fluent/syntax/parser.py @@ -1,5 +1,5 @@ import re -from typing import Any, Callable, List, Set, TypeVar, Union, cast +from typing import Any, Callable, TypeVar, Union, cast from . import ast from .errors import ParseError @@ -45,7 +45,7 @@ def parse(self, source: str) -> ast.Resource: ps = FluentParserStream(source) ps.skip_blank_block() - entries: List[ast.EntryType] = [] + entries: list[ast.EntryType] = [] last_comment = None while ps.current_char: @@ -235,8 +235,8 @@ def get_attribute(self, ps: FluentParserStream) -> ast.Attribute: return ast.Attribute(key, value) - def get_attributes(self, ps: FluentParserStream) -> List[ast.Attribute]: - attrs: List[ast.Attribute] = [] + def get_attributes(self, ps: FluentParserStream) -> list[ast.Attribute]: + attrs: list[ast.Attribute] = [] ps.peek_blank() while ps.is_attribute_start(): @@ -298,8 +298,8 @@ def get_variant(self, ps: FluentParserStream, has_default: bool) -> ast.Variant: return ast.Variant(key, value, default_index) - def get_variants(self, ps: FluentParserStream) -> List[ast.Variant]: - variants: List[ast.Variant] = [] + def get_variants(self, ps: FluentParserStream) -> list[ast.Variant]: + variants: list[ast.Variant] = [] has_default = False ps.skip_blank() @@ -375,7 +375,7 @@ def maybe_get_pattern(self, ps: FluentParserStream) -> Union[ast.Pattern, None]: @with_span def get_pattern(self, ps: FluentParserStream, is_block: bool) -> ast.Pattern: - elements: List[Any] = [] + elements: list[Any] = [] if is_block: # A block pattern is a pattern which starts on a new line. Measure # the indent of this first line for the dedentation logic. @@ -421,20 +421,20 @@ def get_pattern(self, ps: FluentParserStream, is_block: bool) -> ast.Pattern: class Indent(ast.SyntaxNode): def __init__(self, value: str, start: int, end: int): - super(FluentParser.Indent, self).__init__() + super().__init__() self.value = value self.add_span(start, end) def dedent( self, - elements: List[Union[ast.TextElement, ast.Placeable, Indent]], + elements: list[Union[ast.TextElement, ast.Placeable, Indent]], common_indent: int, - ) -> List[Union[ast.TextElement, ast.Placeable]]: + ) -> list[Union[ast.TextElement, ast.Placeable]]: """Dedent a list of elements by removing the maximum common indent from the beginning of text lines. The common indent is calculated in get_pattern. """ - trimmed: List[Union[ast.TextElement, ast.Placeable]] = [] + trimmed: list[Union[ast.TextElement, ast.Placeable]] = [] for element in elements: if isinstance(element, ast.Placeable): @@ -659,9 +659,9 @@ def get_call_argument( @with_span def get_call_arguments(self, ps: FluentParserStream) -> ast.CallArguments: - positional: List[Union[ast.InlineExpression, ast.Placeable]] = [] - named: List[ast.NamedArgument] = [] - argument_names: Set[str] = set() + positional: list[Union[ast.InlineExpression, ast.Placeable]] = [] + named: list[ast.NamedArgument] = [] + argument_names: set[str] = set() ps.expect_char("(") ps.skip_blank() diff --git a/fluent.syntax/fluent/syntax/serializer.py b/fluent.syntax/fluent/syntax/serializer.py index 3659154c..414dce19 100644 --- a/fluent.syntax/fluent/syntax/serializer.py +++ b/fluent.syntax/fluent/syntax/serializer.py @@ -1,4 +1,4 @@ -from typing import List, Union +from typing import Union from . import ast @@ -46,11 +46,11 @@ def __init__(self, with_junk: bool = False): def serialize(self, resource: ast.Resource) -> str: "Serialize a :class:`.ast.Resource` to a string." if not isinstance(resource, ast.Resource): - raise Exception("Unknown resource type: {}".format(type(resource))) + raise Exception(f"Unknown resource type: {type(resource)}") state = 0 - parts: List[str] = [] + parts: list[str] = [] for entry in resource.body: if not isinstance(entry, ast.Junk) or self.with_junk: parts.append(self.serialize_entry(entry, state)) @@ -79,7 +79,7 @@ def serialize_entry(self, entry: ast.EntryType, state: int = 0) -> str: return "{}\n".format(serialize_comment(entry, "###")) if isinstance(entry, ast.Junk): return serialize_junk(entry) - raise Exception("Unknown entry type: {}".format(type(entry))) + raise Exception(f"Unknown entry type: {type(entry)}") def serialize_comment( @@ -104,7 +104,7 @@ def serialize_junk(junk: ast.Junk) -> str: def serialize_message(message: ast.Message) -> str: - parts: List[str] = [] + parts: list[str] = [] if message.comment: parts.append(serialize_comment(message.comment)) @@ -123,7 +123,7 @@ def serialize_message(message: ast.Message) -> str: def serialize_term(term: ast.Term) -> str: - parts: List[str] = [] + parts: list[str] = [] if term.comment: parts.append(serialize_comment(term.comment)) @@ -160,20 +160,20 @@ def serialize_element(element: ast.PatternElement) -> str: return element.value if isinstance(element, ast.Placeable): return serialize_placeable(element) - raise Exception("Unknown element type: {}".format(type(element))) + raise Exception(f"Unknown element type: {type(element)}") def serialize_placeable(placeable: ast.Placeable) -> str: expr = placeable.expression if isinstance(expr, ast.Placeable): - return "{{{}}}".format(serialize_placeable(expr)) + return f"{{{serialize_placeable(expr)}}}" if isinstance(expr, ast.SelectExpression): # Special-case select expressions to control the withespace around the # opening and the closing brace. - return "{{ {}}}".format(serialize_expression(expr)) + return f"{{ {serialize_expression(expr)}}}" if isinstance(expr, ast.Expression): - return "{{ {} }}".format(serialize_expression(expr)) - raise Exception("Unknown expression type: {}".format(type(expr))) + return f"{{ {serialize_expression(expr)} }}" + raise Exception(f"Unknown expression type: {type(expr)}") def serialize_expression(expression: Union[ast.Expression, ast.Placeable]) -> str: @@ -199,13 +199,13 @@ def serialize_expression(expression: Union[ast.Expression, ast.Placeable]) -> st args = serialize_call_arguments(expression.arguments) return f"{expression.id.name}{args}" if isinstance(expression, ast.SelectExpression): - out = "{} ->".format(serialize_expression(expression.selector)) + out = f"{serialize_expression(expression.selector)} ->" for variant in expression.variants: out += serialize_variant(variant) return f"{out}\n" if isinstance(expression, ast.Placeable): return serialize_placeable(expression) - raise Exception("Unknown expression type: {}".format(type(expression))) + raise Exception(f"Unknown expression type: {type(expression)}") def serialize_variant(variant: ast.Variant) -> str: @@ -221,11 +221,11 @@ def serialize_call_arguments(expr: ast.CallArguments) -> str: named = ", ".join(serialize_named_argument(arg) for arg in expr.named) if len(expr.positional) > 0 and len(expr.named) > 0: return f"({positional}, {named})" - return "({})".format(positional or named) + return f"({positional or named})" def serialize_named_argument(arg: ast.NamedArgument) -> str: - return "{}: {}".format(arg.name.name, serialize_expression(arg.value)) + return f"{arg.name.name}: {serialize_expression(arg.value)}" def serialize_variant_key(key: Union[ast.Identifier, ast.NumberLiteral]) -> str: @@ -233,4 +233,4 @@ def serialize_variant_key(key: Union[ast.Identifier, ast.NumberLiteral]) -> str: return key.name if isinstance(key, ast.NumberLiteral): return key.value - raise Exception("Unknown variant key type: {}".format(type(key))) + raise Exception(f"Unknown variant key type: {type(key)}") diff --git a/fluent.syntax/fluent/syntax/stream.py b/fluent.syntax/fluent/syntax/stream.py index 0e276fc3..2a485e22 100644 --- a/fluent.syntax/fluent/syntax/stream.py +++ b/fluent.syntax/fluent/syntax/stream.py @@ -1,6 +1,4 @@ -from typing import Callable, Union - -from typing_extensions import Literal +from typing import Callable, Literal, Union from .errors import ParseError diff --git a/fluent.syntax/fluent/syntax/visitor.py b/fluent.syntax/fluent/syntax/visitor.py index 2f637afb..53fea500 100644 --- a/fluent.syntax/fluent/syntax/visitor.py +++ b/fluent.syntax/fluent/syntax/visitor.py @@ -1,4 +1,4 @@ -from typing import Any, List +from typing import Any from .ast import BaseNode, Node @@ -50,7 +50,7 @@ def visit(self, node: Any) -> Any: def generic_visit(self, node: Node) -> Node: # type: ignore for propname, propvalue in vars(node).items(): if isinstance(propvalue, list): - new_vals: List[Any] = [] + new_vals: list[Any] = [] for child in propvalue: new_val = self.visit(child) if new_val is not None: diff --git a/fluent.syntax/setup.py b/fluent.syntax/setup.py index 08899e2e..3fb4cade 100644 --- a/fluent.syntax/setup.py +++ b/fluent.syntax/setup.py @@ -20,14 +20,14 @@ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3 :: Only", ], packages=["fluent.syntax"], package_data={"fluent.syntax": ["py.typed"]}, - install_requires=["typing-extensions>=3.7,<5"], + python_requires=">=3.9", test_suite="tests.syntax", ) diff --git a/fluent.syntax/tox.ini b/fluent.syntax/tox.ini index a64d39b6..04b57ad6 100644 --- a/fluent.syntax/tox.ini +++ b/fluent.syntax/tox.ini @@ -1,12 +1,10 @@ # This config is for local testing. # It should be correspond to .github/workflows/fluent.syntax.yml [tox] -envlist = py36, py37, py38, py39, pypy3 +envlist = py39, py310, py311, py312, pypy3 skipsdist=True [testenv] setenv = PYTHONPATH = {toxinidir} -deps = - typing-extensions~=3.7 commands = python -m unittest