Skip to content

Drop support for Python < 3.9 #200

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 9 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
4 changes: 2 additions & 2 deletions fluent.runtime/fluent/runtime/builtins.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from typing import Any, Callable, Dict
from typing import Any, Callable

from .types import FluentType, fluent_date, fluent_number

NUMBER = fluent_number
DATETIME = fluent_date


BUILTINS: Dict[str, Callable[[Any], FluentType]] = {
BUILTINS: dict[str, Callable[[Any], FluentType]] = {
"NUMBER": NUMBER,
"DATETIME": DATETIME,
}
19 changes: 9 additions & 10 deletions fluent.runtime/fluent/runtime/bundle.py
Original file line number Diff line number Diff line change
@@ -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
)
39 changes: 15 additions & 24 deletions fluent.runtime/fluent/runtime/fallback.py
Original file line number Diff line number Diff line change
@@ -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,24 +23,24 @@ 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
self.resource_loader = resource_loader
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,18 +109,18 @@ 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.
"""
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):
6 changes: 3 additions & 3 deletions fluent.runtime/fluent/runtime/prepare.py
Original file line number Diff line number Diff line change
@@ -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)
31 changes: 16 additions & 15 deletions fluent.runtime/fluent/runtime/resolver.py
Original file line number Diff line number Diff line change
@@ -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 + "()")

13 changes: 5 additions & 8 deletions fluent.runtime/fluent/runtime/types.py
Original file line number Diff line number Diff line change
@@ -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)}")
10 changes: 4 additions & 6 deletions fluent.runtime/setup.py
Original file line number Diff line number Diff line change
@@ -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",
)
3 changes: 1 addition & 2 deletions fluent.runtime/tools/benchmarks/fluent_benchmark.py
Original file line number Diff line number Diff line change
@@ -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))

3 changes: 1 addition & 2 deletions fluent.runtime/tox.ini
Original file line number Diff line number Diff line change
@@ -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

32 changes: 16 additions & 16 deletions fluent.syntax/fluent/syntax/ast.py
Original file line number Diff line number Diff line change
@@ -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
):
12 changes: 6 additions & 6 deletions fluent.syntax/fluent/syntax/errors.py
Original file line number Diff line number Diff line change
@@ -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":
28 changes: 14 additions & 14 deletions fluent.syntax/fluent/syntax/parser.py
Original file line number Diff line number Diff line change
@@ -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()
32 changes: 16 additions & 16 deletions fluent.syntax/fluent/syntax/serializer.py
Original file line number Diff line number Diff line change
@@ -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,16 +221,16 @@ 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:
if isinstance(key, ast.Identifier):
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)}")
4 changes: 1 addition & 3 deletions fluent.syntax/fluent/syntax/stream.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
from typing import Callable, Union

from typing_extensions import Literal
from typing import Callable, Literal, Union

from .errors import ParseError

4 changes: 2 additions & 2 deletions fluent.syntax/fluent/syntax/visitor.py
Original file line number Diff line number Diff line change
@@ -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:
8 changes: 4 additions & 4 deletions fluent.syntax/setup.py
Original file line number Diff line number Diff line change
@@ -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",
)
4 changes: 1 addition & 3 deletions fluent.syntax/tox.ini
Original file line number Diff line number Diff line change
@@ -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