Skip to content

Remove attrs, type checking #35

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 42 commits into from
May 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
0d16996
Fixed setup and README for running benchmarks
spookylukey Apr 7, 2025
b282960
Removed old requirements files
spookylukey Apr 7, 2025
a2fc4d8
Added .editorconfig
spookylukey Apr 9, 2025
3b79838
Switched FluentResource to be a dataclass
spookylukey Apr 9, 2025
57755d3
Added FormatStyle and CurrencyDisplay enums
spookylukey Apr 9, 2025
fd84b38
Added DateStyle and TimeStyle type aliases
spookylukey Apr 9, 2025
ca114a2
Switch NumberFormatOptions to be a dataclass
spookylukey Apr 9, 2025
22ded7b
Switch DateFormatOptions to be a dataclass
spookylukey Apr 9, 2025
44aed3f
Switch CurrentEnvironment to be a dataclass
spookylukey Apr 10, 2025
aa97eac
Switch CompilerEnvironment to dataclass
spookylukey Apr 10, 2025
87615e1
Switch CompiledFtl to dataclass
spookylukey Apr 10, 2025
9900cfe
Switch from attrs to dataclass in generate_ftl_file.py
spookylukey Apr 10, 2025
fa98ce2
monkeytype apply fluent_compiler.utils
spookylukey Apr 10, 2025
7e60890
monkeytype apply fluent_compiler.types
spookylukey Apr 10, 2025
bcd482e
Stray constant that should be enum value
spookylukey Apr 10, 2025
72ecf20
monkeytype apply fluent_compiler.runtime, with fixes
spookylukey Apr 10, 2025
c15e4e3
monkeytype apply fluent_compiler.resource with fixes
spookylukey Apr 10, 2025
1273b16
monkeytype apply fluent_compiler.bundle with fixes
spookylukey Apr 10, 2025
7b9b8e5
ruff fixes
spookylukey Apr 10, 2025
0dcefec
monkeytype apply fluent_compiler.escapers with fixes
spookylukey Apr 10, 2025
383e113
Type fixes
spookylukey Apr 10, 2025
0e978f8
Different import alias for clarity
spookylukey Apr 10, 2025
0744058
Type hints in codegen
spookylukey Apr 11, 2025
47d10b0
More type hints in codegen
spookylukey Apr 12, 2025
89bc926
More type hints in codegen
spookylukey Apr 12, 2025
5f73961
More type hints...
spookylukey Apr 12, 2025
b9ce6e5
Temp rename to help out monkeytype
spookylukey Apr 12, 2025
bb76d52
Type hints in compiler.py
spookylukey Apr 12, 2025
d996a4c
More type hint stuff, incomplete
spookylukey Apr 12, 2025
2a09aed
More type stuff
spookylukey Apr 12, 2025
e771bd0
Type improvements regarding escapers
spookylukey Apr 14, 2025
8a65c92
More type improvements for escapers, with docs
spookylukey Apr 14, 2025
eb5e507
Type improvements - possible unbound
spookylukey Apr 15, 2025
2aa5291
Type improvements
spookylukey Apr 15, 2025
9eaf10b
Update history
spookylukey Apr 15, 2025
2263f22
Type improvements in codegen.py
spookylukey Apr 15, 2025
fef1ca0
Resolved cyclic dependency
spookylukey Apr 28, 2025
06ce8b8
Typing improvement
spookylukey Apr 28, 2025
3cc7c6d
Fixes for type signatures relating to Escaper/IsEscaper
spookylukey Apr 28, 2025
bc73511
Prep for converting unittest to pytest style tests
spookylukey Apr 28, 2025
683dab5
Convert test_escapers to pytest style
spookylukey Apr 28, 2025
dfd3240
Tests for some error messages
spookylukey Apr 28, 2025
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: 15 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
end_of_line = lf

[*.{yml,yaml}]
indent_size = 2


[*.json]
indent_size = 2
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ repos:
- id: trailing-whitespace
- id: end-of-file-fixer
- repo: https://github.com/charliermarsh/ruff-pre-commit
rev: v0.4.5
rev: v0.11.4
hooks:
- id: ruff
args: [ --fix, --exit-non-zero-on-fix ]
Expand Down
29 changes: 29 additions & 0 deletions ARCHITECTURE.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,35 @@ FTL functions.
Other related level classes for the user are provided in
``fluent_compiler.resource`` and ``fluent_compiler.escapers``.

AST Types
~~~~~~~~~

As we are translating from one language to another, it is easy to get confused
about types of Abstract Syntax Tree objects in the different languages,
especially as sometimes we use identical class names. Here is a quick overview:

- FTL (Fluent Translation List) has its own AST types. In the ``compiler.py``
module, these are imported as ``fl_ast``. So, for example,
``fl_ast.VariableReference`` is an AST node representing a variable reference
in a Fluent document.

- Python AST. This is the end product we generate. It is imported directly into
the ``ast_compat.py`` module. From there it is imported into the
``codegen.py`` module as ``py_ast``.

