Metadata-Version: 2.4
Name: citry_core
Version: 1.6.0
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Rust
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
License-File: LICENSE
Summary: Rust-powered core for Citry, a fast, simple, and smart frontend framework for Python
Keywords: citry,frontend,html,web development,templating,template engine
Home-Page: https://citry.dev
Author-email: Juro Oravec <juraj.oravec.josefson@gmail.com>
License-Expression: MIT
Requires-Python: >=3.10, <4.0
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Changelog, https://github.com/citry-dev/citry/blob/main/packages/py/citry_core/CHANGELOG.md
Project-URL: Donate, https://github.com/sponsors/JuroOravec
Project-URL: Homepage, https://citry.dev
Project-URL: Issues, https://github.com/citry-dev/citry/issues
Project-URL: Repository, https://github.com/citry-dev/citry

# citry_core

Python package that exposes Rust functionality from the twin [`citry_core_py`](https://github.com/citry-dev/citry/tree/main/crates/citry_core_py/) Rust crate.

## Install and quick start

```bash
pip install citry-core
```

The public APIs are namespaced by capability:

```python
from citry_core.template_parser import compile_template, parse_template

template = parse_template("<p>Hello {{ name }}!</p>")
python_source = compile_template(template)
```

The package also exposes HTML transformation, Fluent i18n, safe expression
evaluation, template formatting, and browser-expression analysis.

## Build integration

This package is the **Python-side twin** to the [`citry_core_py`](https://github.com/citry-dev/citry/tree/main/crates/citry_core_py/) Rust crate. The connection between them is defined in [`pyproject.toml`](https://github.com/citry-dev/citry/blob/main/packages/py/citry_core/pyproject.toml) via the `[tool.maturin]` section:

```toml
[tool.maturin]
manifest-path = "../../../crates/citry_core_py/Cargo.toml"
module-name = "citry_core._rust"
```

When built with [maturin](https://maturin.rs/), the Rust crate is compiled into a Python extension module that provides the core functionality, while this package adds Python-side wrappers, type stubs, and additional utilities.

### Maturin module-name configuration

**Important**: The `module-name` setting is crucial for this package to work correctly.

By default, maturin would create a Python module with the same name as the Rust crate (`citry_core_py`), but we want to publish this package as `citry_core`. The `module-name = "citry_core._rust"` setting tells maturin to:

1. Create the compiled Rust extension as `_rust.cpython.so` (instead of `citry_core_py.cpython.so`)
2. Place it in the `citry_core/` directory (matching our Python package name)
3. Merge it with our existing `citry_core/` Python code

This allows us to:

- Keep the Rust crate named `citry_core_py` (useful for potential future bindings like `citry_core_js`)
- Publish the Python package as `citry_core` (the desired public name)
- Have maturin automatically merge the Rust binary with our Python code

The `module-name` format is `<python_package_name>.<rust_module_name>`, where:

- `python_package_name` must match the Python package directory (here,
  `./citry_core/`)
- `rust_module_name` must match the `#[pymodule]` function name in the Rust crate's `lib.rs` (e.g., `fn _rust`)

See the detailed comments in [`pyproject.toml`](https://github.com/citry-dev/citry/blob/main/packages/py/citry_core/pyproject.toml) for more information about this configuration.

## Package structure

The package uses a mixed Rust/Python layout:

```
packages/py/citry_core/
├── citry_core/               # Package code
│   ├── __init__.py           # Empty - see below
│   ├── _rust                 # Platform extension module generated by maturin
│   │                         # from `crates/citry_core_py` (suffix varies).
│   │                         # Name set by `module-name` setting in `pyproject.toml`
│   ├── _rust.pyi             # Type stubs for Rust API
│   │                         # Filename matches <rust_module_name> from `module-name`
│   │
│   │   # API for individual Rust crates defined
│   │   # as separate submodules / subdirectories
│   │
│   ├── html_transform/      # HTML transformation API
│   │   └── __init__.py      # API for this submodule
│   ├── i18n/                # Fluent compiler and locale runtime API
│   ├── safe_eval/           # Safe eval API
│   ├── template_formatter/  # Authored template formatting API
│   ├── template_parser/     # Template parsing API
│   └── ...
├── tests/                    # Package tests
└── pyproject.toml
```

## Manual API management

**By default, maturin auto-generates `__init__.py` files.** However, this package manually manages the Python-side API for several reasons:

1. **Multiple crates**: The Rust crate exposes multiple submodules (`html_transform`, `i18n`, `safe_eval`, `template_formatter`, `template_parser`). We split these into separate submodules to avoid name conflicts.
2. **Python-side post-processing**: Some crates need Python-side code that
   wraps or extends the Rust API. `safe_eval` executes transformed code, while
   `template_parser` provides Python wrappers around the Rust parser and
   compiler.

**The root `__init__.py` is intentionally empty** because we namespace each Rust crate under its own Python module. Instead, the API for each crate is defined in a separate directory with its own `__init__.py` file, e.g.:

- `citry_core/html_transform/__init__.py`
- `citry_core/i18n/__init__.py`
- `citry_core/safe_eval/__init__.py`
- `citry_core/template_formatter/__init__.py`
- `citry_core/template_parser/__init__.py`

This allows clean, namespaced imports:

```python
from citry_core.html_transform import transform_html
from citry_core.i18n import CatalogCompiler, canonicalize_locale
from citry_core.safe_eval import safe_eval
from citry_core.template_formatter import (
    finish_embedded_format,
    format_template,
    prepare_embedded_format,
    python_expression_provider,
)
from citry_core.template_parser import (
    analyze_browser_source,
    compile_template,
    parse_diagnostic,
    parse_template,
)
```

The i18n API compiles and analyzes Fluent project catalogs and runs their
locale-aware messages, formatting, and strict number/date/time input. The
parser API also exposes stable diagnostics, JavaScript analysis, and
parser-owned syntax inventories for editor and batch tooling.

The formatter includes conservative Citry/HTML structural layout plus the
vendored, in-process Python expression adapter. The provider identity function
lets tooling report the exact pinned Ruff implementation used for expression
output. JavaScript and CSS providers use an opaque two-pass API:
`prepare_embedded_format()` returns immutable, source-bound requests for safe
expression-free `<script>` and `<style>` bodies, and
`finish_embedded_format()` validates every result before composing one atomic
template result. This package discovers regions and enforces Citry boundaries;
the caller chooses and invokes the external provider.

**Remember**: When you import <br/>`citry_core.html_transform`,<br/>
you are directly accessing <br/>`citry_core/html_transform/__init__.py`<br/>
You are **NOT** accessing<br/>`citry_core/__init__.py`<br/>

## Type stubs and runtime access

### Issues with type checking

Type checkers (mypy, Pylance/Pyright) **cannot pick up signatures and docstrings** from the Rust API generated by `maturin`. The Rust code is compiled into a binary extension module, and type checkers don't have access to the original Rust source code or its PyO3 annotations.

To solve the type checking issue, we define type stubs in `_rust.pyi`.

The **`_rust.pyi`**:

- Re-defines function signatures and docstrings
- Enables type checking, IDE autocomplete, hover tooltips, etc
- **Important**: The filename `_rust.pyi` MUST match the second
  part of the `module-name` setting in [`pyproject.toml`](https://github.com/citry-dev/citry/blob/main/packages/py/citry_core/pyproject.toml)<br/>
  (`tool.maturin.module-name = "citry_core._rust"`).<br/>
  This ensures type checkers can find the type stubs for the Rust extension module.

### Issues with Pytest and virtual modules

Another issue is with `pytest`. We define nested Python submodules for each Rust crate to avoid name conflicts in the Rust API. But `pytest` can't import the nested modules directly using the `import` keyword. This fails in `pytest`:

```python
from citry_core._rust.template_parser import parse_template
# ModuleNotFoundError: No module named 'citry_core._rust.template_parser'
```

The cause is likely because the Rust-generated submodules (like `_rust.template_parser`) are **virtual modules** - they don't exist as actual files, but only inside the Rust binary.

To fix the issue with `pytest`, we have to avoid importing
the virtual Python modules using `import` keyword.

This simply means that, instead of importing the Rust API as:

```python
from citry_core._rust.html_transform import transform_html

transform_html(...)
```

We have to do:

```python
from citry_core import _rust

_rust.html_transform.transform_html(...)
```

## Development

Installing from the source distribution requires Rust 1.95 or newer. Normal
installations use a prebuilt wheel and do not need a Rust toolchain.

### Build

Build the package using `maturin`:

```bash
cd packages/py/citry_core
maturin develop  # Development build
maturin build    # Production build
```

Or with uv:

```bash
uv run maturin develop
```

### Test

**Important**: Before running tests, you must install the package in development mode:

```bash
cd packages/py/citry_core
uv run maturin develop
```

This builds the Rust extension module and installs the package so Python can import it.
Without this step, tests will fail with `ModuleNotFoundError: No module named 'citry_core'`.

After installing, run tests:

```bash
# From the package directory
uv run pytest
```

### Adding a new Rust crate

When a new Rust crate / submodule is added to the twin Rust crate [`citry_core_py`](https://github.com/citry-dev/citry/tree/main/crates/citry_core_py/):

1. **Add to `_rust.pyi`**: Define the type stubs

   ```python
   class new_module:
       def new_function(...) -> ...:
           """Docstring"""
           ...
   ```

2. **Create Python wrapper**: Add `citry_core/new_module/__init__.py` if needed

NOTE: A validator
[`bindings`](https://github.com/citry-dev/citry/blob/main/scripts/validators/bindings.py) (run by `python scripts/check.py`)
checks that we've added entries to `_rust.pyi` and that the `new_module` subdirectory exists.

### Type stub maintenance

When updating Rust functions:

1. Update the function signature and docstring in `_rust.pyi`
2. Ensure the filename `_rust.pyi` matches the second part from `module-name` in `pyproject.toml`

