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

Experimental python bindings #7735

Draft
wants to merge 42 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
2e37594
python: Initialise bindings from Pythonix
infinisil Feb 1, 2023
ed8c80b
python: Fixes for Nix changes
infinisil Feb 17, 2023
5dc8bcb
python: Integrate incremental and CI build
infinisil Feb 17, 2023
de148e4
python: Fix for IFD
infinisil Feb 17, 2023
827f634
python: Format using clang-format
infinisil Feb 17, 2023
410393f
python: Rename pythonnix namespace to nix::python
infinisil Feb 17, 2023
8837e42
python: Add exampleEnv to try out the bindings
infinisil Feb 17, 2023
4599be3
python: Install the bindings in hopefully the correct location
infinisil Feb 17, 2023
be36946
WIP: Start writing documentation and example
infinisil Feb 17, 2023
e3f56e9
Remove .cache from .gitignore
infinisil Feb 24, 2023
2c3e1c8
Always include config.h
infinisil Feb 24, 2023
9b27833
Release the GIL for Nix evaluation
infinisil Feb 24, 2023
d272171
Don't release GIL lock because it would segfault
infinisil Feb 24, 2023
b8003d6
Allow getting exact throw error messages
infinisil Feb 24, 2023
a1669ed
Readd .cache to .gitignore
infinisil Feb 24, 2023
33ca7e3
Add ThrownNixError subclass
infinisil Feb 27, 2023
7dee1c5
python: Add clang-tools to dev env
infinisil Mar 3, 2023
5bc4948
python: Properly release and reacquire GIL
infinisil Mar 3, 2023
6f8108c
python: Detect stack overflow from recursive data structure
infinisil Mar 3, 2023
446db64
Insert some TODO's
infinisil Mar 3, 2023
481f28c
python: Handle null's in expressions correctly
infinisil Mar 3, 2023
668313f
python: Fix boolean to nix conversion
infinisil Mar 3, 2023
96621a0
python: Add test for Null conversion
infinisil Mar 3, 2023
0503a73
python: Use assertRaises for tests
infinisil Mar 3, 2023
1b0960f
python: We don't need to install the bindings into a subdirectly
infinisil Mar 9, 2023
d8ce01b
Remove python from manual build again
infinisil Mar 9, 2023
9627862
Very simple Nix source filtering
infinisil Mar 9, 2023
a91f312
Fix the buildPythonApplication example
infinisil Mar 9, 2023
06d7135
Fix some unset variable problems with vars-and-functions.sh
infinisil Mar 9, 2023
34c8519
assertEquals -> assertEqual
infinisil Mar 9, 2023
91b0740
Alternate approach to calling init.sh
infinisil Mar 9, 2023
56ef3e3
Don't depend on all files for the python bindings
infinisil Mar 9, 2023
8d734cd
Make buildPythonApplication test work
infinisil Mar 9, 2023
f5fd435
Fix dev shell
infinisil Mar 10, 2023
68996d8
Add debugging capability to dev shell
infinisil Mar 10, 2023
22c9e48
Convert from eval to callExprString API
infinisil Mar 10, 2023
fb5884e
python api: write API docs for callExprString
yorickvP Apr 14, 2023
f1442f8
PyInit_Nix: elaborate on build hook workaround
yorickvP Apr 14, 2023
0d52ddc
buildPythonApplication: fix tests
yorickvP Apr 14, 2023
51440a3
python-bindings: use prev.callPackage to pass args
yorickvP Apr 14, 2023
2cb9dd0
python-to-nix: don't forward declare PyObject, doesn't work everywhere
yorickvP Apr 14, 2023
57e71a3
python: initNix -> initLibStore
yorickvP Apr 19, 2023
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
10 changes: 9 additions & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@
name = "nix-${version}";
inherit version;

src = sourceByRegexInverted [ "tests/nixos/.*" "tests/installer/.*" ] self;
src = sourceByRegexInverted [ "tests/nixos/.*" "tests/installer/.*" "python/*" "flake.nix" ] self;
VERSION_SUFFIX = versionSuffix;

