diff --git a/.github/workflows/fluent.runtime.yml b/.github/workflows/fluent.runtime.yml index a5e7a5ee..ad54c28c 100644 --- a/.github/workflows/fluent.runtime.yml +++ b/.github/workflows/fluent.runtime.yml @@ -58,7 +58,7 @@ jobs: python -m pip install wheel python -m pip install --upgrade pip python -m pip install . - python -m pip install flake8==6 mypy==1 types-babel types-pytz + python -m pip install flake8==6 mypy==1.1.1 types-babel types-pytz - name: Install latest fluent.syntax working-directory: ./fluent.syntax run: | diff --git a/.github/workflows/fluent.syntax.yml b/.github/workflows/fluent.syntax.yml index 458e4afb..8fdb2830 100644 --- a/.github/workflows/fluent.syntax.yml +++ b/.github/workflows/fluent.syntax.yml @@ -52,7 +52,7 @@ jobs: run: | python -m pip install --upgrade pip python -m pip install . - python -m pip install flake8==6 mypy==1 + python -m pip install flake8==6 mypy==1.1.1 - name: flake8 working-directory: ./fluent.syntax run: | diff --git a/fluent.runtime/fluent/runtime/__init__.py b/fluent.runtime/fluent/runtime/__init__.py index 74044378..e8ec252c 100644 --- a/fluent.runtime/fluent/runtime/__init__.py +++ b/fluent.runtime/fluent/runtime/__init__.py @@ -2,8 +2,7 @@ from fluent.syntax.ast import Resource from .bundle import FluentBundle -from .fallback import FluentLocalization, AbstractResourceLoader, FluentResourceLoader - +from .fallback import AbstractResourceLoader, FluentLocalization, FluentResourceLoader __all__ = [ 'FluentLocalization', diff --git a/fluent.runtime/fluent/runtime/builtins.py b/fluent.runtime/fluent/runtime/builtins.py index 4881c04f..1431991d 100644 --- a/fluent.runtime/fluent/runtime/builtins.py +++ b/fluent.runtime/fluent/runtime/builtins.py @@ -1,8 +1,10 @@ from typing import Any, Callable, Dict +from typing_extensions import Final + from .types import FluentType, fluent_date, fluent_number -NUMBER = fluent_number -DATETIME = fluent_date +NUMBER: Final = fluent_number +DATETIME: Final = fluent_date BUILTINS: Dict[str, Callable[[Any], FluentType]] = { diff --git a/fluent.runtime/fluent/runtime/bundle.py b/fluent.runtime/fluent/runtime/bundle.py index 45e32852..b532d28b 100644 --- a/fluent.runtime/fluent/runtime/bundle.py +++ b/fluent.runtime/fluent/runtime/bundle.py @@ -1,9 +1,9 @@ +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Sequence, Tuple, Union, cast +from typing_extensions import Literal + import babel import babel.numbers import babel.plural -from typing import Any, Callable, Dict, List, TYPE_CHECKING, Tuple, Union, cast -from typing_extensions import Literal - from fluent.syntax import ast as FTL from .builtins import BUILTINS @@ -12,6 +12,8 @@ from .utils import native_to_fluent if TYPE_CHECKING: + from _typeshed import SupportsItems, SupportsKeysAndGetItem + from .types import FluentNone, FluentType PluralCategory = Literal['zero', 'one', 'two', 'few', 'many', 'other'] @@ -33,8 +35,8 @@ class FluentBundle: """ def __init__(self, - locales: List[str], - functions: Union[Dict[str, Callable[[Any], 'FluentType']], None] = None, + locales: Sequence[str], + functions: Union['SupportsKeysAndGetItem[str, Callable[[Any], FluentType]]', None] = None, use_isolating: bool = True): self.locales = locales self._functions = {**BUILTINS, **(functions or {})} @@ -79,10 +81,10 @@ def _lookup(self, entry_id: str, term: bool = False) -> Message: def format_pattern(self, pattern: Pattern, - args: Union[Dict[str, Any], None] = None + args: Union['SupportsItems[str, Any]', None] = None ) -> Tuple[Union[str, 'FluentNone'], List[Exception]]: if args is not None: - fluent_args = { + fluent_args: Dict[str, Any] = { argname: native_to_fluent(argvalue) for argname, argvalue in args.items() } diff --git a/fluent.runtime/fluent/runtime/fallback.py b/fluent.runtime/fluent/runtime/fallback.py index bd880306..7e9db848 100644 --- a/fluent.runtime/fluent/runtime/fallback.py +++ b/fluent.runtime/fluent/runtime/fallback.py @@ -1,13 +1,16 @@ import codecs import os -from typing import Any, Callable, Dict, Generator, List, TYPE_CHECKING, Type, Union, cast +from typing import TYPE_CHECKING, Any, Callable, Iterable, Iterator, List, Sequence, Type, Union, cast from fluent.syntax import FluentParser from .bundle import FluentBundle if TYPE_CHECKING: + from _typeshed import SupportsItems, SupportsKeysAndGetItem + from fluent.syntax.ast import Resource + from .types import FluentType @@ -21,12 +24,12 @@ class FluentLocalization: def __init__( self, - locales: List[str], - resource_ids: List[str], + locales: Sequence[str], + resource_ids: Iterable[str], resource_loader: 'AbstractResourceLoader', use_isolating: bool = False, bundle_class: Type[FluentBundle] = FluentBundle, - functions: Union[Dict[str, Callable[[Any], 'FluentType']], None] = None, + functions: Union['SupportsKeysAndGetItem[str, Callable[[Any], FluentType]]', None] = None, ): self.locales = locales self.resource_ids = resource_ids @@ -37,7 +40,7 @@ def __init__( 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) -> str: + def format_value(self, msg_id: str, args: Union['SupportsItems[str, Any]', None] = None) -> str: for bundle in self._bundles(): if not bundle.has_message(msg_id): continue @@ -48,12 +51,12 @@ def format_value(self, msg_id: str, args: Union[Dict[str, Any], None] = None) -> return cast(str, val) # Never FluentNone when format_pattern called externally return msg_id - def _create_bundle(self, locales: List[str]) -> FluentBundle: + def _create_bundle(self, locales: Sequence[str]) -> FluentBundle: return self.bundle_class( locales, functions=self.functions, use_isolating=self.use_isolating ) - def _bundles(self) -> Generator[FluentBundle, None, None]: + def _bundles(self) -> Iterator[FluentBundle]: bundle_pointer = 0 while True: if bundle_pointer == len(self._bundle_cache): @@ -64,7 +67,7 @@ def _bundles(self) -> Generator[FluentBundle, None, None]: yield self._bundle_cache[bundle_pointer] bundle_pointer += 1 - def _iterate_bundles(self) -> Generator[FluentBundle, None, None]: + def _iterate_bundles(self) -> Iterator[FluentBundle]: for first_loc in range(0, len(self.locales)): locs = self.locales[first_loc:] for resources in self.resource_loader.resources(locs[0], self.resource_ids): @@ -79,7 +82,7 @@ class AbstractResourceLoader: Interface to implement for resource loaders. """ - def resources(self, locale: str, resource_ids: List[str]) -> Generator[List['Resource'], None, None]: + def resources(self, locale: str, resource_ids: Iterable[str]) -> Iterator[List['Resource']]: """ Yield lists of FluentResource objects, corresponding to each of the resource_ids. @@ -101,14 +104,14 @@ class FluentResourceLoader(AbstractResourceLoader): different roots. """ - def __init__(self, roots: Union[str, List[str]]): + def __init__(self, roots: Union[str, Iterable[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 + self.roots: Iterable[str] = [roots] if isinstance(roots, str) else roots - def resources(self, locale: str, resource_ids: List[str]) -> Generator[List['Resource'], None, None]: + def resources(self, locale: str, resource_ids: Iterable[str]) -> Iterator[List['Resource']]: for root in self.roots: resources: List[Any] = [] for resource_id in resource_ids: diff --git a/fluent.runtime/fluent/runtime/prepare.py b/fluent.runtime/fluent/runtime/prepare.py index 927105a3..ed2ec733 100644 --- a/fluent.runtime/fluent/runtime/prepare.py +++ b/fluent.runtime/fluent/runtime/prepare.py @@ -1,5 +1,7 @@ -from typing import Any, Dict, List +from typing import Any, Dict, Sequence + from fluent.syntax import ast as FTL + from . import resolver @@ -29,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: Sequence[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 e4d0caa5..de293613 100644 --- a/fluent.runtime/fluent/runtime/resolver.py +++ b/fluent.runtime/fluent/runtime/resolver.py @@ -1,10 +1,12 @@ -import attr import contextlib -from typing import Any, Dict, Generator, List, Set, TYPE_CHECKING, Union, cast +from typing import TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Mapping, Sequence, Set, Union, cast +from typing_extensions import Final, Self +import attr from fluent.syntax import ast as FTL + from .errors import FluentCyclicReferenceError, FluentFormatError, FluentReferenceError -from .types import FluentType, FluentNone, FluentInt, FluentFloat +from .types import FluentFloat, FluentInt, FluentNone, FluentType from .utils import reference_to_id, unknown_reference_error_obj if TYPE_CHECKING: @@ -27,7 +29,7 @@ # Prevent expansion of too long placeables, for memory DOS protection -MAX_PART_LENGTH = 2500 +MAX_PART_LENGTH: Final = 2500 @attr.s @@ -56,7 +58,7 @@ class ResolverEnvironment: current: CurrentEnvironment = attr.ib(factory=CurrentEnvironment) @contextlib.contextmanager - def modified(self, **replacements: Any) -> Generator['ResolverEnvironment', None, None]: + def modified(self, **replacements: Any) -> Iterator[Self]: """ Context manager that modifies the 'current' attribute of the environment, restoring the old data at the end. @@ -68,7 +70,9 @@ def modified(self, **replacements: Any) -> Generator['ResolverEnvironment', None yield self self.current = old_current - def modified_for_term_reference(self, args: Union[Dict[str, Any], None] = None) -> Any: + def modified_for_term_reference(self, + args: Union[Mapping[str, Any], None] = None + ) -> 'contextlib._GeneratorContextManager[Self]': return self.modified(args=args if args is not None else {}, error_for_missing_arg=False) @@ -99,7 +103,7 @@ class Message(FTL.Entry, BaseResolver): def __init__(self, id: 'Identifier', value: Union['Pattern', None] = None, - attributes: Union[List['Attribute'], None] = None, + attributes: Union[Iterable['Attribute'], None] = None, comment: Any = None, **kwargs: Any): super().__init__(**kwargs) @@ -116,7 +120,7 @@ class Term(FTL.Entry, BaseResolver): def __init__(self, id: 'Identifier', value: 'Pattern', - attributes: Union[List['Attribute'], None] = None, + attributes: Union[Iterable['Attribute'], None] = None, comment: Any = None, **kwargs: Any): super().__init__(**kwargs) @@ -129,7 +133,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: Sequence[Union['TextElement', 'Placeable']] def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) @@ -198,14 +202,14 @@ def __call__(self, env: ResolverEnvironment) -> str: class NumberLiteral(FTL.NumberLiteral, BaseResolver): - value: Union[FluentFloat, FluentInt] # type: ignore + value: Union[FluentFloat, FluentInt] # type: ignore[assignment] def __init__(self, value: str, **kwargs: Any): super().__init__(value, **kwargs) - if '.' in cast(str, self.value): - self.value = FluentFloat(self.value) + if '.' in value: + self.value = FluentFloat(value) else: - self.value = FluentInt(self.value) + self.value = FluentInt(value) def __call__(self, env: ResolverEnvironment) -> Union[FluentFloat, FluentInt]: return self.value @@ -285,7 +289,7 @@ class Attribute(FTL.Attribute, BaseResolver): class SelectExpression(FTL.SelectExpression, BaseResolver): selector: 'InlineExpression' - variants: List['Variant'] # type: ignore + variants: Sequence['Variant'] def __call__(self, env: ResolverEnvironment) -> Union[str, FluentNone]: key = self.selector(env) @@ -340,8 +344,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: Sequence[Union['InlineExpression', Placeable]] + named: Sequence['NamedArgument'] class FunctionReference(FTL.FunctionReference, BaseResolver): diff --git a/fluent.runtime/fluent/runtime/types.py b/fluent.runtime/fluent/runtime/types.py index e57e653c..176828a2 100644 --- a/fluent.runtime/fluent/runtime/types.py +++ b/fluent.runtime/fluent/runtime/types.py @@ -1,34 +1,36 @@ import warnings from datetime import date, datetime from decimal import Decimal +from typing import Any, ClassVar, Generic, Mapping, Type, TypeVar, Union, cast, overload +from typing_extensions import Final, Literal, Self 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 import Any, Dict, Type, TypeVar, Union, cast -from typing_extensions import Literal -FORMAT_STYLE_DECIMAL = "decimal" -FORMAT_STYLE_CURRENCY = "currency" -FORMAT_STYLE_PERCENT = "percent" -FORMAT_STYLE_OPTIONS = { +_T = TypeVar('_T') + +FORMAT_STYLE_DECIMAL: Final = "decimal" +FORMAT_STYLE_CURRENCY: Final = "currency" +FORMAT_STYLE_PERCENT: Final = "percent" +FORMAT_STYLE_OPTIONS: Final = { FORMAT_STYLE_DECIMAL, FORMAT_STYLE_CURRENCY, FORMAT_STYLE_PERCENT, } -CURRENCY_DISPLAY_SYMBOL = "symbol" -CURRENCY_DISPLAY_CODE = "code" -CURRENCY_DISPLAY_NAME = "name" -CURRENCY_DISPLAY_OPTIONS = { +CURRENCY_DISPLAY_SYMBOL: Final = "symbol" +CURRENCY_DISPLAY_CODE: Final = "code" +CURRENCY_DISPLAY_NAME: Final = "name" +CURRENCY_DISPLAY_OPTIONS: Final = { CURRENCY_DISPLAY_SYMBOL, CURRENCY_DISPLAY_CODE, CURRENCY_DISPLAY_NAME, } -DATE_STYLE_OPTIONS = { +DATE_STYLE_OPTIONS: Final = { "full", "long", "medium", @@ -36,7 +38,7 @@ None, } -TIME_STYLE_OPTIONS = { +TIME_STYLE_OPTIONS: Final = { "full", "long", "medium", @@ -54,7 +56,7 @@ class FluentNone(FluentType): def __init__(self, name: Union[str, None] = None): self.name = name - def __eq__(self, other: Any) -> bool: + def __eq__(self, other: object) -> bool: return isinstance(other, FluentNone) and self.name == other.name def format(self, locale: Locale) -> str: @@ -83,19 +85,19 @@ class NumberFormatOptions: maximumSignificantDigits: Union[int, None] = attr.ib(default=None) -class FluentNumber(FluentType): +class FluentNumber(FluentType, Generic[_T]): - default_number_format_options = NumberFormatOptions() + default_number_format_options: ClassVar = NumberFormatOptions() def __new__(cls, - value: Union[int, float, Decimal, 'FluentNumber'], - **kwargs: Any) -> 'FluentNumber': - self = super().__new__(cls, value) # type: ignore + value: Union[str, _T, 'FluentNumber[_T]'], + **kwargs: Any) -> Self: + self = super().__new__(cls, value) # type: ignore[call-arg] return self._init(value, kwargs) def _init(self, - value: Union[int, float, Decimal, 'FluentNumber'], - kwargs: Dict[str, Any]) -> 'FluentNumber': + value: Union[str, _T, 'FluentNumber[_T]'], + kwargs: Mapping[str, Any]) -> Self: self.options = merge_options(NumberFormatOptions, getattr(value, 'options', self.default_number_format_options), kwargs) @@ -174,10 +176,7 @@ def replacer(s: str) -> str: return pattern -Options = TypeVar('Options', bound=Union[NumberFormatOptions, 'DateFormatOptions']) - - -def merge_options(options_class: Type[Options], base: Union[Options, None], kwargs: Dict[str, Any]) -> Options: +def merge_options(options_class: Type[_T], base: Union[_T, None], kwargs: Mapping[str, Any]) -> _T: """ Given an 'options_class', an optional 'base' object to copy from, and some keyword arguments, create a new options instance @@ -200,7 +199,7 @@ def merge_options(options_class: Type[Options], base: Union[Options, None], kwar for k in kwargs.keys(): setattr(retval, k, getattr(kwarg_options, k)) - return retval # type: ignore + return retval # We want types that inherit from both FluentNumber and a native type, @@ -213,22 +212,56 @@ def merge_options(options_class: Type[Options], base: Union[Options, None], kwar # instances in place of a native type and will work just the same without # modification (in most cases). -class FluentInt(FluentNumber, int): +class FluentInt(FluentNumber[int], int): pass -class FluentFloat(FluentNumber, float): +class FluentFloat(FluentNumber[float], float): pass -class FluentDecimal(FluentNumber, Decimal): +class FluentDecimal(FluentNumber[Decimal], Decimal): pass +AnyFluentNumber = TypeVar('AnyFluentNumber', bound=FluentNumber[Any]) + + +@overload +def fluent_number(number: AnyFluentNumber) -> AnyFluentNumber: # type: ignore[misc] + ... + + +@overload +def fluent_number(number: int, **kwargs: Any) -> FluentInt: + ... + + +@overload +def fluent_number(number: float, **kwargs: Any) -> FluentFloat: + ... + + +@overload +def fluent_number(number: Decimal, **kwargs: Any) -> FluentDecimal: + ... + + +@overload +def fluent_number(number: FluentNone, **kwargs: Any) -> FluentNone: + ... + + +@overload +def fluent_number(number: Union[int, float, Decimal, FluentNumber[Any], FluentNone], + **kwargs: Any) -> Union[FluentInt, FluentFloat, FluentDecimal, FluentNone]: + ... + + def fluent_number( - number: Union[int, float, Decimal, FluentNumber, FluentNone], + number: Union[int, float, Decimal, FluentNumber[Any], FluentNone], **kwargs: Any -) -> Union[FluentNumber, FluentNone]: +) -> Union[FluentNumber[Any], FluentNone]: if isinstance(number, FluentNumber) and not kwargs: return number if isinstance(number, int): @@ -244,7 +277,7 @@ def fluent_number( .format(number, type(number))) -_UNGROUPED_PATTERN = parse_pattern("#0") +_UNGROUPED_PATTERN: Final = parse_pattern("#0") def clone_pattern(pattern: NumberPattern) -> NumberPattern: @@ -288,7 +321,7 @@ class DateFormatOptions: validator=attr.validators.in_(TIME_STYLE_OPTIONS)) -_SUPPORTED_DATETIME_OPTIONS = ['dateStyle', 'timeStyle', 'timeZone'] +_SUPPORTED_DATETIME_OPTIONS: Final = ['dateStyle', 'timeStyle', 'timeZone'] class FluentDateType(FluentType): @@ -296,7 +329,7 @@ class FluentDateType(FluentType): # some Python implementation (e.g. PyPy) implement some methods. # 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]) -> None: + def _init_options(self, dt_obj: Union[date, datetime], kwargs: Mapping[str, Any]) -> None: if 'timeStyle' in kwargs and not isinstance(self, datetime): raise TypeError("timeStyle option can only be specified for datetime instances, not date instance") @@ -347,7 +380,7 @@ def _ensure_datetime_tzinfo(dt: datetime, tzinfo: Union[str, None] = None) -> da class FluentDate(FluentDateType, date): @classmethod - def from_date(cls, dt_obj: date, **kwargs: Any) -> 'FluentDate': + def from_date(cls, dt_obj: date, **kwargs: Any) -> Self: obj = cls(dt_obj.year, dt_obj.month, dt_obj.day) obj._init_options(dt_obj, kwargs) return obj @@ -355,7 +388,7 @@ def from_date(cls, dt_obj: date, **kwargs: Any) -> 'FluentDate': class FluentDateTime(FluentDateType, datetime): @classmethod - def from_date_time(cls, dt_obj: datetime, **kwargs: Any) -> 'FluentDateTime': + def from_date_time(cls, dt_obj: datetime, **kwargs: Any) -> Self: obj = cls(dt_obj.year, dt_obj.month, dt_obj.day, dt_obj.hour, dt_obj.minute, dt_obj.second, dt_obj.microsecond, tzinfo=dt_obj.tzinfo) @@ -363,6 +396,35 @@ def from_date_time(cls, dt_obj: datetime, **kwargs: Any) -> 'FluentDateTime': return obj +AnyFluentDate = TypeVar('AnyFluentDate', bound=FluentDateType) + + +@overload +def fluent_date(dt: AnyFluentDate) -> AnyFluentDate: # type: ignore[misc] + ... + + +@overload +def fluent_date(dt: datetime, **kwargs: Any) -> FluentDateTime: # type: ignore[misc] + ... + + +@overload +def fluent_date(dt: date, **kwargs: Any) -> FluentDate: + ... + + +@overload +def fluent_date(dt: FluentNone, **kwargs: Any) -> FluentNone: + ... + + +@overload +def fluent_date(dt: Union[FluentDateType, date, FluentNone], + **kwargs: Any) -> Union[FluentDateTime, FluentDate, FluentNone]: + ... + + def fluent_date( dt: Union[date, datetime, FluentDateType, FluentNone], **kwargs: Any diff --git a/fluent.runtime/fluent/runtime/utils.py b/fluent.runtime/fluent/runtime/utils.py index 86d44cec..79c5f7dd 100644 --- a/fluent.runtime/fluent/runtime/utils.py +++ b/fluent.runtime/fluent/runtime/utils.py @@ -1,14 +1,48 @@ from datetime import date, datetime from decimal import Decimal -from typing import Any, Union +from typing import Any, TypeVar, Union, overload +from typing_extensions import Final from fluent.syntax.ast import MessageReference, TermReference -from .types import FluentInt, FluentFloat, FluentDecimal, FluentDate, FluentDateTime from .errors import FluentReferenceError +from .types import FluentDate, FluentDateTime, FluentDecimal, FluentFloat, FluentInt -TERM_SIGIL = '-' -ATTRIBUTE_SEPARATOR = '.' +TERM_SIGIL: Final = '-' +ATTRIBUTE_SEPARATOR: Final = '.' + + +_T = TypeVar('_T') + + +@overload +def native_to_fluent(val: int) -> FluentInt: # type: ignore[misc] + ... + + +@overload +def native_to_fluent(val: float) -> FluentFloat: # type: ignore[misc] + ... + + +@overload +def native_to_fluent(val: Decimal) -> FluentDecimal: # type: ignore[misc] + ... + + +@overload +def native_to_fluent(val: datetime) -> FluentDateTime: # type: ignore[misc] + ... + + +@overload +def native_to_fluent(val: date) -> FluentDate: # type: ignore[misc] + ... + + +@overload +def native_to_fluent(val: _T) -> _T: + ... def native_to_fluent(val: Any) -> Any: diff --git a/fluent.runtime/setup.cfg b/fluent.runtime/setup.cfg index 060669fd..4456911c 100644 --- a/fluent.runtime/setup.cfg +++ b/fluent.runtime/setup.cfg @@ -12,6 +12,8 @@ max-line-length=120 line_length=120 skip_glob=.tox not_skip=__init__.py +extra_standard_library=typing_extensions, _typeshed [mypy] -strict = True +strict=True +python_version=3.6 diff --git a/fluent.runtime/setup.py b/fluent.runtime/setup.py index 41a5e11e..0ca7b4f2 100755 --- a/fluent.runtime/setup.py +++ b/fluent.runtime/setup.py @@ -33,7 +33,7 @@ 'attrs', 'babel', 'pytz', - 'typing-extensions>=3.7,<5' + 'typing-extensions>=4.0,<4.2' ], test_suite='tests', ) diff --git a/fluent.syntax/fluent/syntax/ast.py b/fluent.syntax/fluent/syntax/ast.py index d2e48490..f6e2b9b8 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 -import json -from typing import Any, Callable, Dict, List, TypeVar, Union, cast +from typing import Any, Callable, Dict, Iterable, List, Match, Sequence, Union, cast +from typing_extensions import Self, TypeAlias -Node = TypeVar('Node', bound='BaseNode') -ToJsonFn = Callable[[Dict[str, Any]], Any] +ToJsonFn: TypeAlias = Callable[[Dict[str, Any]], Any] def to_json(value: Any, fn: Union[ToJsonFn, None] = None) -> Any: @@ -21,7 +21,7 @@ def to_json(value: Any, fn: Union[ToJsonFn, None] = None) -> Any: def from_json(value: Any) -> Any: if isinstance(value, dict): cls = getattr(sys.modules[__name__], value['type']) - args = { + args: Dict[str, Any] = { k: from_json(v) for k, v in value.items() if k != 'type' @@ -33,7 +33,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: Iterable[str]) -> bool: """Compare two nodes which are not lists.""" if type(node1) != type(node2): @@ -52,7 +52,7 @@ class BaseNode: Annotation. Implements __str__, to_json and traverse. """ - def clone(self: Node) -> Node: + def clone(self) -> Self: """Create a deep clone of the current node.""" def visit(value: Any) -> Any: """Clone node and its descendants.""" @@ -69,7 +69,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: Iterable[str] = ['span']) -> bool: """Compare two nodes. Nodes are deeply compared on a field by field basis. If possible, False @@ -134,9 +134,9 @@ 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[Sequence['EntryType'], None] = None, **kwargs: Any): super().__init__(**kwargs) - self.body = body or [] + self.body: Sequence['EntryType'] = body or [] class Entry(SyntaxNode): @@ -147,28 +147,28 @@ class Message(Entry): def __init__(self, id: 'Identifier', value: Union['Pattern', None] = None, - attributes: Union[List['Attribute'], None] = None, + attributes: Union[Sequence['Attribute'], None] = None, comment: Union['Comment', None] = None, **kwargs: Any): super().__init__(**kwargs) self.id = id self.value = value - self.attributes = attributes or [] + self.attributes: Sequence['Attribute'] = attributes or [] self.comment = comment class Term(Entry): - def __init__(self, id: 'Identifier', value: 'Pattern', attributes: Union[List['Attribute'], None] = None, + def __init__(self, id: 'Identifier', value: 'Pattern', attributes: Union[Sequence['Attribute'], None] = None, comment: Union['Comment', None] = None, **kwargs: Any): super().__init__(**kwargs) self.id = id self.value = value - self.attributes = attributes or [] + self.attributes: Sequence['Attribute'] = attributes or [] self.comment = comment class Pattern(SyntaxNode): - def __init__(self, elements: List[Union['TextElement', 'Placeable']], **kwargs: Any): + def __init__(self, elements: Sequence[Union['TextElement', 'Placeable']], **kwargs: Any): super().__init__(**kwargs) self.elements = elements @@ -208,10 +208,10 @@ def parse(self) -> Dict[str, Any]: class StringLiteral(Literal): def parse(self) -> Dict[str, str]: - def from_escape_sequence(matchobj: Any) -> str: + def from_escape_sequence(matchobj: Match[str]) -> str: c, codepoint4, codepoint6 = matchobj.groups() if c: - return cast(str, c) + return c codepoint = int(codepoint4 or codepoint6, 16) if codepoint <= 0xD7FF or 0xE000 <= codepoint: return chr(codepoint) @@ -229,7 +229,7 @@ def from_escape_sequence(matchobj: Any) -> str: class NumberLiteral(Literal): - def parse(self) -> Dict[str, Union[float, int]]: + def parse(self) -> Dict[str, Any]: value = float(self.value) decimal_position = self.value.find('.') precision = 0 @@ -274,7 +274,7 @@ def __init__(self, id: 'Identifier', arguments: 'CallArguments', **kwargs: Any): class SelectExpression(Expression): - def __init__(self, selector: 'InlineExpression', variants: List['Variant'], **kwargs: Any): + def __init__(self, selector: 'InlineExpression', variants: Sequence['Variant'], **kwargs: Any): super().__init__(**kwargs) self.selector = selector self.variants = variants @@ -282,12 +282,12 @@ def __init__(self, selector: 'InlineExpression', variants: List['Variant'], **kw class CallArguments(SyntaxNode): def __init__(self, - positional: Union[List[Union['InlineExpression', Placeable]], None] = None, - named: Union[List['NamedArgument'], None] = None, + positional: Union[Sequence[Union['InlineExpression', Placeable]], None] = None, + named: Union[Sequence['NamedArgument'], None] = None, **kwargs: Any): super().__init__(**kwargs) - self.positional = [] if positional is None else positional - self.named = [] if named is None else named + self.positional: Sequence[Union['InlineExpression', Placeable]] = [] if positional is None else positional + self.named: Sequence['NamedArgument'] = [] if named is None else named class Attribute(SyntaxNode): @@ -362,15 +362,15 @@ def __init__(self, start: int, end: int, **kwargs: Any): class Annotation(SyntaxNode): def __init__(self, code: str, - arguments: Union[List[Any], None] = None, + arguments: Union[Sequence[Any], None] = None, message: Union[str, None] = None, **kwargs: Any): super().__init__(**kwargs) self.code = code - self.arguments = arguments or [] + self.arguments: Sequence[Any] = arguments or [] self.message = message -EntryType = Union[Message, Term, Comment, GroupComment, ResourceComment, Junk] -InlineExpression = Union[NumberLiteral, StringLiteral, MessageReference, - TermReference, VariableReference, FunctionReference] +EntryType: TypeAlias = Union[Message, Term, Comment, GroupComment, ResourceComment, Junk] +InlineExpression: TypeAlias = Union[NumberLiteral, StringLiteral, MessageReference, + TermReference, VariableReference, FunctionReference] diff --git a/fluent.syntax/fluent/syntax/parser.py b/fluent.syntax/fluent/syntax/parser.py index 87075409..f10a2277 100644 --- a/fluent.syntax/fluent/syntax/parser.py +++ b/fluent.syntax/fluent/syntax/parser.py @@ -1,13 +1,14 @@ import re -from typing import Any, Callable, List, Set, TypeVar, Union, cast +from typing import Any, Callable, Iterable, List, Set, TypeVar, Union, cast + from . import ast -from .stream import EOL, FluentParserStream from .errors import ParseError +from .stream import EOL, FluentParserStream -R = TypeVar("R", bound=ast.SyntaxNode) +WithSpanFunc = TypeVar("WithSpanFunc", bound=Callable[..., ast.SyntaxNode]) -def with_span(fn: Callable[..., R]) -> Callable[..., R]: +def with_span(fn: WithSpanFunc) -> WithSpanFunc: def decorated(self: 'FluentParser', ps: FluentParserStream, *args: Any, **kwargs: Any) -> Any: if not self.with_spans: return fn(self, ps, *args, **kwargs) @@ -24,7 +25,7 @@ def decorated(self: 'FluentParser', ps: FluentParserStream, *args: Any, **kwargs node.add_span(start, end) return node - return decorated + return cast(WithSpanFunc, decorated) class FluentParser: @@ -412,7 +413,7 @@ def __init__(self, value: str, start: int, end: int): self.add_span(start, end) def dedent(self, - elements: List[Union[ast.TextElement, ast.Placeable, Indent]], + elements: Iterable[Union[ast.TextElement, ast.Placeable, Indent]], common_indent: int ) -> List[Union[ast.TextElement, ast.Placeable]]: '''Dedent a list of elements by removing the maximum common indent from diff --git a/fluent.syntax/fluent/syntax/serializer.py b/fluent.syntax/fluent/syntax/serializer.py index 68ea89b3..edfaf21f 100644 --- a/fluent.syntax/fluent/syntax/serializer.py +++ b/fluent.syntax/fluent/syntax/serializer.py @@ -1,4 +1,5 @@ from typing import List, Union + from . import ast diff --git a/fluent.syntax/fluent/syntax/stream.py b/fluent.syntax/fluent/syntax/stream.py index 150ac933..377a96c3 100644 --- a/fluent.syntax/fluent/syntax/stream.py +++ b/fluent.syntax/fluent/syntax/stream.py @@ -1,5 +1,6 @@ from typing import Callable, Union -from typing_extensions import Literal +from typing_extensions import Final, Literal + from .errors import ParseError @@ -59,9 +60,9 @@ def skip_to_peek(self) -> None: self.peek_offset = 0 -EOL = '\n' -EOF = None -SPECIAL_LINE_START_CHARS = ('}', '.', '[', '*') +EOL: Final = '\n' +EOF: Final = None +SPECIAL_LINE_START_CHARS: Final = ('}', '.', '[', '*') class FluentParserStream(ParserStream): diff --git a/fluent.syntax/fluent/syntax/visitor.py b/fluent.syntax/fluent/syntax/visitor.py index 0df9f596..7ea78047 100644 --- a/fluent.syntax/fluent/syntax/visitor.py +++ b/fluent.syntax/fluent/syntax/visitor.py @@ -1,5 +1,8 @@ -from typing import Any, List -from .ast import BaseNode, Node +from typing import Any, List, TypeVar + +from .ast import BaseNode + +Node = TypeVar("Node", bound=BaseNode) class Visitor: @@ -46,7 +49,7 @@ def visit(self, node: Any) -> Any: visit = getattr(self, f'visit_{nodename}', self.generic_visit) return visit(node) - def generic_visit(self, node: Node) -> Node: # type: ignore + def generic_visit(self, node: Node) -> Node: # type: ignore[override] for propname, propvalue in vars(node).items(): if isinstance(propvalue, list): new_vals: List[Any] = [] diff --git a/fluent.syntax/setup.cfg b/fluent.syntax/setup.cfg index 4d72ca1e..78e60250 100644 --- a/fluent.syntax/setup.cfg +++ b/fluent.syntax/setup.cfg @@ -12,6 +12,8 @@ max-line-length=120 line_length=120 skip_glob=.tox not_skip=__init__.py +extra_standard_library=typing_extensions, _typeshed [mypy] -strict = True +strict=True +python_version=3.6 diff --git a/fluent.syntax/setup.py b/fluent.syntax/setup.py index 23c17064..f264c860 100644 --- a/fluent.syntax/setup.py +++ b/fluent.syntax/setup.py @@ -27,7 +27,7 @@ packages=['fluent.syntax'], package_data={'fluent.syntax': ['py.typed']}, install_requires=[ - 'typing-extensions>=3.7,<5' + 'typing-extensions>=4.0,<4.2' ], test_suite='tests.syntax' )