diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 904c9ad..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,105 +0,0 @@ -version: 2.1 - -jobs: - lint: - docker: - # When upgrading the Docker image, make sure the cache key is updated too. - - image: cimg/python:3.11.9 - resource_class: small - steps: - - checkout - - run: - name: Install system Python packages - command: pipx install uv pre-commit - # Installing packages into a virtualenv is useful as it provides an easier target to cache. - # Note that this step needs including in all jobs that install Python packages. - - run: - name: Create virtualenv - command: | - uv venv /home/circleci/venv/ - echo "source /home/circleci/venv/bin/activate" >> $BASH_ENV - - restore_cache: - name: Restore venv cache - keys: - - &cache-key python-3.11.9-packages-v1-{{ checksum "requirements/development.txt" }} - - python-3.11.9-packages-v1- - - run: - name: Create pre-commit cache key - command: | - cp .pre-commit-config.yaml pre-commit-cache-key.txt - python --version >> pre-commit-cache-key.txt - - restore_cache: - name: Restore pre-commit cache - keys: - - &precommit-cache-key pre-commit-v1-{{ checksum "pre-commit-cache-key.txt" }} - - pre-commit-v1- - - run: - name: Install dependencies - command: make dev - - save_cache: - name: Save venv cache - key: *cache-key - paths: - - "~/venv/" - - "~/.cache/pip" - - "~/.cache/uv" - - run: - name: Run ruff formatter - command: make ruff_format - when: always - - run: - name: Run ruff linter - command: make ruff_lint - when: always - - run: - name: Run Mypy - command: make mypy - when: always - - run: - name: Run pre-commit hooks - environment: - # Don't run pre-commit checks which have a dedicated CI step. - # Also, don't complain about commits on the main branch in CI. - SKIP: ruff-lint,ruff-format,no-commit-to-branch - command: pre-commit run --all-files - when: always - - save_cache: - name: Save pre-commit cache - key: *precommit-cache-key - paths: - - "~/.cache/pre-commit" - - store_test_results: - path: test-results - - test: - docker: - # When upgrading the Docker image, make sure the cache key is updated too. - - image: cimg/python:3.11.9 - resource_class: small - steps: - - checkout - - run: - name: Install additional Python versions and system Python packages - command: | - # Install additional Python versions that Nox needs. Note the - # `cimg/python:3.11` image already has Python 3.10 installed. - pyenv install 3.12.3 - pyenv global 3.11.9 3.12.3 - pipx install uv nox - when: always - - run: - name: Run tests - # To start with, it's cost-effective to run all matrix tests in one - # CI job. But as the test suite grows, it will make more sense to - # split the matrix sessions across multiple CI jobs. This can be done - # using CircleCI's matrix jobs functionality to pass in the Nox session name to run. - command: make matrix_test - when: always - - store_test_results: - path: test-results - -workflows: - test-build: - jobs: - - lint - - test diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..bd335ef --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @kraken-tech/open-source-maintainers diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..e3387c8 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,47 @@ +name: Python tests + +on: + pull_request: + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-22.04 + timeout-minutes: 5 + + steps: + - name: Clone the code + uses: actions/checkout@v4 + + - name: Set up Python versions + uses: actions/setup-python@v5 + with: + python-version: | + 3.12 + 3.11 + cache: 'pip' + cache-dependency-path: | + pyproject.toml + requirements/*.txt + noxfile.py + + - name: Make a virtualenv + run: python3 -m venv .venv + + - name: Install requirements + run: | + source .venv/bin/activate + pip install uv==0.1.40 + make install_python_packages + + - name: Run linters + run: | + source .venv/bin/activate + make lint + + - name: Run the tests + run: | + source .venv/bin/activate + nox diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..710045e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,188 @@ +# Contributing + +During development you will also need: + +- `uv` installed as a system package. + +## Local development + +When making changes please remember to update the `CHANGELOG.md`, which follows the guidelines at +[keepachangelog]. Add your changes to the `[Unreleased]` section when you create your PR. + +[keepachangelog]: https://keepachangelog.com/ + +### Installation + +Ensure one of the above Pythons is installed and used by the `python` executable: + +```sh +python --version +Python 3.11.9 # or any of the supported versions +``` + +Ensure `uv` is installed as a system package. This can be done with `pipx` or Homebrew. + +Then create and activate a virtual environment. If you don't have any other way of managing virtual +environments this can be done by running: + +```sh +uv venv +source .venv/bin/activate +``` + +You could also use [virtualenvwrapper], [direnv] or any similar tool to help manage your virtual +environments. + +Once you are in an active virtual environment run + +```sh +make dev +``` + +This will set up your local development environment, installing all development dependencies. + +[virtualenvwrapper]: https://virtualenvwrapper.readthedocs.io/ +[direnv]: https://direnv.net + +### Testing (single Python version) + +To run the test suite using the Python version of your virtual environment, run: + +```sh +make test +``` + +### Testing (all supported Python versions) + +To test against multiple Python (and package) versions, we need to: + +- Have [`nox`][nox] installed outside of the virtualenv. This is best done using `pipx`: + + ```sh + pipx install nox + ``` + +- Ensure that all supported Python versions are installed and available on your system (as e.g. + `python3.10`, `python3.11` etc). This can be done with `pyenv`. + +Then run `nox` with: + +```sh +nox +``` + +Nox will create a separate virtual environment for each combination of Python and package versions +defined in `noxfile.py`. + +To list the available sessions, run: + +```sh +nox --list-sessions +``` + +To run the test suite in a specific Nox session, use: + +```sh +nox -s $SESSION_NAME +``` + +[nox]: https://nox.thea.codes/en/stable/ + +### Static analysis + +Run all static analysis tools with: + +```sh +make lint +``` + +### Auto formatting + +Reformat code to conform with our conventions using: + +```sh +make format +``` + +### Dependencies + +Package dependencies are declared in `pyproject.toml`. + +- _package_ dependencies in the `dependencies` array in the `[project]` section. +- _development_ dependencies in the `dev` array in the `[project.optional-dependencies]` section. + +For local development, the dependencies declared in `pyproject.toml` are pinned to specific +versions using the `requirements/development.txt` lock file. + +#### Adding a new dependency + +To install a new Python dependency add it to the appropriate section in `pyproject.toml` and then +run: + +```sh +make dev +``` + +This will: + +1. Build a new version of the `requirements/development.txt` lock file containing the newly added + package. +2. Sync your installed packages with those pinned in `requirements/development.txt`. + +This will not change the pinned versions of any packages already in any requirements file unless +needed by the new packages, even if there are updated versions of those packages available. + +Remember to commit your changed `requirements/development.txt` files alongside the changed +`pyproject.toml`. + +#### Removing a dependency + +Removing Python dependencies works exactly the same way: edit `pyproject.toml` and then run +`make dev`. + +#### Updating all Python packages + +To update the pinned versions of all packages simply run: + +```sh +make update +``` + +This will update the pinned versions of every package in the `requirements/development.txt` lock +file to the latest version which is compatible with the constraints in `pyproject.toml`. + +You can then run: + +```sh +make dev +``` + +to sync your installed packages with the updated versions pinned in `requirements/development.txt`. + +#### Updating individual Python packages + +Upgrade a single development dependency with: + +```sh +uv pip compile -P $PACKAGE==$VERSION pyproject.toml --extra=dev --output-file=requirements/development.txt +``` + +You can then run: + +```sh +make dev +``` + +to sync your installed packages with the updated versions pinned in `requirements/development.txt`. + +## Versioning + +This project uses [SemVer] for versioning with no additional suffix after the version number. When +it is time for a new release, run the command `make version_{type}` where `{type}` should be +replaced with one of `major`, `minor`, `patch` depending on the type of changes in the release. + +The command will update the version in `pyproject.toml` and move the changes from the "Unreleased" +section of the changelog to a versioned section and create a new "Unreleased" section for future +improvements. + +[semver]: https://semver.org/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f76c455 --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2024, Kraken Technologies Limited + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 954d1f1..255b85f 100644 --- a/README.md +++ b/README.md @@ -1,199 +1,52 @@ # rustfluent -TODO describe the purpose of 'rustfluent' here. +A Python interface to the Rust Fluent Library. + +This project is a small shim around [fluent-rs](https://github.com/projectfluent/fluent-rs), so it +can be used from Python. + +> [!WARNING] +> This package is under active development, and breaking changes may be released at any time. Be sure to pin to +> specific versions if you're using this package in a production environment. ## Prerequisites This package supports: -- Python 3.10 - Python 3.11 - Python 3.12 -During development you will also need: - -- `uv` installed as a system package. -- pre-commit 3 _(Optional, but strongly recommended)_ - -## Local development - -When making changes please remember to update the `CHANGELOG.md`, which follows the guidelines at -[keepachangelog]. Add your changes to the `[Unreleased]` section when you create your PR. - -[keepachangelog]: https://keepachangelog.com/ - -### Installation - -Ensure one of the above Pythons is installed and used by the `python` executable: - -```sh -python --version -Python 3.11.9 # or any of the supported versions -``` - -Ensure `uv` is installed as a system package. This can be done with `pipx` or Homebrew. - -Then create and activate a virtual environment. If you don't have any other way of managing virtual -environments this can be done by running: - -```sh -uv venv -source .venv/bin/activate -``` - -You could also use [virtualenvwrapper], [direnv] or any similar tool to help manage your virtual -environments. - -Once you are in an active virtual environment run - -```sh -make dev -``` - -This will set up your local development environment, installing all development dependencies. - -[virtualenvwrapper]: https://virtualenvwrapper.readthedocs.io/ -[direnv]: https://direnv.net - -### Testing (single Python version) - -To run the test suite using the Python version of your virtual environment, run: - -```sh -make test -``` - -### Testing (all supported Python versions) - -To test against multiple Python (and package) versions, we need to: - -- Have [`nox`][nox] installed outside of the virtualenv. This is best done using `pipx`: +## Installation - ```sh - pipx install nox - ``` - -- Ensure that all supported Python versions are installed and available on your system (as e.g. - `python3.10`, `python3.11` etc). This can be done with `pyenv`. - -Then run `nox` with: - -```sh -nox ``` - -Nox will create a separate virtual environment for each combination of Python and package versions -defined in `noxfile.py`. - -To list the available sessions, run: - -```sh -nox --list-sessions +pip install rustfluent ``` -To run the test suite in a specific Nox session, use: - -```sh -nox -s $SESSION_NAME -``` +## Usage -[nox]: https://nox.thea.codes/en/stable/ +```python +import rustfluent as fluent -### Static analysis +# First load a bundle +bundle = fluent.Bundle( + "en", + [ + # Multiple FTL files can be specified. Entries in later + # files overwrite earlier ones. + "en.ftl", + ], +) -Run all static analysis tools with: +# Fetch a translation +assert bundle.get_translation("hello-world") == "Hello World" -```sh -make lint +# Fetch a translation that takes a keyword argument +assert bundle.get_translation("hello-user", user="Bob") == "Hello, \u2068Bob\u2069" ``` -### Auto formatting - -Reformat code to conform with our conventions using: - -```sh -make format -``` - -### Dependencies - -Package dependencies are declared in `pyproject.toml`. - -- _package_ dependencies in the `dependencies` array in the `[project]` section. -- _development_ dependencies in the `dev` array in the `[project.optional-dependencies]` section. - -For local development, the dependencies declared in `pyproject.toml` are pinned to specific -versions using the `requirements/development.txt` lock file. - -#### Adding a new dependency - -To install a new Python dependency add it to the appropriate section in `pyproject.toml` and then -run: - -```sh -make dev -``` - -This will: - -1. Build a new version of the `requirements/development.txt` lock file containing the newly added - package. -2. Sync your installed packages with those pinned in `requirements/development.txt`. - -This will not change the pinned versions of any packages already in any requirements file unless -needed by the new packages, even if there are updated versions of those packages available. - -Remember to commit your changed `requirements/development.txt` files alongside the changed -`pyproject.toml`. - -#### Removing a dependency - -Removing Python dependencies works exactly the same way: edit `pyproject.toml` and then run -`make dev`. - -#### Updating all Python packages - -To update the pinned versions of all packages simply run: - -```sh -make update -``` - -This will update the pinned versions of every package in the `requirements/development.txt` lock -file to the latest version which is compatible with the constraints in `pyproject.toml`. - -You can then run: - -```sh -make dev -``` - -to sync your installed packages with the updated versions pinned in `requirements/development.txt`. - -#### Updating individual Python packages - -Upgrade a single development dependency with: - -```sh -uv pip compile -P $PACKAGE==$VERSION pyproject.toml --extra=dev --output-file=requirements/development.txt -``` - -You can then run: - -```sh -make dev -``` - -to sync your installed packages with the updated versions pinned in `requirements/development.txt`. - -## Versioning - -This project uses [SemVer] for versioning with no additional suffix after the version number. When -it is time for a new release, run the command `make version_{type}` where `{type}` should be -replaced with one of `major`, `minor`, `patch` depending on the type of changes in the release. +The Unicode characters around "Bob" in the above example are for +[Unicode bidirectional handling](https://www.unicode.org/reports/tr9/). -The command will update the version in `pyproject.toml` and move the changes from the "Unreleased" -section of the changelog to a versioned section and create a new "Unreleased" section for future -improvements. +## Contributing -[semver]: https://semver.org/ +See [Contributing](./CONTRIBUTING.md). diff --git a/noxfile.python-only.py b/noxfile.py similarity index 95% rename from noxfile.python-only.py rename to noxfile.py index b703f27..e4c828f 100644 --- a/noxfile.python-only.py +++ b/noxfile.py @@ -18,7 +18,6 @@ @nox.parametrize( "python", [ - nox.param("3.10", id="python=3.10"), nox.param("3.11", id="python=3.11"), nox.param("3.12", id="python=3.12"), ], diff --git a/noxfile.with-packages.py b/noxfile.with-packages.py deleted file mode 100644 index 7362c03..0000000 --- a/noxfile.with-packages.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -This `noxfile.py` is configured to run the test suite with multiple versions of Python and multiple -versions of Django (used as an example). -""" - -import contextlib -import os -import tempfile -from typing import IO, Generator - -import nox - - -# Use uv to manage venvs. -nox.options.default_venv_backend = "uv" - - -@contextlib.contextmanager -def temp_constraints_file() -> Generator[IO[str], None, None]: - with tempfile.NamedTemporaryFile(mode="w", prefix="constraints.", suffix=".txt") as f: - yield f - - -@contextlib.contextmanager -def temp_lock_file() -> Generator[IO[str], None, None]: - with tempfile.NamedTemporaryFile(mode="w", prefix="packages.", suffix=".txt") as f: - yield f - - -@nox.session() -@nox.parametrize( - "package_constraint", - [ - nox.param("django>=4.2,django<4.3", id="django=4.2.X"), - nox.param("django>=5.0,django<5.1", id="django=5.0.X"), - ], -) -@nox.parametrize( - "python", - [ - nox.param("3.10", id="python=3.10"), - nox.param("3.11", id="python=3.11"), - nox.param("3.12", id="python=3.12"), - ], -) -def tests(session: nox.Session, package_constraint: str) -> None: - """ - Run the test suite. - """ - with temp_constraints_file() as constraints_file, temp_lock_file() as lock_file: - # Create a constraints file with the parameterized package versions. - # It's easy to add more constraints here if needed. - constraints_file.write(f"{package_constraint}\n") - - # Compile a new development lock file with the additional package constraints from this - # session. Use a unique lock file name to avoid session pollution. - session.run( - "uv", - "pip", - "compile", - "--quiet", - "--strip-extras", - "--extra=dev", - "pyproject.toml", - "--constraint", - constraints_file.name, - "--output-file", - lock_file.name, - ) - - # Install the dependencies from the newly compiled lockfile and main package. - session.install("-r", lock_file.name, ".") - - # When run in CircleCI, create JUnit XML test results. - commands = ["pytest"] - if "CIRCLECI" in os.environ: - commands.append(f"--junitxml=test-results/junit.{session.name}.xml") - - session.run(*commands, *session.posargs) diff --git a/pyproject.toml b/pyproject.toml index 2d120b9..d935bc7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,8 +14,8 @@ where = ["src"] [tool.setuptools.package-data] # Include the root-package `py.typed` file so Mypy uses inline type annotations. -"kraken.rustfluent" = [ - "kraken/rustfluent/py.typed", +"rustfluent" = [ + "rustfluent/py.typed", ] # Project @@ -23,7 +23,7 @@ where = ["src"] [project] name = "rustfluent" -requires-python = ">=3.10" +requires-python = ">=3.11" # Do not manually edit the version, use `make version_{type}` instead. # This should match the version in the [tool.bumpversion] section. @@ -51,6 +51,9 @@ dev = [ # Versioning "bump-my-version", + + # Workflow + "pre-commit", ] # Ruff @@ -80,7 +83,6 @@ ignore = [ "**/__init__.py" = [ "F401" ] [tool.ruff.lint.isort] -known-first-party = ["kraken"] lines-after-imports = 2 section-order = [ "future", @@ -93,7 +95,7 @@ section-order = [ [tool.ruff.lint.isort.sections] "project" = [ - "kraken.rustfluent", + "rustfluent", "tests", ] @@ -103,10 +105,7 @@ section-order = [ [tool.mypy] files = "." exclude = "build/" -# Mypy doesn't understand how to handle namespace packages unless we run it with -# the `explicit_package_bases` flag and specify where all the packages are -# located. The "$MYPY_CONFIG_FILE_DIR/src" path allows Mypy to find the "kraken" -# namespace package. The "$MYPY_CONFIG_FILE_DIR" path allows Mypy to find the +# The "$MYPY_CONFIG_FILE_DIR" path allows Mypy to find the # "tests" package and noxfile.py. explicit_package_bases = true mypy_path = [ diff --git a/requirements/development.txt b/requirements/development.txt new file mode 100644 index 0000000..2eb67c5 --- /dev/null +++ b/requirements/development.txt @@ -0,0 +1,97 @@ +# This file was autogenerated by uv via the following command: +# uv pip compile pyproject.toml --extra=dev --output-file=requirements/development.txt +annotated-types==0.7.0 + # via pydantic +argcomplete==3.5.0 + # via nox +bracex==2.5 + # via wcmatch +build==1.2.2 + # via rustfluent (pyproject.toml) +bump-my-version==0.26.0 + # via rustfluent (pyproject.toml) +cfgv==3.4.0 + # via pre-commit +click==8.1.7 + # via + # bump-my-version + # rich-click +colorlog==6.8.2 + # via nox +distlib==0.3.8 + # via virtualenv +filelock==3.16.0 + # via virtualenv +identify==2.6.0 + # via pre-commit +iniconfig==2.0.0 + # via pytest +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +mypy==1.11.2 + # via rustfluent (pyproject.toml) +mypy-extensions==1.0.0 + # via mypy +nodeenv==1.9.1 + # via pre-commit +nox==2024.4.15 + # via rustfluent (pyproject.toml) +packaging==24.1 + # via + # build + # nox + # pytest +platformdirs==4.3.2 + # via virtualenv +pluggy==1.5.0 + # via pytest +pre-commit==3.8.0 + # via rustfluent (pyproject.toml) +prompt-toolkit==3.0.36 + # via questionary +pydantic==2.9.1 + # via + # bump-my-version + # pydantic-settings +pydantic-core==2.23.3 + # via pydantic +pydantic-settings==2.5.2 + # via bump-my-version +pygments==2.18.0 + # via rich +pyproject-hooks==1.1.0 + # via build +pytest==8.3.3 + # via rustfluent (pyproject.toml) +python-dotenv==1.0.1 + # via pydantic-settings +pyyaml==6.0.2 + # via pre-commit +questionary==2.0.1 + # via bump-my-version +rich==13.8.1 + # via + # bump-my-version + # rich-click +rich-click==1.8.3 + # via bump-my-version +ruff==0.6.4 + # via rustfluent (pyproject.toml) +tomlkit==0.13.2 + # via bump-my-version +typing-extensions==4.12.2 + # via + # mypy + # pydantic + # pydantic-core + # rich-click +virtualenv==20.26.4 + # via + # nox + # pre-commit +wcmatch==9.0 + # via bump-my-version +wcwidth==0.2.13 + # via prompt-toolkit diff --git a/src/rustfluent/__init__.py b/src/rustfluent/__init__.py index e69de29..2f37847 100644 --- a/src/rustfluent/__init__.py +++ b/src/rustfluent/__init__.py @@ -0,0 +1,3 @@ +""" +A Python interface to the Rust Fluent Library. +"""