Skip to content

Commit 3dc1946

Browse files
authored
Merge pull request #6 from kraken-tech/dev-ergonomics
Dev ergonomics
2 parents cc44747 + ded2332 commit 3dc1946

15 files changed

+155
-113
lines changed

CONTRIBUTING.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,23 @@ This will set up your local development environment, installing all development
4747

4848
### Testing (single Python version)
4949

50-
To run the test suite using the Python version of your virtual environment, run:
50+
Run all Rust and Python tests (within your specific virtual environment) using this command:
5151

5252
```sh
5353
make test
5454
```
5555

56+
This will recompile the Rust code (if necessary) before running the tests.
57+
58+
#### Using pytest directly
59+
60+
You can also run tests using pytest directly (e.g. via your IDE), but you will need to recompile the Rust code after
61+
any changes, otherwise they won't get picked up. You can recompile by running:
62+
63+
```sh
64+
maturin develop
65+
```
66+
5667
### Testing (all supported Python versions)
5768

5869
To test against multiple Python (and package) versions, we need to:

README.md

+58-4
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ pip install rustfluent
2525
## Usage
2626

2727
```python
28-
import rustfluent as fluent
28+
import rustfluent
2929

3030
# First load a bundle
31-
bundle = fluent.Bundle(
31+
bundle = rustfluent.Bundle(
3232
"en",
3333
[
3434
# Multiple FTL files can be specified. Entries in later
@@ -40,13 +40,67 @@ bundle = fluent.Bundle(
4040
# Fetch a translation
4141
assert bundle.get_translation("hello-world") == "Hello World"
4242

43-
# Fetch a translation that takes a keyword argument
44-
assert bundle.get_translation("hello-user", user="Bob") == "Hello, \u2068Bob\u2069"
43+
# Fetch a translation that includes variables
44+
assert bundle.get_translation("hello-user", variables={"user": "Bob"}) == "Hello, \u2068Bob\u2069"
4545
```
4646

