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

Flatten python requirements 2 #981

Open
wants to merge 9 commits into
base: python-rewrite
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 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
6 changes: 2 additions & 4 deletions docs/reference/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -2713,17 +2713,16 @@ boolean
- [https://github.com/cachix/devenv/blob/main/src/modules/languages/python.nix](https://github.com/cachix/devenv/blob/main/src/modules/languages/python.nix)



## languages.python.venv.requirements

Contents of pip requirements.txt file.
Path to pip requirements.txt file as a string. Must be relative to devenv root.
This is passed to `pip install -r` during `devenv shell` initialisation.




*Type:*
null or strings concatenated with “\\n” or path
null or string



Expand All @@ -2734,7 +2733,6 @@ null or strings concatenated with “\\n” or path
- [https://github.com/cachix/devenv/blob/main/src/modules/languages/python.nix](https://github.com/cachix/devenv/blob/main/src/modules/languages/python.nix)



## languages.python.version

The Python version to use.
Expand Down
5 changes: 3 additions & 2 deletions examples/python/.test.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env bash
#!/bin/sh
set -ex
python --version | grep "3.11.3"
python -c "import requests;print(requests)"
python -c "import requests;print(requests)"
python -c "import arrow;print(arrow.__version__)" | grep "1.2.3"
2 changes: 1 addition & 1 deletion examples/python/devenv.nix
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@
version = "3.11.3";

venv.enable = true;
venv.requirements = ./requirements.txt;
venv.requirements = "./requirements.txt";
};
}
1 change: 1 addition & 0 deletions examples/python/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
requests
-r requirements2.txt
1 change: 1 addition & 0 deletions examples/python/requirements2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-r subrequirements/requirements3.txt
1 change: 1 addition & 0 deletions examples/python/subrequirements/constraints.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
arrow==1.2.3
2 changes: 2 additions & 0 deletions examples/python/subrequirements/requirements3.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
arrow
-c constraints.txt
3 changes: 3 additions & 0 deletions src/devenv/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ def cli(ctx, offline, system, debugger, nix_flags, verbose):
ctx.obj["gc_root"] = DEVENV_HOME_GC
ctx.obj["gc_project"] = DEVENV_HOME_GC / str(int(time.time() * 1000))

@cli.group()
mcdonc marked this conversation as resolved.
Show resolved Hide resolved
def processes():
pass

@cli.group()
def processes():
Expand Down
50 changes: 32 additions & 18 deletions src/modules/languages/python.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

let
cfg = config.languages.python;
flattenreq = pkgs.writers.writePython3 "flattenreq" { flakeIgnore = [ "E501" ]; } (builtins.readFile ../support/flattenreq.py);
libraries = lib.makeLibraryPath (
cfg.libraries
++ (lib.optional cfg.manylinux.enable pkgs.pythonManylinuxPackages.manylinux2014Package)
Expand All @@ -26,12 +27,6 @@ let
];
};

requirements = pkgs.writeText "requirements.txt" (
if lib.isPath cfg.venv.requirements
then builtins.readFile cfg.venv.requirements
else cfg.venv.requirements
);

