Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python bindings #1036

Merged
merged 9 commits into from
Jan 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@

# pre-commit-hooks
/.pre-commit-config.yaml

# Python virtual env
.venv/
venv/
86 changes: 86 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ members = [
"lsp/nls",
"utilities",
"nickel-wasm-repl",
"pyckel",
]

# Enable this to use flamegraphs
Expand Down
43 changes: 27 additions & 16 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -189,29 +189,38 @@
# Customize source filtering as Nickel uses non-standard-Rust files like `*.lalrpop`.
src = filterNickelSrc craneLib.filterCargoSources;

# Args passed to all `cargo` invocations by Crane.
cargoExtraArgs = "--frozen --offline --workspace";
in
rec {
# set of cargo args common to all builds
cargoBuildExtraArgs = "--frozen --offline";

# Build *just* the cargo dependencies, so we can reuse all of that work (e.g. via cachix) when running in CI
cargoArtifacts = craneLib.buildDepsOnly {
inherit
src
cargoExtraArgs;
inherit src;
cargoExtraArgs = "${cargoBuildExtraArgs} --workspace";
# pyo3 needs a Python interpreter in the build environment
# https://pyo3.rs/v0.17.3/building_and_distribution#configuring-the-python-version
buildInputs = [ pkgs.python3 ];
GuillaumeDesforges marked this conversation as resolved.
Show resolved Hide resolved
};

nickel = craneLib.buildPackage {
inherit
src
cargoExtraArgs
cargoArtifacts;
};
buildPackage = packageName:
craneLib.buildPackage {
inherit
src
cargoArtifacts;

cargoExtraArgs = "${cargoBuildExtraArgs} --package ${packageName}";
};


in
rec {
nickel = buildPackage "nickel-lang";
lsp-nls = buildPackage "nickel-lang-lsp";
nickel-wasm-repl = buildPackage "nickel-repl";

rustfmt = craneLib.cargoFmt {
# Notice that unlike other Crane derivations, we do not pass `cargoArtifacts` to `cargoFmt`, because it does not need access to dependencies to format the code.
inherit src;

# We don't reuse the `cargoExtraArgs` in scope because `cargo fmt` does not accept nor need any of `--frozen`, `--offline` or `--workspace`
yannham marked this conversation as resolved.
Show resolved Hide resolved
cargoExtraArgs = "--all";

# `-- --check` is automatically prepended by Crane
Expand All @@ -221,12 +230,11 @@
clippy = craneLib.cargoClippy {
inherit
src
cargoExtraArgs
cargoArtifacts;

cargoExtraArgs = cargoBuildExtraArgs;
cargoClippyExtraArgs = "--all-targets -- --deny warnings --allow clippy::new-without-default --allow clippy::match_like_matches_macro";
};

};

makeDevShell = { rust }: pkgs.mkShell {
Expand All @@ -241,6 +249,7 @@
pkgs.nodejs
pkgs.node2nix
pkgs.nodePackages.markdownlint-cli
pkgs.python3
];

shellHook = (pre-commit-builder { inherit rust; checkFormat = true; }).shellHook + ''
Expand Down Expand Up @@ -395,6 +404,8 @@
checks = {
inherit (mkCraneArtifacts { })
nickel
lsp-nls
nickel-wasm-repl
clippy
rustfmt;
# An optimizing release build is long: eschew optimizations in checks by
Expand Down
18 changes: 18 additions & 0 deletions pyckel/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "pyckel"
version = "0.3.1"
authors = ["Nickel team"]
license = "MIT"
readme = "README.md"
description = "Python bindings for the Nickel programming language."
homepage = "https://nickel-lang.org"
repository = "https://github.com/tweag/nickel"
keywords = ["configuration", "language", "nix", "nickel"]
edition = "2018"

[dependencies]
nickel-lang = {default-features = false, path = "../", version = "0.3.1" }
pyo3 = { version = "0.17.3", features = ["extension-module"] }

[lib]
crate-type = ["cdylib", "rlib"]
21 changes: 21 additions & 0 deletions pyckel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# pyckel

Python bindings to use Nickel.

## Install

```shell
pip install .
```

## Use

```python
import pyckel

result = pyckel.run("let x = 1 in { y = x + 2 }")
print(result)
# {
# "y": 3
# }
```
6 changes: 6 additions & 0 deletions pyckel/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[build-system]
requires = ["maturin>=0.14,<0.15"]
build-backend = "maturin"

[tool.maturin]

68 changes: 68 additions & 0 deletions pyckel/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use std::io::Cursor;

use nickel_lang::{
error::{Error, SerializationError},
eval::cache::CBNCache,
program::Program,
serialize,
};

use pyo3::{create_exception, exceptions::PyException, prelude::*};

create_exception!(pyckel, NickelException, PyException);

// see https://pyo3.rs/v0.17.3/function/error_handling.html#foreign-rust-error-types
struct NickelError(Error);
struct NickelSerializationError(SerializationError);

impl From<Error> for NickelError {
fn from(value: Error) -> Self {
Self(value)
}
}

impl std::convert::From<NickelError> for PyErr {
fn from(err: NickelError) -> PyErr {
match err {
// TODO better exceptions
NickelError(error) => NickelException::new_err(format!("{error:?}")),
}
}
}

impl From<SerializationError> for NickelSerializationError {
fn from(value: SerializationError) -> Self {
Self(value)
}
}

impl std::convert::From<NickelSerializationError> for PyErr {
fn from(err: NickelSerializationError) -> PyErr {
match err {
// TODO better exceptions
NickelSerializationError(error) => NickelException::new_err(format!("{error:?}")),
}
}
}

/// Evaluate from a Python str of a Nickel expression to a Python str of the resulting JSON.
#[pyfunction]
pub fn run(s: String) -> PyResult<String> {
let mut program: Program<CBNCache> =
Program::new_from_source(Cursor::new(s.to_string()), "python")?;

let term = program.eval_full().map_err(NickelError)?;

serialize::validate(serialize::ExportFormat::Json, &term).map_err(NickelSerializationError)?;

let json_string = serialize::to_string(serialize::ExportFormat::Json, &term)
.map_err(NickelSerializationError)?;

Ok(json_string)
}

#[pymodule]
pub fn pyckel(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(run, m)?)?;
Ok(())
}