Skip to content

Add type hints to fluent.syntax & fluent.runtime #180

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

Merged
merged 11 commits into from
Mar 13, 2023
17 changes: 13 additions & 4 deletions .github/workflows/fluent.runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,35 @@ jobs:
run: |
python -m pip install wheel
python -m pip install --upgrade pip
python -m pip install fluent.syntax==${{ matrix.fluent-syntax }}
python -m pip install fluent.syntax==${{ matrix.fluent-syntax }} six
python -m pip install .
- name: Test
working-directory: ./fluent.runtime
run: |
./runtests.py
lint:
name: flake8
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Install dependencies
working-directory: ./fluent.runtime
run: |
python -m pip install wheel
python -m pip install --upgrade pip
python -m pip install flake8==6
- name: lint
python -m pip install .
python -m pip install flake8==6 mypy==1 types-babel types-pytz
- name: Install latest fluent.syntax
working-directory: ./fluent.syntax
run: |
python -m pip install .
- name: flake8
working-directory: ./fluent.runtime
run: |
python -m flake8
- name: mypy
working-directory: ./fluent.runtime
run: |
python -m mypy fluent/
14 changes: 10 additions & 4 deletions .github/workflows/fluent.syntax.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,29 @@ jobs:
run: |
python -m pip install wheel
python -m pip install --upgrade pip
python -m pip install .
- name: Test
working-directory: ./fluent.syntax
run: |
./runtests.py
syntax:
name: flake8
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: 3.9
- name: Install dependencies
working-directory: ./fluent.syntax
run: |
python -m pip install --upgrade pip
python -m pip install flake8==6
- name: lint
python -m pip install .
python -m pip install flake8==6 mypy==1
- name: flake8
working-directory: ./fluent.syntax
run: |
python -m flake8
- name: mypy
working-directory: ./fluent.syntax
run: |
python -m mypy fluent/
7 changes: 5 additions & 2 deletions fluent.docs/setup.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from setuptools import setup, find_namespace_packages
from setuptools import setup

setup(
name='fluent.docs',
packages=find_namespace_packages(include=['fluent.*']),
packages=['fluent.docs'],
install_requires=[
'typing-extensions>=3.7,<5'
],
)
101 changes: 3 additions & 98 deletions fluent.runtime/fluent/runtime/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,7 @@
import babel
import babel.numbers
import babel.plural

from fluent.syntax import FluentParser
from fluent.syntax.ast import Message, Term
from fluent.syntax.ast import Resource

from .builtins import BUILTINS
from .prepare import Compiler
from .resolver import ResolverEnvironment, CurrentEnvironment
from .utils import native_to_fluent
from .bundle import FluentBundle
from .fallback import FluentLocalization, AbstractResourceLoader, FluentResourceLoader


Expand All @@ -21,94 +14,6 @@
]


def FluentResource(source):
def FluentResource(source: str) -> Resource:
parser = FluentParser()
return parser.parse(source)


class FluentBundle:
"""
Bundles are single-language stores of translations. They are
aggregate parsed Fluent resources in the Fluent syntax and can
format translation units (entities) to strings.

Always use `FluentBundle.get_message` to retrieve translation units from
a bundle. Generate the localized string by using `format_pattern` on
`message.value` or `message.attributes['attr']`.
Translations can contain references to other entities or
external arguments, conditional logic in form of select expressions, traits
which describe their grammatical features, and can use Fluent builtins.
See the documentation of the Fluent syntax for more information.
"""

def __init__(self, locales, functions=None, use_isolating=True):
self.locales = locales
_functions = BUILTINS.copy()
if functions:
_functions.update(functions)
self._functions = _functions
self.use_isolating = use_isolating
self._messages = {}
self._terms = {}
self._compiled = {}
self._compiler = Compiler()
self._babel_locale = self._get_babel_locale()
self._plural_form = babel.plural.to_python(self._babel_locale.plural_form)

def add_resource(self, resource, allow_overrides=False):
# TODO - warn/error about duplicates
for item in resource.body:
if not isinstance(item, (Message, Term)):
continue
map_ = self._messages if isinstance(item, Message) else self._terms
full_id = item.id.name
if full_id not in map_ or allow_overrides:
map_[full_id] = item

def has_message(self, message_id):
return message_id in self._messages

def get_message(self, message_id):
return self._lookup(message_id)