outputs = [ "out" "dev" "doc" ];
Expand Down Expand Up @@ -439,6 +439,8 @@
postUnpack = "sourceRoot=$sourceRoot/perl";
});

passthru.python-bindings = prev.callPackage ./python { };

meta.platforms = lib.platforms.unix;
});

Expand Down Expand Up @@ -500,6 +502,10 @@
# Perl bindings for various platforms.
perlBindings = forAllSystems (system: nixpkgsFor.${system}.native.nix.perl-bindings);

pythonBindings = nixpkgs.lib.genAttrs systems (system: self.packages.${system}.nix.python-bindings);
# TODO: recurseIntoAttrs or combine multiple tests into a single one
pythonBindingsTests = nixpkgs.lib.genAttrs systems (system: self.packages.${system}.nix.python-bindings.tests.example-buildPythonApplication);

# Binary tarball for various platforms, containing a Nix store
# with the closure of 'nix' package, and the second half of
# the installation script.
Expand Down Expand Up @@ -645,6 +651,7 @@
checks = forAllSystems (system: {
binaryTarball = self.hydraJobs.binaryTarball.${system};
perlBindings = self.hydraJobs.perlBindings.${system};
pythonBindings = self.hydraJobs.pythonBindings.${system};
installTests = self.hydraJobs.installTests.${system};
nixpkgsLibTests = self.hydraJobs.tests.nixpkgsLibTests.${system};
} // (lib.optionalAttrs (builtins.elem system linux64BitSystems)) {
Expand Down Expand Up @@ -727,6 +734,7 @@
(forAllCrossSystems (crossSystem: let pkgs = nixpkgsFor.${system}.cross.${crossSystem}; in makeShell pkgs pkgs.stdenv)) //
{
default = self.devShells.${system}.native-stdenvPackages;
python = self.packages.${system}.nix.python-bindings.shell;
}
);
};
Expand Down
3 changes: 3 additions & 0 deletions python/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build
# For clang-tools
.cache
49 changes: 49 additions & 0 deletions python/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# This Makefile is only used for development of the Python bindings, it is not
# used in the Nix build.
# The reason this exists is to make it easier to develop the Python bindings in
# tandem with the main Nix.
# The default `make` (defaults to `make build`) calls the main Nix projects
# `make install` before calling the Python bindings' `meson compile`, therefore
# ensuring that the needed Nix dynamic libraries are up-to-date

builddir=build

.PHONY: build
build: nix-install setup-done
meson compile -C $(builddir)

.PHONY: test
test: nix-install setup-done
meson test -C $(builddir) -v

.PHONY: clean
clean:
rm -rf $(builddir)

# We include the main Nix projects Makefile.config file to know the $(libdir)
# variable, which is where Nix is installed in, which we can then use to setup
# the meson build
include ../Makefile.config

# We need the file to exist though
../Makefile.config:
@# Throw a good error message in case ./configure hasn't been run yet
@[[ -e ../config.status ]] || ( echo "The main Nix project needs to be configured first, see https://nixos.org/manual/nix/stable/contributing/hacking.html" && exit 1 )
@# If ./configure is done, we can create the file ourselves
$(MAKE) -C .. Makefile.config

.PHONY: setup
setup: nix-install
@# Make meson be able to find the locally-installed Nix
PKG_CONFIG_PATH=$(libdir)/pkgconfig:$$PKG_CONFIG_PATH meson setup $(builddir)

.PHONY: setup-done
setup-done:
@# A better error message in case the build directory doesn't exist yet
@[[ -e $(builddir) ]] || ( echo "Run 'make setup' once to configure the project build directory" && exit 1 )

.PHONY: nix-install
nix-install:
@# The python bindings don't technically need an _entire_ Nix installation,
@# but it seems non-trivial to pick out only exactly the files it actually needs
$(MAKE) -C .. install
13 changes: 13 additions & 0 deletions python/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Python Bindings

This directory contains experimental Python bindings to a small subset of Nix's functionality. These bindings are very fast since they link to the necessary dynamic libraries directly, without having to call the Nix CLI for every operation.