- Codegen AST. We have our own layer of classes for Python code generation which
are used by the ``compiler.py`` module, which represent a simplified Python
AST with conveniences for easier construction, and eventually emit Python AST.
The base classes used here are ``CodeGenAst`` and ``CodeGenAstList``.

This module is imported into ``compiler.py`` as ``codegen``.

So, for example, in ``compiler.py`` you find both ``fl_ast.VariableReference``
and ``codegen.VariableReference``. In ``codegen.py`` you find both ``If`` (the
``If`` AST node defined in the ``codgen.py`` module) and ``py_ast.If`` (the
Python AST node from Python stdlib). If you get lost remember which module/layer
you are in.

Tests
~~~~~

Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ fluent_compiler 1.2 (unreleased)

* Dropped Python 3.7 support
* Compiler performance improvements - thanks `@leamingrad <https://github.com/leamingrad>`_.
* Switched from `attrs <https://www.attrs.org/en/stable/index.html>`_ to stdlib
`dataclasses <https://docs.python.org/3/library/dataclasses.html>`_, and added
lots of type signatures and cleaned up internally.
* The documented API is still exactly the same as it was. However, if you were
depending on implementation details, like the fact that ``CompiledFtl`` was
an attrs dataclass when it is now a stdlib dataclass, there may be some
small backwards incompatibilities. In addition if you are running a type
checker, you may notice some differences as more things will be checked.

fluent_compiler 1.1 (2024-04-02)
--------------------------------
Expand Down
27 changes: 14 additions & 13 deletions docs/escaping.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,19 @@ passed to the ``FluentBundle`` constructor or to ``compile_messages``.

An ``escaper`` is an object that defines the following set of attributes. The
object could be a module, or a simple namespace object you could create using
``types.SimpleNamespace``, or an instance of a class with appropriate
methods defined. The attributes are:
``types.SimpleNamespace`` or the provided
:class:`fluent_compiler.escapers.Escaper` dataclass, or an instance of a class
with appropriate methods defined. The attributes are:

- ``name`` - a simple text value that is used in error messages.
- ``name: str`` - a simple text value that is used in error messages.

- ``select(**hints)``

A callable that is used to decide whether or not to use this escaper for a
given message (or message attribute). It is passed a number of hints as
keyword arguments, currently only the following:

- ``message_id`` - a string that is the name of the message or term. For terms
- ``message_id: str`` - a string that is the name of the message or term. For terms
it is a string with a leading dash - e.g. ``-brand-name``. For message
attributes, it is a string in the form ``messsage-name.attribute-name``

Expand All @@ -48,10 +49,10 @@ methods defined. The attributes are:
``select`` callable of each escaper in the list of escapers is tried in turn,
and the first to return ``True`` is used.

- ``output_type`` - the type of values that are returned by ``escape``,
- ``output_type: type`` - the type of values that are returned by ``escape``,
``mark_escape``, and ``join``, and therefore by the whole message.

- ``escape(text_to_be_escaped)``
- ``escape(text_to_be_escaped: str)``

A callable that will escape the passed in text. It must return a value that is
an instance of ``output_type`` (or a subclass).
Expand All @@ -64,24 +65,24 @@ methods defined. The attributes are:
A callable that marks the passed in text as markup i.e. already escaped. It
must return a value that is an instance of ``output_type`` (or a subclass).

- ``join(parts)``
- ``join(parts: Iterable)``

A callable that accepts an iterable of components, each of type
``output_type``, and combines them into a larger value of the same type.

- ``use_isolating``
- ``use_isolating: bool | None``

A boolean that determines whether the normal bidi isolating characters should
be inserted. If it is ``None`` the value from the ``FluentBundle`` will be
used, otherwise use ``True`` or ``False`` to override.

The escaping functions need to obey some rules:

- escape must be idempotent:
- ``escape`` must be idempotent:

``escape(escape(text)) == escape(text)``

- escape must be a no-op on the output of ``mark_escaped``:
- ``escape`` must be a no-op on the output of ``mark_escaped``:

``escape(mark_escaped(text)) == mark_escaped(text)``

Expand All @@ -101,13 +102,13 @@ This example is for

.. code-block:: python

from fluent_compiler.utils import SimpleNamespace
from fluent_compiler.escapers import Escaper
from markupsafe import Markup, escape

empty_markup = Markup('')