def _lookup(self, entry_id, term=False):
if term:
compiled_id = '-' + entry_id
else:
compiled_id = entry_id
try:
return self._compiled[compiled_id]
except LookupError:
pass
entry = self._terms[entry_id] if term else self._messages[entry_id]
self._compiled[compiled_id] = self._compiler(entry)
return self._compiled[compiled_id]

def format_pattern(self, pattern, args=None):
if args is not None:
fluent_args = {
argname: native_to_fluent(argvalue)
for argname, argvalue in args.items()
}
else:
fluent_args = {}

errors = []
env = ResolverEnvironment(context=self,
current=CurrentEnvironment(args=fluent_args),
errors=errors)
try:
result = pattern(env)
except ValueError as e:
errors.append(e)
result = '{???}'
return [result, errors]

def _get_babel_locale(self):
for lc in self.locales:
try:
return babel.Locale.parse(lc.replace('-', '_'))
except babel.UnknownLocaleError:
continue
# TODO - log error
return babel.Locale.default()
5 changes: 3 additions & 2 deletions fluent.runtime/fluent/runtime/builtins.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from .types import fluent_date, fluent_number
from typing import Any, Callable, Dict
from .types import FluentType, fluent_date, fluent_number

NUMBER = fluent_number
DATETIME = fluent_date


BUILTINS = {
BUILTINS: Dict[str, Callable[[Any], FluentType]] = {
'NUMBER': NUMBER,
'DATETIME': DATETIME,
}
110 changes: 110 additions & 0 deletions fluent.runtime/fluent/runtime/bundle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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
from .prepare import Compiler
from .resolver import CurrentEnvironment, Message, Pattern, ResolverEnvironment
from .utils import native_to_fluent

if TYPE_CHECKING:
from .types import FluentNone, FluentType

PluralCategory = Literal['zero', 'one', 'two', 'few', 'many', 'other']


class FluentBundle:
"""
Bundles are single-language stores of translations. They are
aggregate parsed Fluent resources in the Fluent syntax and can
format translation units (entities) to strings.

Always use `FluentBundle.get_message` to retrieve translation units from
a bundle. Generate the localized string by using `format_pattern` on
`message.value` or `message.attributes['attr']`.
Translations can contain references to other entities or
external arguments, conditional logic in form of select expressions, traits
which describe their grammatical features, and can use Fluent builtins.
See the documentation of the Fluent syntax for more information.
"""

def __init__(self,
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] = {}
# 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())
self._babel_locale = self._get_babel_locale()
self._plural_form = cast(Callable[[Any], Callable[[Union[int, float]], PluralCategory]],
babel.plural.to_python)(self._babel_locale.plural_form)

def add_resource(self, resource: FTL.Resource, allow_overrides: bool = False) -> None:
# TODO - warn/error about duplicates
for item in resource.body:
if not isinstance(item, (FTL.Message, FTL.Term)):
continue
map_ = self._messages if isinstance(item, FTL.Message) else self._terms
full_id = item.id.name
if full_id not in map_ or allow_overrides:
map_[full_id] = item

def has_message(self, message_id: str) -> bool:
return message_id in self._messages

def get_message(self, message_id: str) -> Message:
return self._lookup(message_id)

def _lookup(self, entry_id: str, term: bool = False) -> Message:
if term:
compiled_id = '-' + entry_id
else:
compiled_id = entry_id
try:
return self._compiled[compiled_id]
except LookupError:
pass
entry = self._terms[entry_id] if term else self._messages[entry_id]
self._compiled[compiled_id] = self._compiler(entry)
return self._compiled[compiled_id]

def format_pattern(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)
for argname, argvalue in args.items()
}
else:
fluent_args = {}

errors: List[Exception] = []
env = ResolverEnvironment(context=self,
current=CurrentEnvironment(args=fluent_args),
errors=errors)
try:
result = pattern(env)
except ValueError as e:
errors.append(e)
result = '{???}'
return (result, errors)

def _get_babel_locale(self) -> babel.Locale:
for lc in self.locales:
try:
return babel.Locale.parse(lc.replace('-', '_'))
except babel.UnknownLocaleError:
continue
# TODO - log error
return babel.Locale.default()
8 changes: 5 additions & 3 deletions fluent.runtime/fluent/runtime/errors.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from typing import cast


class FluentFormatError(ValueError):
def __eq__(self, other):
return ((other.__class__ == self.__class__) and
other.args == self.args)
def __eq__(self, other: object) -> bool:
return ((other.__class__ == self.__class__) and cast(ValueError, other).args == self.args)


class FluentReferenceError(FluentFormatError):
Expand Down
Loading