Contributing to fluent-compiler

Issues

You can help by filing bugs on GitHub: https://github.com/django-ftl/fluent-compiler/issues.

Please check existing issues before filing a new one.

Development environment

To contribute fixes and features, you’ll need to get set up for development:

  1. Fork fluent_compiler on GitHub.

  2. Clone and go to the forked repository.

  3. Create and activate a virtual environment for development (or your preferred mechanism for isolated Python environments).

  4. Install the package in development mode:

    pip install -e .
    
  5. Install test requirements:

    pip install -r requirements-test.txt
    
  6. Run the tests:

    pytest
    

If all that is successful, you are in good shape to start developing!

We also have several linters and code formatters that we require use of, including ruff and black. These are most easily added by using pre-commit:

  • Install pre-commit globally e.g. pipx install pre-commit if you already have pipx.

  • Do pre-commit install in the repo.

Now all the linters will run when you commit changes.

To run tests on multiple Python versions locally you can also install and use tox.

Fixes and features

Please submit fixes and features by:

  1. First creating a branch for your changes.

  2. Sending us a PR on GitHub.

For new features it is often better to open an issue first to see if we agree that the feature is a good idea, before spending a lot of time implementing it.

Architecture of fluent-compiler

The following is a brief, very high-level overview of what fluent-compiler does and the various layers.

Our basic strategy is that we take an FTL file like this:

hello-user = Hello { $username }!

welcome = { hello-user } Welcome to { -app-name }.

-app-name = Acme CMS

and compile it to Python functions, something roughly like this:

def hello_user(args, errors):
    try:
        username = args['username']
    except KeyError:
        username = "username"
        errors.append(FluentReferenceError("Unknown external: username"))
    return f"Hello {username}"


def welcome(args, errors):
    return f"{hello_user(args, errors)} Welcome to Acme CMS."

We then need to store these message functions in some dictionary-like object, to allow us to call them.

message_functions = {
    'hello-user': hello_user,
    'welcome': welcome,
}

To actually format a message we have to do something like:

errors = []
formatted_message = message_functions['hello-user']({'username': 'guest'}, errors)
return formatted_message, errors

Note a few things:

  • Each message becomes a Python function.

  • Message references are handled by calling other message functions.

  • We do lots of optimizations at compile time to heavily simplify the expressions that are evaluated at runtime, including things like inlining terms.

  • We have to handle possible errors in accordance with the Fluent philosophy. Where possible we detect errors at compile time, in addition to the runtime handling shown above.

We do not, in fact, generate Python code as a string, but instead generate AST which we can convert to executable Python functions using the builtin functions compile and exec.

Layers

The highest level code, which can be used as an entry point by users, is in fluent_compiler.bundle. The interface provided here, however, is meant mainly for demonstration purposes, since it is expected that in many circumstances the next level down will be used. For example, django-ftl by-passes this module and uses the next layer down.

The next layer is fluent_compiler.compiler, which handles actual compilation, converting FTL expressions (i.e. FTL AST nodes) into Python code. The bulk of the FTL specific logic is found here. See especially the comments on compile_expr.

For generating Python code, it uses the classes provided by the fluent_compiler.codegen module. These are simplified versions of various Python constructs, with an interface that makes it easy for the compiler module to construct correct code without worrying about lower level details.

The classes in the codegen module eventually need to produce AST objects that can be passed to Python’s builtin compile function. The stdlib ast module has incompatible differences between different Python versions, so we abstract over these in fluent_compiler.ast_compat which allows the codegen module to almost entirely ignore the differences in AST for different Python.

In addition to these modules, there are some runtime functions and types that are needed by the generated Python code, found in fluent_compiler.runtime.

The fluent_compiler.types module contains types for handling number/date formatting - these are used directly by users of fluent_compiler, as well as internally for implementing things like the NUMBER and DATETIME builtin FTL functions.

Other related level classes for the user are provided in fluent_compiler.resource and fluent_compiler.escapers.

Tests

The highest level tests are in tests/format/. These are essentially functional tests that ensure we produce correct output at runtime.

In addition we have many tests of the lower layers of code. These include a lot of tests for our optimizations, many of which work at the level of examining the generated Python code.

We also have benchmarking tests in tools.