html_escaper = SimpleNamespace(
select=lambda message_id=None, **hints: message_id.endswith('-html'),
html_escaper = Escaper(
select=lambda message_id, **hints: message_id.endswith('-html'),
output_type=Markup,
mark_escaped=Markup,
escape=escape,
Expand Down
11 changes: 8 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,11 @@ classifiers = [
dynamic = ["version"]

dependencies = [
"attrs>=19.3.0",
"babel>=2.12.0",
"backports-strenum>=1.2.4 ; python_full_version < '3.11'",
"fluent-syntax>=0.14",
"pytz>=2025.2",
"typing-extensions>=4.13.0 ; python_full_version < '3.10'",
]

[project.urls]
Expand Down Expand Up @@ -75,7 +76,8 @@ target-version = ['py39']

[tool.ruff]
line-length = 120
target-version = 'py37'
target-version = 'py38'


[tool.ruff.lint]
ignore = ["E501","E731"]
Expand All @@ -92,11 +94,14 @@ known-first-party = ["fluent_compiler"]
dev = [
"ast-decompiler>=0.8",
"beautifulsoup4>=4.7.1",
"fluent-runtime>=0.4.0",
"hypothesis>=4.9.0",
"ipython>=8.12.3",
"markdown>=3.0.1",
"markupsafe>=1.1.1",
"pre-commit>=3.5.0",
"pytest>=7.4.4",
"tox-uv>=1.13.1",
"pytest-benchmark>=4.0.0",
"tox>=4.25.0",
"tox-uv>=1.13.1",
]
7 changes: 0 additions & 7 deletions requirements-test.txt

This file was deleted.

9 changes: 0 additions & 9 deletions requirements.txt

This file was deleted.

46 changes: 31 additions & 15 deletions src/fluent_compiler/ast_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def NewAst(...):
"""
import ast
import sys
from typing import TypedDict, TypeVar

# This is a very limited subset of Python AST:
# - only the things needed by codegen.py
Expand Down Expand Up @@ -61,21 +62,36 @@ def NewAst(...):
arg = ast.arg
keyword = ast.keyword
walk = ast.walk
Constant = ast.Constant
AST = ast.AST
stmt = ast.stmt
expr = ast.expr


if sys.version_info >= (3, 8):
Constant = ast.Constant
# `compile` builtin needs these attributes on AST nodes.
# It's hard to get something sensible we can put for line/col numbers so we put arbitrary values.


class DefaultAstArgs(TypedDict):
lineno: int
col_offset: int


DEFAULT_AST_ARGS: DefaultAstArgs = {"lineno": 1, "col_offset": 1}
# Some AST types have different requirements:
DEFAULT_AST_ARGS_MODULE = dict()
DEFAULT_AST_ARGS_ADD = dict()
DEFAULT_AST_ARGS_ARGUMENTS = dict()

T = TypeVar("T")


if sys.version_info < (3, 9):
# Old versions need an `Index` object here:
def subscript_slice_object(value: T) -> T:
return ast.Index(value, **DEFAULT_AST_ARGS)

else:
# For Python 3.7, in terms of runtime behaviour we could also use
# Constant for Str/Num, but this seems to trigger bugs when decompiling with
# ast_decompiler, which is needed by tests. So we use the more normal
# ast that Python 3.7 use for this code.
def Constant(arg, **kwargs):
if isinstance(arg, str):
return ast.Str(arg, **kwargs)
elif isinstance(arg, (int, float)):
return ast.Num(arg, **kwargs)
elif arg is None:
return ast.NameConstant(arg, **kwargs)
else:
raise NotImplementedError(f"Constant not implemented for args of type {type(arg)}")
# New versions need nothing.
def subscript_slice_object(value: T) -> T:
return value
32 changes: 26 additions & 6 deletions src/fluent_compiler/bundle.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
from .compiler import compile_messages
from __future__ import annotations

from typing import Any, Callable, Sequence

from fluent_compiler.escapers import IsEscaper

from .compiler import CompilationErrorItem, compile_messages
from .resource import FtlResource
from .utils import ATTRIBUTE_SEPARATOR, TERM_SIGIL

Expand All @@ -16,7 +22,14 @@ class FluentBundle:

"""

def __init__(self, locale, resources, functions=None, use_isolating=True, escapers=None):
def __init__(
self,
locale: str,
resources: Sequence[FtlResource],
functions: dict[str, Callable] | None = None,
use_isolating: bool = True,
escapers: Sequence[IsEscaper] | None = None,
):
self.locale = locale
compiled_ftl = compile_messages(
locale,
Expand All @@ -29,7 +42,14 @@ def __init__(self, locale, resources, functions=None, use_isolating=True, escape
self._compilation_errors = compiled_ftl.errors

@classmethod
def from_string(cls, locale, text, functions=None, use_isolating=True, escapers=None):
def from_string(
cls,
locale: str,
text: str,
functions: dict[str, Callable] | None = None,
use_isolating: bool = True,
escapers: Sequence[IsEscaper] | None = None,
) -> FluentBundle:
return cls(
locale,
[FtlResource.from_string(text)],
Expand All @@ -48,14 +68,14 @@ def from_files(cls, locale, filenames, functions=None, use_isolating=True, escap
escapers=escapers,
)

def has_message(self, message_id):
def has_message(self, message_id: str) -> bool:
if message_id.startswith(TERM_SIGIL) or ATTRIBUTE_SEPARATOR in message_id:
return False
return message_id in self._compiled_messages

def format(self, message_id, args=None):
def format(self, message_id: str, args: Any | None = None) -> Any:
errors = []
return self._compiled_messages[message_id](args, errors), errors

def check_messages(self):
def check_messages(self) -> list[CompilationErrorItem]:
return self._compilation_errors
Loading
Loading