Metadata-Version: 2.4
Name: pybonsai-lang
Version: 1.2.0
Summary: PyBonsai, a hermetic subset of Python 3.12 interpreted from its syntax tree
Author: PacketFive
License-Expression: GPL-2.0-only
Project-URL: Homepage, https://github.com/PacketFive/pybonsai
Project-URL: Source, https://github.com/PacketFive/pybonsai
Project-URL: Issues, https://github.com/PacketFive/pybonsai/issues
Keywords: configuration,dsl,hermetic,deterministic,starlark,interpreter
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Interpreters
Classifier: Topic :: System :: Systems Administration
Classifier: Typing :: Typed
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# PyBonsai

PyBonsai is a restricted subset of Python 3.12 for writing configuration
that must give the same result on every machine. It is the configuration
language of [PyrixOS](https://github.com/PacketFive/PyrixOS) and is defined
in section 7 of RFC 0001, but the package has no knowledge of packages or
builds and can be embedded in any Python program.

A PyBonsai file is ordinary Python syntax. It is never run by CPython.
PyBonsai parses it into a syntax tree, rejects every construct outside an
allow list, and then interprets the tree itself. A file therefore cannot:

* read or write files, open network connections, or read the clock or
  environment variables;
* change a value after creating it, since lists, dicts and sets become
  immutable tuples, mappings and frozensets;
* loop without a bound, since `while` and recursion are rejected and every
  `for` runs over a finite collection.

The same file with the same inputs always produces the same values, and
evaluation always finishes within fixed limits.

## Status

Implemented. The parser, checker and evaluator follow RFC 0001 section 7,
and the tests encode its rules. The coverage floor is 80%.

## Install

The distribution is named `pybonsai-lang`, because the name `pybonsai`
on PyPI belongs to an unrelated project. The import name is `pybonsai`.
To use it in another project:

```sh
pip install pybonsai-lang
```

or add `pybonsai-lang>=1.0,<2` to the project's dependencies. Version 1
keeps the language and the names in `pybonsai.__all__` stable: a file that
evaluates under 1.0 gives the same values under every 1.x release.

To work on PyBonsai itself:

```sh
python -m pip install --upgrade "pip>=25.1"
pip install --group dev
pip install --config-settings editable_mode=strict -e .
pybootstrap check
```

Python 3.12 or later is required. There are no runtime dependencies. The
`dev` dependency group installs [pybootstrap](https://github.com/packetfive/pybootstrap)
and the tools its gates drive: ruff for formatting and lint, pyright for
type checking, pytest with coverage (at least 80%), and hypothesis for
property tests. Each tool also works on its own, for example `pytest` or
`ruff check src tests`.

The repository's `src` directory is the package itself, mapped to its
import name in `pyproject.toml`. Strict editable mode gives that mapping a
real `pybonsai/` directory under `build/`, which pyright needs to find the
package. Tests import the installed package, so install before running
them.

Releases are made by pushing a tag `vX.Y.Z` that matches `__version__`
and has a section in `CHANGELOG.md`. The release workflow runs the gates,
builds the package, publishes it to PyPI through trusted publishing with
attestations, and creates the GitHub release.

## Use

```python
from pybonsai import evaluate_source

names = evaluate_source("xs = [i * i for i in range(4)]")
assert names["xs"] == (0, 1, 4, 9)
```

A program adds its own functions and value types through a `Host`:

```python
from pybonsai import Builtin, evaluate_source, make_host

host = make_host(
    names={"double": Builtin("double", lambda x: 2 * x)},
    types={},
)
evaluate_source("y = double(21)", host=host)
```

A host can also give a file constants, such as a mapping of options,
which the file reads like any other name. A dict or list given here
reaches the file as a read-only mapping or tuple:

```python
host = make_host(names={"config": {"DEBUG": True}})
evaluate_source("debug = config.get('DEBUG', False)", host=host)
```

A host name may also hold a function that an earlier evaluation
defined, so files can be evaluated in layers that build on one another.
The function keeps the scope of its own file, and an error inside it
names that file:

```python
first = evaluate_source("def greet(name):\n    return 'hello ' + name", "lib.pyb")
host = make_host(names={"greet": first["greet"]})
evaluate_source("message = greet('world')", "main.pyb", host=host)
```

Host types must be immutable and hashable, and each one lists the
attributes a file may read. Without a host, a file sees only the prelude:
`len`, `range`, `sorted`, `min`, `max`, `sum`, `any`, `all`, `abs`, `str`,
`int`, `bool`, `enumerate`, `zip` and `reversed`.

## Layout

| Module | Role |
| --- | --- |
| `parser.py` | Parse source with the Python 3.12 grammar |
| `checker.py` | Reject constructs outside the allow list before evaluation |
| `allowlist.py` | Permitted syntax nodes, operators and attributes |
| `evaluator.py` | Interpret the checked syntax tree |
| `values.py` | Immutable value types, freezing and attribute access |
| `scope.py` | Name lookup for module and function scopes |
| `limits.py` | Size, depth and step limits |
| `prelude.py` | The pure built-in functions |
| `host.py` | The interface for embedding programs |
| `errors.py` | Error types with file and line positions |

## License

GPL-2.0-only. See [LICENSE](LICENSE).
