Repository: railwayapp/nixpacks
Pull request #66 (2022-05-06 03:34:58)

Support Python 2.7
- Support Python 2.7 by reading the `.python-version` file or the `NIXPACKS_PYTHON_VERSION` environment variable. An error is thrown if the version doesn't resolve to 2.7 or 3.8

### Future steps for Python

- Support Poetry
- Support Pipenv



Patch:
diff --git a/examples/python-2/.python-version b/examples/python-2/.python-version
new file mode 100644
index 0000000..1effb00
--- /dev/null
+++ b/examples/python-2/.python-version
@@ -0,0 +1,1 @@
+2.7
diff --git a/examples/python-2/main.py b/examples/python-2/main.py
new file mode 100644
index 0000000..23c4e4a
--- /dev/null
+++ b/examples/python-2/main.py
@@ -0,0 +1,1 @@
+print "Hello from Python 2"
diff --git a/src/nixpacks/mod.rs b/src/nixpacks/mod.rs
index faf0e75..5cc3c40 100644
--- a/src/nixpacks/mod.rs
+++ b/src/nixpacks/mod.rs
@@ -216,13 +216,8 @@ impl<'a> AppBuilder<'a> {
         let env_var_pkgs = self
             .environment
             .get_config_variable("PKGS")
-            .map(|pkg_string| {
-                pkg_string
-                    .split(" ")
-                    .map(|s| Pkg::new(s))
-                    .collect::<Vec<_>>()
-            })
-            .unwrap_or(Vec::new());
+            .map(|pkg_string| pkg_string.split(' ').map(Pkg::new).collect::<Vec<_>>())
+            .unwrap_or_default();
 
         // Add custom user packages
         let mut pkgs = [self.options.custom_pkgs.clone(), env_var_pkgs].concat();
diff --git a/src/providers/npm.rs b/src/providers/npm.rs
index b345271..e1d3982 100644
--- a/src/providers/npm.rs
+++ b/src/providers/npm.rs
@@ -118,7 +118,7 @@ impl NpmProvider {
             .as_ref()
             .and_then(|engines| engines.get("node"));
 
-        let node_version = env_node_version.or(pkg_node_version);
+        let node_version = pkg_node_version.or(env_node_version);
 
         let node_version = match node_version {
             Some(node_version) => node_version,
diff --git a/src/providers/python.rs b/src/providers/python.rs
index 915a2c6..dcf0e80 100644
--- a/src/providers/python.rs
+++ b/src/providers/python.rs
@@ -1,6 +1,7 @@
 use std::collections::HashMap;
 
-use anyhow::{Context, Result};
+use anyhow::{bail, Context, Result};
+
 use serde::Deserialize;
 
 use crate::{
@@ -15,7 +16,10 @@ use crate::{
 
 use super::Provider;
 
+pub const DEFAULT_PYTHON_PKG_NAME: &'static &str = &"python38";
+
 pub struct PythonProvider {}
+
 impl Provider for PythonProvider {
     fn name(&self) -> &str {
         "python"
@@ -27,12 +31,9 @@ impl Provider for PythonProvider {
             || app.includes_file("pyproject.toml"))
     }
 
-    fn setup(
-        &self,
-        _app: &App,
-        _env: &crate::nixpacks::environment::Environment,
-    ) -> Result<Option<SetupPhase>> {
-        Ok(Some(SetupPhase::new(vec![Pkg::new("python38")])))
+    fn setup(&self, app: &App, env: &Environment) -> Result<Option<SetupPhase>> {
+        let pkg = PythonProvider::get_nix_python_package(app, env)?;
+        Ok(Some(SetupPhase::new(vec![pkg])))
     }
 
     fn install(&self, app: &App, _env: &Environment) -> Result<Option<InstallPhase>> {
@@ -63,7 +64,7 @@ impl Provider for PythonProvider {
 
     fn start(&self, app: &App, _env: &Environment) -> Result<Option<StartPhase>> {
         if app.includes_file("pyproject.toml") {
-            if let Ok(meta) = self.parse_pyproject(app) {
+            if let Ok(meta) = PythonProvider::parse_pyproject(app) {
                 if let Some(entry_point) = meta.entry_point {
                     return Ok(Some(StartPhase::new(match entry_point {
                         EntryPoint::Command(cmd) => cmd,
@@ -110,7 +111,30 @@ enum EntryPoint {
 }
 
 impl PythonProvider {
-    fn read_pyproject(&self, app: &App) -> Result<Option<PyProject>> {
+    fn get_nix_python_package(app: &App, env: &Environment) -> Result<Pkg> {
+        let mut custom_version = env
+            .get_config_variable("PYTHON_VERSION")
+            .map(|s| s.to_string());
+
+        if custom_version.is_none() && app.includes_file(".python-version") {
+            custom_version = Some(app.read_file(".python-version")?);
+        }
+
+        match custom_version.map(|s| s.trim().to_string()) {
+            Some(custom_version) => {
+                if custom_version == "2" || custom_version == "2.7" {
+                    Ok(Pkg::new("python27Full"))
+                } else if custom_version == "3" || custom_version == "3.8" {
+                    Ok(Pkg::new(DEFAULT_PYTHON_PKG_NAME))
+                } else {
+                    bail!("Only the latest version of Python `2` or `3` are supported.")
+                }
+            }
+            None => Ok(Pkg::new(DEFAULT_PYTHON_PKG_NAME)),
+        }
+    }
+
+    fn read_pyproject(app: &App) -> Result<Option<PyProject>> {
         if app.includes_file("pyproject.toml") {
             return Ok(Some(
                 app.read_toml("pyproject.toml")
@@ -119,7 +143,8 @@ impl PythonProvider {
         }
         Ok(None)
     }
-    fn parse_project(&self, project: &PyProject) -> ProjectMeta {
+
+    fn parse_project(project: &PyProject) -> ProjectMeta {
         let project_name = project
             .project
             .as_ref()
@@ -148,11 +173,60 @@ impl PythonProvider {
             entry_point,
         }
     }
-    fn parse_pyproject(&self, app: &App) -> Result<ProjectMeta> {
-        Ok(self.parse_project(
-            &(self
-                .read_pyproject(app)?
+
+    fn parse_pyproject(app: &App) -> Result<ProjectMeta> {
+        Ok(PythonProvider::parse_project(
+            &(PythonProvider::read_pyproject(app)?
                 .ok_or_else(|| anyhow::anyhow!("failed to load pyproject.toml"))?),
         ))
     }
 }
+
+#[cfg(test)]
+mod test {
+    use super::*;
+    use crate::nixpacks::{app::App, environment::Environment, nix::Pkg};
+    use std::collections::HashMap;
+
+    #[test]
+    fn test_no_version() -> Result<()> {
+        assert_eq!(
+            PythonProvider::get_nix_python_package(
+                &App::new("./examples/python")?,
+                &Environment::default()
+            )?,
+            Pkg::new(DEFAULT_PYTHON_PKG_NAME)
+        );
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_custom_version() -> Result<()> {
+        assert_eq!(
+            PythonProvider::get_nix_python_package(
+                &App::new("./examples/python-2")?,
+                &Environment::default()
+            )?,
+            Pkg::new("python27Full")
+        );
+
+        Ok(())
+    }
+
+    #[test]
+    fn test_version_from_environment_variable() -> Result<()> {
+        assert_eq!(
+            PythonProvider::get_nix_python_package(
+                &App::new("./examples/python")?,
+                &Environment::new(HashMap::from([(
+                    "NIXPACKS_PYTHON_VERSION".to_string(),
+                    "2.7".to_string()
+                )]))
+            )?,
+            Pkg::new("python27Full")
+        );
+
+        Ok(())
+    }
+}


Test patch:
diff --git a/tests/docker_run_tests.rs b/tests/docker_run_tests.rs
index 0b61202..e1b9f33 100644
--- a/tests/docker_run_tests.rs
+++ b/tests/docker_run_tests.rs
@@ -21,7 +21,7 @@ fn get_container_ids_from_image(image: String) -> String {
     String::from_utf8_lossy(&output.stdout).to_string()
 }
 
-fn stop_containers(container_id: &String) {
+fn stop_containers(container_id: &str) {
     Command::new("docker")
         .arg("stop")
         .arg(container_id)
@@ -31,7 +31,7 @@ fn stop_containers(container_id: &String) {
         .expect("failed to execute docker stop");
 }
 
-fn remove_containers(container_id: &String) {
+fn remove_containers(container_id: &str) {
     Command::new("docker")
         .arg("rm")
         .arg(container_id)
@@ -145,6 +145,13 @@ fn test_python() {
     assert!(output.contains("Hello from Python"));
 }
 
+#[test]
+fn test_python_2() {
+    let name = simple_build("./examples/python-2");
+    let output = run_image(name);
+    assert!(output.contains("Hello from Python 2"));
+}
+
 #[test]
 fn test_deno() {
     let name = simple_build("./examples/deno");