Thanks to [@Mic92](https://github.com/Mic92) who wrote [Pythonix](https://github.com/Mic92/pythonix) which these bindings were originally based on, before they became the official bindings that are part of the Nix project. They were upstreamed to decrease maintenance overhead and make sure they are always up-to-date.

Note that the Python bindings are new and experimental. The interface is likely to change based on known issues and user feedback.

## Documentation

See [index.md](./doc/index.md), which is also rendered in the HTML manual.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't find where they are rendered in the manual.


To hack on these bindings, see [hacking.md](./doc/hacking.md), also rendered in the HTML manual.
97 changes: 97 additions & 0 deletions python/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
{
system,
lib,
python3,
boost,
gdb,
clang-tools,
pkg-config,
ninja,
meson,
nix,
mkShell,
enableDebugging,
recurseIntoAttrs,
isShell ? false,
}:
let
_python = python3;
in
let
python3 = _python.override { self = enableDebugging _python; };
# Extracts tests/init.sh
testScripts = nix.overrideAttrs (old: {
name = "nix-test-scripts-${old.version}";
outputs = [ "out" ];
separateDebugInfo = false;
buildPhase = ''
make tests/{init.sh,common/vars-and-functions.sh}
'';
script = ''
pushd ${placeholder "out"}/libexec >/dev/null
source init.sh
popd >/dev/null
'';
passAsFile = [ "script" ];
installPhase = ''
rm -rf "$out"
mkdir -p "$out"/{libexec/common,share/bash}
cp tests/init.sh "$out"/libexec
cp tests/common/vars-and-functions.sh "$out"/libexec/common

cp "$scriptPath" "$out"/share/bash/nix-test.sh
'';
dontFixup = true;
});
in
python3.pkgs.buildPythonPackage {
name = "nix";
format = "other";

src = builtins.path {
path = ./.;
filter = path: type:
path == toString ./meson.build
|| path == toString ./tests.py
|| path == toString ./test.sh
|| lib.hasPrefix (toString ./src) path;
};


strictDeps = true;

nativeBuildInputs = [
ninja
pkg-config
(meson.override { inherit python3; })
] ++ lib.optional (!isShell) nix;

buildInputs = nix.propagatedBuildInputs ++ [
boost
] ++ lib.optional (!isShell) nix;

mesonBuildType = "release";

doInstallCheck = true;
TEST_SCRIPTS = testScripts;
installCheckPhase = "meson test -v";

passthru = {
exampleEnv = python3.withPackages (p: [ nix.python-bindings ]);
tests = {
example-buildPythonApplication = import ./examples/buildPythonApplication {
inherit nix system testScripts python3;
};
};
shell = mkShell {
packages = [
clang-tools
gdb
];
TEST_SCRIPTS = testScripts;
inputsFrom = [
(nix.python-bindings.override { isShell = true; })
];
};
};
}
18 changes: 18 additions & 0 deletions python/doc/api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Experimental Python Bindings

## callExprString

```python
nix.callExprString(expression: str, arg)
```
Parse a nix expression, then call it as a nix function.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Parse a nix expression, then call it as a nix function.
Parse a Nix expression from the given string, then call it as a Nix function.

This needs some more information, such as whether it writes to the store when evaluating derivations and such. Also, how to pass parameters to the evaluator?


Note that this function is experimental and subject to change based on known issues and feedback.

**Parameters:**,
`expression` (str): The string containing a nix expression.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`expression` (str): The string containing a nix expression.
`expression` (str): The string containing a Nix expression.

`arg`: the argument to pass to the function

**Returns:**
`result`: the result of the function invocation, converted to python datatypes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`result`: the result of the function invocation, converted to python datatypes.
`result`: the result of the function invocation, converted to Python data types.

This needs some more information on the data type mapping between Nix and Python.


3 changes: 3 additions & 0 deletions python/doc/hacking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Python Bindings Hacking

This is how to hack on the bindings
32 changes: 32 additions & 0 deletions python/doc/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Experimental Python Bindings

Nix comes with minimal experimental Python bindings that link directly to the necessary dynamic libraries, making them very fast.

## Trying it out

The easiest way to try out the bindings is using the provided example environment:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice touch!


```
$ nix run github:NixOS/nix#nix.python-bindings.exampleEnv
Python 3.10.8 (main, Oct 11 2022, 11:35:05) [GCC 11.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import nix
>>> nix.callExprString('"Hello ${name}!"', arg={"name": "Python"}))
'Hello Python!'
```

For the available functions and their interfaces, see the API section.

## Build integration

In the future these Python bindings will be available from Nixpkgs as `python3Packages.nix`.

Until then the Python bindings are only available from the Nix derivation via the `python-bindings` [passthru attribute](https://nixos.org/manual/nixpkgs/stable/#var-stdenv-passthru). Without any modifications, this derivation is built for the default Python 3 version from the Nixpkgs version used to build Nix. This Python version might not match the Python version of the project you're trying to use them in. Therefore it is recommended to override the bindings with the correct Python version using
Comment on lines +22 to +24
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should make promises about the future in a manual.

Suggested change
In the future these Python bindings will be available from Nixpkgs as `python3Packages.nix`.
Until then the Python bindings are only available from the Nix derivation via the `python-bindings` [passthru attribute](https://nixos.org/manual/nixpkgs/stable/#var-stdenv-passthru). Without any modifications, this derivation is built for the default Python 3 version from the Nixpkgs version used to build Nix. This Python version might not match the Python version of the project you're trying to use them in. Therefore it is recommended to override the bindings with the correct Python version using
The Python bindings are only available from the Nix derivation via the `python-bindings` [passthru attribute](https://nixos.org/manual/nixpkgs/stable/#var-stdenv-passthru). Without any modifications, this derivation is built for the default Python 3 version from the Nixpkgs version used to build Nix. This Python version might not match the Python version of the project you're trying to use them in. Therefore it is recommended to override the bindings with the correct Python version using


```
nix.python-bindings.override {
python = myPythonVersion;
}
```

For complete examples, see https://github.com/NixOS/nix/tree/master/python/examples
37 changes: 37 additions & 0 deletions python/examples/buildPythonApplication/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
system ? builtins.currentSystem,
pkgs ? import (fetchTarball {
url = "https://github.com/NixOS/nixpkgs/archive/545c7a31e5dedea4a6d372712a18e00ce097d462.tar.gz";
sha256 = "1dbsi2ccq8x0hyl8n0hisigj8q19amvj9irzfbgy4b3szb6x2y6l";
}) {
config = {};
overlays = [];
inherit system;
},
python3 ? pkgs.python3,
nix ? (import ../../..).default,
testScripts,
}:
let
nixBindings = nix.python-bindings.override { inherit python3; };
in python3.pkgs.buildPythonApplication {
pname = "hello-nix";
version = "0.1";
src = builtins.path {
path = ./.;
filter = path: type:
pkgs.lib.hasPrefix (toString ./hello) path
|| path == toString ./setup.py;
};
propagatedBuildInputs = [ nixBindings ];
doInstallCheck = true;
nativeCheckInputs = [
nix
];
installCheckPhase = ''
(
source ${testScripts}/share/bash/nix-test.sh
$out/bin/hello-nix
)
'';
}
4 changes: 4 additions & 0 deletions python/examples/buildPythonApplication/hello/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import nix

def greet():
print("Evaluating 1 + 1 in Nix gives: " + str(nix.callExprString("_: 1 + 1", None)))
12 changes: 12 additions & 0 deletions python/examples/buildPythonApplication/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from setuptools import setup, find_packages

setup(
name="hello-nix",
version="0.1",
packages=find_packages(),
entry_points={
'console_scripts': [
'hello-nix = hello:greet',
],
},
)
17 changes: 17 additions & 0 deletions python/meson.build
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
project('python-nix', 'cpp',
version : '0.1.8',
license : 'LGPL-2.0',
)

python_mod = import('python')
py_installation = python_mod.find_installation()

nix_expr_dep = dependency('nix-expr', required: true)

subdir('src')

fs = import('fs')

env = environment()
env.prepend('PYTHONPATH', fs.parent(nix_bindings.full_path()))
test('python test', find_program('bash'), args : files('test.sh'), env : env)
Loading