Skip to content

Commit 407afdb

Browse files
authored
Merge pull request #1036 from GuillaumeDesforges/python
Python bindings
2 parents 4d60b13 + 76782d6 commit 407afdb

File tree

8 files changed

+231
-16
lines changed

8 files changed

+231
-16
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,7 @@
2121

2222
# pre-commit-hooks
2323
/.pre-commit-config.yaml
24+
25+
# Python virtual env
26+
.venv/
27+
venv/

Cargo.lock

Lines changed: 86 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ members = [
8080
"lsp/nls",
8181
"utilities",
8282
"nickel-wasm-repl",
83+
"pyckel",
8384
]
8485

8586
# Enable this to use flamegraphs

flake.nix

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -189,29 +189,38 @@
189189
# Customize source filtering as Nickel uses non-standard-Rust files like `*.lalrpop`.
190190
src = filterNickelSrc craneLib.filterCargoSources;
191191

192-
# Args passed to all `cargo` invocations by Crane.
193-
cargoExtraArgs = "--frozen --offline --workspace";
194-
in
195-
rec {
192+
# set of cargo args common to all builds
193+
cargoBuildExtraArgs = "--frozen --offline";
194+
196195
# Build *just* the cargo dependencies, so we can reuse all of that work (e.g. via cachix) when running in CI
197196
cargoArtifacts = craneLib.buildDepsOnly {
198-
inherit
199-
src
200-
cargoExtraArgs;
197+
inherit src;
198+
cargoExtraArgs = "${cargoBuildExtraArgs} --workspace";
199+
# pyo3 needs a Python interpreter in the build environment
200+
# https://pyo3.rs/v0.17.3/building_and_distribution#configuring-the-python-version
201+
buildInputs = [ pkgs.python3 ];
201202
};
202203

203-
nickel = craneLib.buildPackage {
204-
inherit
205-
src
206-
cargoExtraArgs
207-
cargoArtifacts;
208-
};
204+
buildPackage = packageName:
205+
craneLib.buildPackage {
206+
inherit
207+
src
208+
cargoArtifacts;
209+
210+
cargoExtraArgs = "${cargoBuildExtraArgs} --package ${packageName}";
211+
};
212+
213+
214+
in
215+
rec {
216+
nickel = buildPackage "nickel-lang";
217+
lsp-nls = buildPackage "nickel-lang-lsp";
218+
nickel-wasm-repl = buildPackage "nickel-repl";
209219

210220
rustfmt = craneLib.cargoFmt {
211221
# 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.
212222
inherit src;
213223

214-
# We don't reuse the `cargoExtraArgs` in scope because `cargo fmt` does not accept nor need any of `--frozen`, `--offline` or `--workspace`
215224
cargoExtraArgs = "--all";
216225

217226
# `-- --check` is automatically prepended by Crane
@@ -221,12 +230,11 @@
221230
clippy = craneLib.cargoClippy {
222231
inherit
223232
src
224-
cargoExtraArgs
225233
cargoArtifacts;
226234

235+
cargoExtraArgs = cargoBuildExtraArgs;
227236
cargoClippyExtraArgs = "--all-targets -- --deny warnings --allow clippy::new-without-default --allow clippy::match_like_matches_macro";
228237
};
229-
230238
};
231239

232240
makeDevShell = { rust }: pkgs.mkShell {
@@ -241,6 +249,7 @@
241249
pkgs.nodejs
242250
pkgs.node2nix
243251
pkgs.nodePackages.markdownlint-cli
252+
pkgs.python3
244253
];
245254

246255
shellHook = (pre-commit-builder { inherit rust; checkFormat = true; }).shellHook + ''
@@ -395,6 +404,8 @@
395404
checks = {
396405
inherit (mkCraneArtifacts { })
397406
nickel
407+
lsp-nls
408+
nickel-wasm-repl
398409
clippy
399410
rustfmt;
400411
# An optimizing release build is long: eschew optimizations in checks by

pyckel/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "pyckel"
3+
version = "0.3.1"
4+
authors = ["Nickel team"]
5+
license = "MIT"
6+
readme = "README.md"
7+
description = "Python bindings for the Nickel programming language."
8+
homepage = "https://nickel-lang.org"
9+
repository = "https://github.com/tweag/nickel"
10+
keywords = ["configuration", "language", "nix", "nickel"]
11+
edition = "2018"
12+
13+
[dependencies]
14+
nickel-lang = {default-features = false, path = "../", version = "0.3.1" }
15+
pyo3 = { version = "0.17.3", features = ["extension-module"] }
16+
17+
[lib]
18+
crate-type = ["cdylib", "rlib"]

pyckel/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# pyckel
2+
3+
Python bindings to use Nickel.
4+
5+
## Install
6+
7+
```shell
8+
pip install .
9+
```
10+
11+
## Use
12+
13+
```python
14+
import pyckel
15+
16+
result = pyckel.run("let x = 1 in { y = x + 2 }")
17+
print(result)
18+
# {
19+
# "y": 3
20+
# }
21+
```

pyckel/pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[build-system]
2+
requires = ["maturin>=0.14,<0.15"]
3+
build-backend = "maturin"
4+
5+
[tool.maturin]
6+

pyckel/src/lib.rs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use std::io::Cursor;
2+
3+
use nickel_lang::{
4+
error::{Error, SerializationError},
5+
eval::cache::CBNCache,
6+
program::Program,
7+
serialize,
8+
};
9+
10+
use pyo3::{create_exception, exceptions::PyException, prelude::*};
11+
12+
create_exception!(pyckel, NickelException, PyException);
13+
14+
// see https://pyo3.rs/v0.17.3/function/error_handling.html#foreign-rust-error-types
15+
struct NickelError(Error);
16+
struct NickelSerializationError(SerializationError);
17+
18+
impl From<Error> for NickelError {
19+
fn from(value: Error) -> Self {
20+
Self(value)
21+
}
22+
}
23+
24+
impl std::convert::From<NickelError> for PyErr {
25+
fn from(err: NickelError) -> PyErr {
26+
match err {
27+
// TODO better exceptions
28+
NickelError(error) => NickelException::new_err(format!("{error:?}")),
29+
}
30+
}
31+
}
32+
33+
impl From<SerializationError> for NickelSerializationError {
34+
fn from(value: SerializationError) -> Self {
35+
Self(value)
36+
}
37+
}
38+
39+
impl std::convert::From<NickelSerializationError> for PyErr {
40+
fn from(err: NickelSerializationError) -> PyErr {
41+
match err {
42+
// TODO better exceptions
43+
NickelSerializationError(error) => NickelException::new_err(format!("{error:?}")),
44+
}
45+
}
46+
}
47+
48+
/// Evaluate from a Python str of a Nickel expression to a Python str of the resulting JSON.
49+
#[pyfunction]
50+
pub fn run(s: String) -> PyResult<String> {
51+
let mut program: Program<CBNCache> =
52+
Program::new_from_source(Cursor::new(s.to_string()), "python")?;
53+
54+
let term = program.eval_full().map_err(NickelError)?;
55+
56+
serialize::validate(serialize::ExportFormat::Json, &term).map_err(NickelSerializationError)?;
57+
58+
let json_string = serialize::to_string(serialize::ExportFormat::Json, &term)
59+
.map_err(NickelSerializationError)?;
60+
61+
Ok(json_string)
62+
}
63+
64+
#[pymodule]
65+
pub fn pyckel(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
66+
m.add_function(wrap_pyfunction!(run, m)?)?;
67+
Ok(())
68+
}

0 commit comments

Comments
 (0)