nixpkgs-python = config.lib.getInput {
name = "nixpkgs-python";
url = "github:cachix/nixpkgs-python";
Expand All @@ -45,10 +40,16 @@ let

VENV_PATH="${config.env.DEVENV_STATE}/venv"

function recreate_venv () {
${pkgs.coreutils}/bin/rm -rf "$VENV_PATH"
${package.interpreter} -m venv --upgrade-deps "$VENV_PATH"
echo "${package.interpreter}" > "$VENV_PATH/.devenv_interpreter"
}

profile_python="$(${readlink} ${package.interpreter})"
devenv_interpreter_path="$(${pkgs.coreutils}/bin/cat "$VENV_PATH/.devenv_interpreter" 2> /dev/null|| false )"
venv_python="$(${readlink} "$devenv_interpreter_path")"
requirements="${lib.optionalString (cfg.venv.requirements != null) ''${requirements}''}"
requirements="${lib.optionalString (cfg.venv.requirements != null) ''$DEVENV_ROOT/"${cfg.venv.requirements}"''}"

# recreate venv if necessary
if [ -z $venv_python ] || [ $profile_python != $venv_python ]
Expand All @@ -58,24 +59,37 @@ let
${lib.optionalString cfg.poetry.enable ''
[ -f "${config.env.DEVENV_STATE}/poetry.lock.checksum" ] && rm ${config.env.DEVENV_STATE}/poetry.lock.checksum
''}
echo ${package.interpreter} -m venv --upgrade-deps "$VENV_PATH"
${package.interpreter} -m venv --upgrade-deps "$VENV_PATH"
echo "${package.interpreter}" > "$VENV_PATH/.devenv_interpreter"
recreate_venv
venv_recreated=1
fi

source "$VENV_PATH"/bin/activate

# reinstall requirements if necessary
# -n means nonempty
if [ -n "$requirements" ]
then
devenv_requirements_path="$(${pkgs.coreutils}/bin/cat "$VENV_PATH/.devenv_requirements" 2> /dev/null|| false )"
devenv_requirements="$(${readlink} "$devenv_requirements_path")"
if [ -z $devenv_requirements ] || [ $devenv_requirements != $requirements ]
tmp="$(${pkgs.coreutils}/bin/mktemp -d)"
${flattenreq} "$requirements" "$tmp"

existing_requirements="$VENV_PATH/.devenv_requirements"
[ -f $existing_requirements ] || existing_requirements="/dev/null"
existing_constraints="$VENV_PATH/.devenv_constraints"
[ -f $existing_constraints ] || existing_constraints="/dev/null"

if ! ${pkgs.diffutils}/bin/cmp --silent "$tmp/requirements.txt" "$existing_requirements" || ! ${pkgs.diffutils}/bin/cmp --silent "$tmp/constraints.txt" "$existing_constraints";
then
echo "${requirements}" > "$VENV_PATH/.devenv_requirements"
echo "Requirements changed, running pip install -r ${requirements}..."
"$VENV_PATH"/bin/pip install -r ${requirements}
if [ -z "$venv_recreated" ]
then
echo "Requirements changed, rebuilding Python venv..."
recreate_venv
fi
echo "Installing requirements..."
${pkgs.coreutils}/bin/install "$tmp/requirements.txt" "$VENV_PATH/.devenv_requirements"
${pkgs.coreutils}/bin/install "$tmp/constraints.txt" "$VENV_PATH/.devenv_constraints"
"$VENV_PATH"/bin/pip install -r "$VENV_PATH/.devenv_requirements" -c "$VENV_PATH/.devenv_constraints"
fi
${pkgs.coreutils}/bin/rm -rf "$tmp"
fi
'';

Expand Down Expand Up @@ -176,10 +190,10 @@ in
venv.enable = lib.mkEnableOption "Python virtual environment";

venv.requirements = lib.mkOption {
type = lib.types.nullOr (lib.types.either lib.types.lines lib.types.path);
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Contents of pip requirements.txt file.
Path to pip requirements.txt file as a string. Must be relative to devenv root.
This is passed to `pip install -r` during `devenv shell` initialisation.
'';
};
Expand Down
74 changes: 74 additions & 0 deletions src/modules/support/flattenreq.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import os
import re
import sys

envvar_pattern = r"\$\{([^\}]+)\}"


def replace_envvars(text, file_path):
def replace(match):
env_var = match.group(1)
val = os.environ.get(env_var, None)
if val is None:
raise ValueError(
f"No such environment variable {env_var} in {text} within "
f"{file_path}"
)
return val

return re.sub(envvar_pattern, replace, text)


def flatten_requirements(file_path, outdir):
requirements = set()
constraints = set()

def process_file(file_path):
if not file_path.startswith(os.path.sep):
prefix = os.getcwd()
file_path = os.path.join(prefix, file_path)
with open(file_path, "r") as file:
for line in file:
prefix = os.path.dirname(file_path)
line = line.strip()
if line.startswith("-r"):
line = replace_envvars(line, file_path)
nested_file_path = re.match(r"-r\s+(.+)", line).group(1)
if not nested_file_path.startswith(os.path.sep):
nested_file_path = os.path.join(prefix, nested_file_path)
process_file(nested_file_path)
elif line.startswith("-c"):
line = replace_envvars(line, file_path)
constraint_file_path = re.match(r"-c\s+(.+)", line).group(1)
if not constraint_file_path.startswith(os.path.sep):
constraint_file_path = os.path.join(
prefix, constraint_file_path
)
process_constraints(constraint_file_path)
elif not line.startswith("#") and line:
requirements.add(line)

def process_constraints(constraint_file_path):
with open(constraint_file_path, "r") as file:
for line in file:
line = line.strip()
if not line.startswith("#") and line:
constraints.add(line)

process_file(file_path)

with open(os.path.join(outdir, "requirements.txt"), "w") as output_file:
for req in sorted(requirements):
output_file.write(req + "\n")

with open(os.path.join(outdir, "constraints.txt"), "w") as con_output_file:
for con in sorted(constraints):
con_output_file.write(con + "\n")


if __name__ == "__main__":
requirements_file_path, outdir = sys.argv[1], sys.argv[2]
if os.path.isfile(outdir):
raise OSError(f"{outdir} is an existing file")
os.makedirs(outdir, exist_ok=True)
flatten_requirements(requirements_file_path, outdir)