4747
The Unicode characters around "Bob" in the above example are for
4848
[Unicode bidirectional handling](https://www.unicode.org/reports/tr9/).
4949

50+
## API reference
51+
52+
### `Bundle` class
53+
54+
A set of translations for a specific language.
55+
56+
```python
57+
import rustfluent
58+
59+
bundle = rustfluent.Bundle(
60+
language="en-US",
61+
ftl_files=[
62+
"/path/to/messages.ftl",
63+
"/path/to/more/messages.ftl",
64+
],
65+
)
66+
```
67+
68+
#### Parameters
69+
70+
| Name | Type | Description |
71+
|-------------|-------------|-------------------------------------------------------------------------------------------------------------------------|
72+
| `language` | `str` | [Unicode Language Identifier](https://unicode.org/reports/tr35/tr35.html#Unicode_language_identifier) for the language. |
73+
| `ftl_files` | `list[str]` | Full paths to the FTL files containing the translations. Entries in later files overwrite earlier ones. |
74+
75+
#### Raises
76+
77+
- `FileNotFoundError` if any of the FTL files could not be found.
78+
- `ValueError` if any of the FTL files contain errors.
79+
80+
### `Bundle.get_translation`
81+
82+
```
83+
>>> bundle.get_translation(identifier="hello-world")
84+
"Hello, world!"
85+
>>> bundle.get_translation(identifier="hello-user", variables={"user": "Bob"})
86+
"Hello, Bob!"
87+
```
88+
89+
#### Parameters
90+
91+
| Name | Type | Description |
92+
|--------------|----------------------------|------------------------------------------------------------------------------------------------------------|
93+
| `identifier` | `str` | The identifier for the Fluent message. |
94+
| `variables` | `dict[str, str]`, optional | Any [variables](https://projectfluent.org/fluent/guide/variables.html) to be passed to the Fluent message. |
95+
96+
#### Return value
97+
98+
`str`: the translated message.
99+
100+
#### Raises
101+
102+
- `ValueError` if the message could not be found or has no translation available.
103+
50104
## Contributing
51105

52106
See [Contributing](./CONTRIBUTING.md).

makefile

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ dev: install_python_packages .git/hooks/pre-commit
1414
.PHONY:test
1515
test:
1616
cargo test
17+
maturin develop
1718
pytest
1819

1920
.PHONY:matrix_test

pyproject.toml

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ dev = [
4444
"bump-my-version",
4545

4646
# Workflow
47+
"pip", # Without this, `maturin develop` won't work.
4748
"pre-commit",
4849
"maturin",
4950
]

requirements/development.txt

+2-5
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ argcomplete==3.5.0
66
# via nox
77
bracex==2.5
88
# via wcmatch
9-
build==1.2.2
10-
# via rustfluent (pyproject.toml)
119
bump-my-version==0.26.0
1210
# via rustfluent (pyproject.toml)
1311
cfgv==3.4.0
@@ -42,9 +40,10 @@ nox==2024.4.15
4240
# via rustfluent (pyproject.toml)
4341
packaging==24.1
4442
# via
45-
# build
4643
# nox
4744
# pytest
45+
pip==24.2
46+
# via rustfluent (pyproject.toml)
4847
platformdirs==4.3.2
4948
# via virtualenv
5049
pluggy==1.5.0
@@ -63,8 +62,6 @@ pydantic-settings==2.5.2
6362
# via bump-my-version
6463
pygments==2.18.0
6564
# via rich
66-
pyproject-hooks==1.1.0
67-
# via build
6865
pytest==8.3.3
6966
# via rustfluent (pyproject.toml)
7067
python-dotenv==1.0.1

src/lib.rs

+8-8
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ struct Bundle {
2121
#[pymethods]
2222
impl Bundle {
2323
#[new]
24-
fn new(namespace: &str, ftl_filenames: &'_ Bound<'_, PyList>) -> PyResult<Self> {
25-
let langid_en: LanguageIdentifier = namespace.parse().expect("Parsing failed");
26-
let mut bundle = FluentBundle::new_concurrent(vec![langid_en]);
24+
fn new(language: &str, ftl_filenames: &'_ Bound<'_, PyList>) -> PyResult<Self> {
25+
let langid: LanguageIdentifier = language.parse().expect("Parsing failed");
26+
let mut bundle = FluentBundle::new_concurrent(vec![langid]);
2727

2828
for file_path in ftl_filenames.iter() {
2929
let path_string = file_path.to_string();
@@ -47,11 +47,11 @@ impl Bundle {
4747
Ok(Self { bundle })
4848
}
4949

50-
#[pyo3(signature = (identifier, **kwargs))]
50+
#[pyo3(signature = (identifier, variables=None))]
5151
pub fn get_translation(
5252
&self,
5353
identifier: &str,
54-
kwargs: Option<&Bound<'_, PyDict>>,
54+
variables: Option<&Bound<'_, PyDict>>,
5555
) -> PyResult<String> {
5656
let msg = match self.bundle.get_message(identifier) {
5757
Some(m) => m,
@@ -71,9 +71,9 @@ impl Bundle {
7171

7272
let mut args = FluentArgs::new();
7373

74-
if let Some(kwargs) = kwargs {
75-
for kwarg in kwargs {
76-
args.set(kwarg.0.to_string(), kwarg.1.to_string());
74+
if let Some(variables) = variables {
75+
for variable in variables {
76+
args.set(variable.0.to_string(), variable.1.to_string());
7777
}
7878
}
7979

src/rustfluent.pyi

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
class Bundle:
2-
def __init__(self, namespace: str, ftl_filenames: list[str]) -> None: ...
3-
def get_translation(self, identifier: str, **kwargs: str) -> str: ...
2+
def __init__(self, language: str, ftl_filenames: list[str]) -> None: ...
3+
def get_translation(self, identifier: str, variables: dict[str, str] | None = None) -> str: ...
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

tests/rustfluent/__init__.py

Whitespace-only changes.

tests/rustfluent/test_python_interface.py

-93
This file was deleted.
File renamed without changes.

tests/test_python_interface.py

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#!/usr/bin/env python
2+
3+
import pathlib
4+
5+
import pytest
6+
7+
import rustfluent as fluent
8+
9+
10+
data_dir = pathlib.Path(__file__).parent.resolve() / "data"
11+
12+
13+
def test_en_basic():
14+
bundle = fluent.Bundle("en", [str(data_dir / "en.ftl")])
15+
assert bundle.get_translation("hello-world") == "Hello World"
16+
17+
18+
def test_en_basic_with_named_arguments():
19+
bundle = fluent.Bundle(
20+
language="en",
21+
ftl_filenames=[str(data_dir / "en.ftl")],
22+
)
23+
assert bundle.get_translation("hello-world") == "Hello World"
24+
25+
26+
def test_en_with_args():
27+
bundle = fluent.Bundle("en", [str(data_dir / "en.ftl")])
28+
assert (
29+
bundle.get_translation("hello-user", variables={"user": "Bob"}) == "Hello, \u2068Bob\u2069"
30+
)
31+
32+
33+
def test_fr_basic():
34+
bundle = fluent.Bundle("fr", [str(data_dir / "fr.ftl")])
35+
assert bundle.get_translation("hello-world") == "Bonjour le monde!"
36+
37+
38+
def test_fr_with_args():
39+
bundle = fluent.Bundle("fr", [str(data_dir / "fr.ftl")])
40+
assert (
41+
bundle.get_translation("hello-user", variables={"user": "Bob"})
42+
== "Bonjour, \u2068Bob\u2069!"
43+
)
44+
45+
46+
def test_new_overwrites_old():
47+
bundle = fluent.Bundle(
48+
"en",
49+
[str(data_dir / "fr.ftl"), str(data_dir / "en_hello.ftl")],
50+
)
51+
assert bundle.get_translation("hello-world") == "Hello World"
52+
assert (
53+
bundle.get_translation("hello-user", variables={"user": "Bob"})
54+
== "Bonjour, \u2068Bob\u2069!"
55+
)
56+
57+
58+
def test_id_not_found():
59+
bundle = fluent.Bundle("fr", [str(data_dir / "fr.ftl")])
60+
with pytest.raises(ValueError):
61+
bundle.get_translation("missing", variables={"user": "Bob"})
62+
63+
64+
def test_file_not_found():
65+
with pytest.raises(FileNotFoundError):
66+
fluent.Bundle("fr", [str(data_dir / "none.ftl")])
67+
68+
69+
def test_file_has_errors():
70+
with pytest.raises(ValueError):
71+
fluent.Bundle("fr", [str(data_dir / "errors.ftl")])

0 commit comments

Comments
 (0)