feat(python): per import requirement pin (#5520)

* implement single line pin

* make panic-safe

* use pin even if multiple modules imported withing single statement

* add repins and make imports respect pins

* keep all pins

* Allow multiple pins

* add comments + handle stuff more safely

* fix fully qualified imports

* remove ignore

* sort nested

* apply unique to output requirements list

* fix typo

* remove mut

* update sqlx

* sort imports

* sort imports

* fix formatter and format

* refactor

* fix comptime error

* write tests

* perf: do not capture if string is empty
This commit is contained in:
pyranota
2025-04-11 21:31:51 +00:00
committed by GitHub
parent d5186da271
commit 0b6d017fed
8 changed files with 638 additions and 117 deletions
@@ -0,0 +1,23 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT content FROM script WHERE path = $1 AND workspace_id = $2\n AND created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND\n workspace_id = $2)\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "content",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "443bd83bcea1d37c79cb080095343c98104529879f991c49585cd181e34aa827"
}
@@ -1,23 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT content FROM script WHERE path = $1 AND workspace_id = $2\n AND created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND\n workspace_id = $2)\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "content",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false
]
},
"hash": "ac01e45d3335015f53f3d63fe159e631efb65c3d326b6b6ae8361a2116bff145"
}
@@ -11,6 +11,7 @@ mod mapping;
use async_recursion::async_recursion;
use itertools::Itertools;
use lazy_static::lazy_static;
use std::collections::HashMap;
use mapping::{FULL_IMPORTS_MAP, SHORT_IMPORTS_MAP};
#[cfg(not(target_arch = "wasm32"))]
@@ -20,6 +21,7 @@ use regex_lite::Regex;
use rustpython_parser::{
ast::{Stmt, StmtImport, StmtImportFrom, Suite},
text_size::TextRange,
Parse,
};
use sqlx::{Pool, Postgres};
@@ -41,9 +43,10 @@ fn replace_full_import(x: &str) -> Option<String> {
lazy_static! {
static ref RE: Regex = Regex::new(r"^\#\s?(\S+)\s*$").unwrap();
static ref PIN_RE: Regex = Regex::new(r"(?:\s*#\s*(pin|repin):\s*)(\S*)").unwrap();
}
fn process_import(module: Option<String>, path: &str, level: usize) -> Vec<String> {
fn process_import(module: Option<String>, path: &str, level: usize) -> Vec<NImport> {
if level > 0 {
let mut imports = vec![];
let splitted_path = path.split("/");
@@ -52,17 +55,18 @@ fn process_import(module: Option<String>, path: &str, level: usize) -> Vec<Strin
.take(splitted_path.count() - level)
.join("/");
if let Some(m) = module {
imports.push(format!("relative:{base}/{}", m.replace(".", "/")));
imports.push(NImport::Relative(format!("{base}/{}", m.replace(".", "/"))));
} else {
imports.push(format!("relative:{base}"));
imports.push(NImport::Relative(format!("{base}")));
}
imports
} else if let Some(module) = module {
let imprt = module.split('.').next().unwrap_or("").replace("_", "-");
if imprt == "u" || imprt == "f" {
vec![format!("relative:{}", module.replace(".", "/"))]
vec![NImport::Relative(module.replace(".", "/"))]
} else {
vec![replace_full_import(&module).unwrap_or(replace_import(imprt))]
let pkg = replace_full_import(&module).unwrap_or(replace_import(imprt));
vec![NImport::Auto { key: if module == pkg { None } else { Some(module) }, pkg }]
}
} else {
vec![]
@@ -73,17 +77,68 @@ pub fn parse_relative_imports(code: &str, path: &str) -> error::Result<Vec<Strin
let nimports = parse_code_for_imports(code, path)?;
return Ok(nimports
.into_iter()
.filter_map(|x| {
if x.starts_with("relative:") {
Some(x.replace("relative:", ""))
} else {
None
}
.filter_map(|x| match x {
NImport::Relative(path) => Some(path),
_ => None,
})
.collect());
}
fn parse_code_for_imports(code: &str, path: &str) -> error::Result<Vec<String>> {
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum NImport {
// Order matters! First we want to resolve all repins
// manually repinned requirement
// e.g.:
// import pandas # repin: pandas==x.y.z
Repin {
pin: ImportPin,
key: String,
},
// manually pinned requirements
// e.g.:
// import pandas # pin: pandas>=x.y.z
// import pandas # pin: pandas<=x.y.z
//
// NOTE: It is possible for multiple pins exist on same import
// That's why we store vector of pins
Pin {
pins: Vec<ImportPin>,
key: String,
},
// Automatically inferred requirement
// e.g.:
// import pandas
Auto {
// Take `x.y.z` for example
// x is going to be the `root`
// and x.y.z is `full`
//
// `full` will be None if it is equal to root
//
// We will use `root` as a requirement name and pass to `uv pip compile` if it was not replaced with any pin
pkg: String,
// However we still need full, since all pins pin against full import names
key: Option<String>,
},
// Relative imports
Relative(String),
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
enum NImportResolved {
Repin { pin: ImportPin, key: String },
Pin { pins: Vec<ImportPin>, key: String },
Auto { pkg: String, key: Option<String> },
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
struct ImportPin {
pkg: String,
path: String,
}
fn parse_code_for_imports(code: &str, path: &str) -> error::Result<Vec<NImport>> {
let mut code = code.split(DEF_MAIN).next().unwrap_or("").to_string();
// remove main function decorator from end of file if it exists
@@ -104,19 +159,56 @@ fn parse_code_for_imports(code: &str, path: &str) -> error::Result<Vec<String>>
let ast = Suite::parse(&code, "main.py").map_err(|e| {
error::Error::ExecutionErr(format!("Error parsing code for imports: {}", e.to_string()))
})?;
let nimports: Vec<String> = ast
let find_pin = |range: TextRange, key: String| {
let hs = code
.chars()
.skip(range.end().to_usize())
.take_while(|e| *e != '\n')
.collect::<String>();
if hs.trim_start().is_empty(){
return None;
}
PIN_RE
.captures(&hs)
.and_then(|x| {
x.get(1).zip(x.get(2)).and_then(|(ty_m, pkg_m)| {
let pkg = pkg_m.as_str().to_owned();
if ty_m.as_str() == "pin" {
Some(vec![NImport::Pin {
pins: vec![ImportPin { pkg, path: path.to_owned() }],
key,
}])
} else if ty_m.as_str() == "repin" {
Some(vec![NImport::Repin {
pin: ImportPin { pkg, path: path.to_owned() },
key,
}])
} else {
None
}
})
})
};
let mut nimports: Vec<NImport> = ast
.into_iter()
.filter_map(|x| match x {
Stmt::Import(StmtImport { names, .. }) => Some(
names
.into_iter()
.map(|x| {
let name = x.name.to_string();
process_import(Some(name), path, 0)
})
.flatten()
.collect::<Vec<String>>(),
),
Stmt::Import(StmtImport { names, range }) => names
.get(0)
.and_then(|al| find_pin(range, al.name.to_string()))
.or(Some(
names
.into_iter()
.map(|x| {
let name = x.name.to_string();
process_import(Some(name), path, 0)
})
.flatten()
.collect::<Vec<NImport>>(),
)),
Stmt::ImportFrom(StmtImportFrom { level: Some(i), module, .. }) if i.to_u32() > 0 => {
Some(process_import(
module.map(|x| x.to_string()),
@@ -124,15 +216,25 @@ fn parse_code_for_imports(code: &str, path: &str) -> error::Result<Vec<String>>
i.to_usize(),
))
}
Stmt::ImportFrom(StmtImportFrom { level: _, module, .. }) => {
Some(process_import(module.map(|x| x.to_string()), path, 0))
}
Stmt::ImportFrom(StmtImportFrom { level: _, module, range, .. }) => find_pin(
range,
module.clone().map(|x| x.to_string()).unwrap_or_default(),
)
.or(Some(process_import(module.map(|x| x.to_string()), path, 0))),
_ => None,
})
.flatten()
.filter(|x| !STDIMPORTS.contains(&x.as_str()))
.filter(|x| {
if let NImport::Auto { ref pkg, .. } = x {
!STDIMPORTS.contains(&(*pkg).as_str())
} else {
true
}
})
.unique()
.collect();
nimports.sort();
return Ok(nimports);
}
@@ -143,8 +245,9 @@ pub async fn parse_python_imports(
db: &Pool<Postgres>,
already_visited: &mut Vec<String>,
annotated_pyv_numeric: &mut Option<u32>,
) -> error::Result<Vec<String>> {
parse_python_imports_inner(
) -> error::Result<(Vec<String>, Option<String>)> {
let mut compile_error_hint: Option<String> = None;
let mut imports = parse_python_imports_inner(
code,
w_id,
path,
@@ -153,7 +256,32 @@ pub async fn parse_python_imports(
annotated_pyv_numeric,
&mut annotated_pyv_numeric.and_then(|_| Some(path.to_owned())),
)
.await
.await?
.into_values()
.map(|nimport| match nimport {
NImportResolved::Pin { pins, .. } => pins.into_iter().map(|p| {
if let Some(hint) = &mut compile_error_hint{
hint.push_str(&format!("\n - pin to {} in {}", p.pkg, p.path));
} else {
compile_error_hint = Some("\n\nMultiple pins can cause problems during lockfile resolution.\nMake sure you checked every pin for conflicts:\n".into())
};
Ok(p.pkg)
}).collect_vec(),
NImportResolved::Repin { pin: ImportPin { pkg, .. }, .. } => vec![Ok(pkg)],
NImportResolved::Auto { pkg, ..} => vec![Ok(pkg)],
})
.flatten()
.collect::<error::Result<Vec<String>>>()?
.into_iter()
.unique()
.collect_vec();
imports.sort();
compile_error_hint
.as_mut()
.map(|e| e.push_str("\n\nNOTE: You can also `repin` to override all pins"));
Ok((imports, compile_error_hint))
}
#[async_recursion]
@@ -165,7 +293,7 @@ async fn parse_python_imports_inner(
already_visited: &mut Vec<String>,
annotated_pyv_numeric: &mut Option<u32>,
path_where_annotated_pyv: &mut Option<String>,
) -> error::Result<Vec<String>> {
) -> error::Result<HashMap<String, NImportResolved>> {
let PythonAnnotations { py310, py311, py312, py313, .. } = PythonAnnotations::parse(&code);
// we pass only if there is none or only one annotation
@@ -194,7 +322,6 @@ async fn parse_python_imports_inner(
} else {
*annotated_pyv_numeric = Some(numeric);
}
*path_where_annotated_pyv = Some(path.to_owned());
}
Ok(())
@@ -209,74 +336,205 @@ async fn parse_python_imports_inner(
.lines()
.find_position(|x| x.starts_with("#requirements:") || x.starts_with("# requirements:"));
if let Some((pos, _)) = find_requirements {
let lines = code
.lines()
let mut requirements = HashMap::new();
code.lines()
.skip(pos + 1)
.map_while(|x| {
RE.captures(x)
.map(|x| x.get(1).unwrap().as_str().to_string())
RE.captures(x).and_then(|x| {
x.get(1).map(|m| {
let requirement = m.as_str().to_string();
requirements.insert(
requirement.clone(),
NImportResolved::Repin {
pin: ImportPin { pkg: requirement, path: Default::default() },
key: Default::default(),
},
);
})
})
})
.collect();
Ok(lines)
.collect_vec();
Ok(requirements)
} else {
let find_extra_requirements = code.lines().find_position(|x| {
x.starts_with("#extra_requirements:") || x.starts_with("# extra_requirements:")
});
let mut imports: Vec<String> = vec![];
let mut imports: HashMap<String, NImportResolved> = HashMap::new();
if let Some((pos, _)) = find_extra_requirements {
let lines: Vec<String> = code
.lines()
code.lines()
.skip(pos + 1)
.map_while(|x| {
RE.captures(x)
.map(|x| x.get(1).unwrap().as_str().to_string())
RE.captures(x).and_then(|x| {
x.get(1).map(|m| {
let requirement = m.as_str().to_string();
imports.insert(
requirement.clone(),
NImportResolved::Auto { key: None, pkg: requirement },
);
})
})
})
.collect();
imports.extend(lines);
.collect_vec();
}
let nimports = parse_code_for_imports(code, path)?;
for n in nimports.iter() {
let nested = if n.starts_with("relative:") {
let rpath = n.replace("relative:", "");
let code = sqlx::query_scalar!(
r#"
SELECT content FROM script WHERE path = $1 AND workspace_id = $2
AND created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND
workspace_id = $2)
"#,
&rpath,
w_id
)
.fetch_optional(db)
.await?
.unwrap_or_else(|| "".to_string());
// Will get unsorted vector of imports found in current script
let mut nimports = parse_code_for_imports(code, path)?;
if already_visited.contains(&rpath) {
vec![]
} else {
already_visited.push(rpath.clone());
parse_python_imports_inner(
&code,
w_id,
// It is important to note, that sorting is important and will always result in this pattern:
// 1. All Repins go first
// 2. All Pins go second
// 3. All Auto go third
// 4. All relative imports go the last
//
// This way we make sure all repins are resolved before (re)pins inside imported relative scripts.
nimports.sort();
for n in nimports.into_iter() {
let mut nested = match n {
NImport::Relative(rpath) => {
let code = sqlx::query_scalar!(
r#"
SELECT content FROM script WHERE path = $1 AND workspace_id = $2
AND created_at = (SELECT max(created_at) FROM script WHERE path = $1 AND
workspace_id = $2)
"#,
&rpath,
db,
already_visited,
annotated_pyv_numeric,
path_where_annotated_pyv,
w_id
)
.fetch_optional(db)
.await?
.unwrap_or_else(|| "".to_string());
if already_visited.contains(&rpath) {
vec![]
} else {
already_visited.push(rpath.clone());
// Because the algo goes depth first, this function will never return relative import
// This why we can safely assume later, that there is no relative imports
parse_python_imports_inner(
&code,
w_id,
&rpath,
db,
already_visited,
annotated_pyv_numeric,
path_where_annotated_pyv,
)
.await?
.into_values()
.collect_vec()
}
}
} else {
vec![n.to_string()]
NImport::Repin { pin, key } => vec![NImportResolved::Repin { pin, key }],
NImport::Pin { pins, key } => vec![NImportResolved::Pin { pins, key }],
NImport::Auto { pkg, key } => vec![NImportResolved::Auto { pkg, key }],
};
// Nested should also be sorted for the same reason
nested.sort();
// At this point there should be no NImport::Relative in `nested`
for imp in nested {
if !imports.contains(&imp) {
imports.push(imp);
let key = match imp.clone() {
NImportResolved::Pin { key, .. } => key,
NImportResolved::Repin { key, .. } => key,
NImportResolved::Auto { key, pkg } => key.unwrap_or(pkg),
};
// Handled cases:
//
// 1.
// Error: Imported windmill scripts have different pins
//
// auto
// ├── pin:2
// └── pin:1
//
// Fix 1:
//
// auto
// ├── pin:1
// └── pin:1
//
// Fix 2:
//
// repin:1
// ├── pin:2
// └── pin:1
//
// 2.
// Error: Imported windmill scripts have different pins
//
// pin:2
// └── pin:1
//
// Fix 1:
//
// auto
// └── pin:1
//
// Fix 2:
//
// repin:2
// └── pin:1
//
// 3. repins allowed to be repinned again
//
// repin:2
// └── repin:1
//
match imp.clone() {
NImportResolved::Repin { .. } => {
if let Some(existing_import) = imports.get(&key) {
match existing_import {
// replace
p if matches!(
p,
NImportResolved::Pin { .. } | NImportResolved::Auto { .. }
) =>
{
imports.insert(key, imp);
}
// do nothing (older repins have greater precedence)
NImportResolved::Repin { .. } => {}
// Should not be possible
_ => {
return Err(anyhow::anyhow!(
"Internal error: cannot resolve requirement pins",
)
.into());
}
}
} else {
imports.insert(key, imp.clone());
}
}
NImportResolved::Pin { pins: new_pins, .. } => {
if let Some(existing_import) = imports.get_mut(&key) {
match existing_import {
// Check if pin is the same version, if same, do nothing, if not error
NImportResolved::Pin { pins: existing_pins, .. } => {
existing_pins.extend(new_pins)
}
// do nothing
NImportResolved::Repin { .. } => {}
// Replace with new pin
NImportResolved::Auto { .. } => {
imports.insert(key, imp);
}
}
} else {
imports.insert(key, imp.clone());
}
}
NImportResolved::Auto { .. } => {
if !imports.contains_key(&key) {
imports.insert(key, imp);
}
}
}
}
}
imports.sort();
Ok(imports)
}
}
@@ -19,7 +19,7 @@ def main():
";
let mut already_visited = vec![];
let r = parse_python_imports(
let (r, ..) = parse_python_imports(
code,
"test-workspace",
"f/foo/bar",
@@ -52,7 +52,7 @@ def main():
";
let mut already_visited = vec![];
let r = parse_python_imports(
let (r, ..) = parse_python_imports(
code,
"test-workspace",
"f/foo/bar",
@@ -83,7 +83,7 @@ def main():
";
let mut already_visited = vec![];
let r = parse_python_imports(
let (r, ..) = parse_python_imports(
code,
"test-workspace",
"f/foo/bar",
+51
View File
@@ -0,0 +1,51 @@
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'test-user',
'
# requirements:
# microdot==2.2.0
import pandas
import requests
import tiny # pin: tiny==0.1.2
def main():
pass
',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system/requirements', 12346, 'python3', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'test-user',
'
# extra_requirements:
# bottle==0.13.2
import tiny
def main():
pass
',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system/extra_requirements', 12347, 'python3', '');
INSERT INTO public.script(workspace_id, created_by, content, schema, summary, description, path, hash, language, lock) VALUES (
'test-workspace',
'test-user',
'
import tiny # pin: bottle==0.13.2
import simplejson # pin: simplejson==3.19.3
def main():
return [test1(), test2(), test3(), test4()]
',
'{"$schema":"https://json-schema.org/draft/2020-12/schema","properties":{},"required":[],"type":"object"}',
'',
'',
'f/system/pins', 12348, 'python3', '');
+203
View File
@@ -3831,6 +3831,209 @@ def main():
run_preview_relative_imports(&db, content, ScriptLang::Python3).await;
}
async fn assert_lockfile(
db: &Pool<Postgres>,
script_content: String,
language: ScriptLang,
expected_lines: Vec<&str>,
) {
initialize_tracing().await;
let server = ApiServer::start(db.clone()).await;
let port = server.addr.port();
let client = windmill_api_client::create_client(
&format!("http://localhost:{port}"),
"SECRET_TOKEN".to_string(),
);
client
.create_script(
"test-workspace",
&NewScript {
language: NewScriptLanguage::from_str(language.as_str()).unwrap(),
content: script_content,
path: "f/system/test_import".to_string(),
concurrent_limit: None,
concurrency_time_window_s: None,
cache_ttl: None,
dedicated_worker: None,
description: "".to_string(),
draft_only: None,
envs: vec![],
is_template: None,
kind: None,
parent_hash: None,
lock: None,
summary: "".to_string(),
tag: None,
schema: std::collections::HashMap::new(),
ws_error_handler_muted: Some(false),
priority: None,
delete_after_use: None,
timeout: None,
restart_unless_cancelled: None,
deployment_message: None,
concurrency_key: None,
visible_to_runner_only: None,
no_main_func: None,
codebase: None,
has_preprocessor: None,
on_behalf_of_email: None,
},
)
.await
.unwrap();
let mut completed = listen_for_completed_jobs(&db).await;
let db2 = db.clone();
in_test_worker(
&db,
async move {
completed.next().await; // deployed script
let script = sqlx::query!(
"SELECT hash FROM script WHERE path = $1",
"f/system/test_import".to_string()
)
.fetch_one(&db2)
.await
.unwrap();
let job = RunJob::from(JobPayload::Dependencies {
path: "f/system/test_import".to_string(),
hash: ScriptHash(script.hash),
dedicated_worker: None,
language,
})
.push(&db2)
.await;
completed.next().await; // completed job
let result = completed_job(job, &db2).await.json_result().unwrap();
assert_eq!(
result,
json!({
"lock": expected_lines.join("\n"),
"status": "Successful lock file generation"
})
);
},
port,
)
.await;
}
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_requirements_python(db: Pool<Postgres>) {
let content = r#"
# py311
# requirements:
# tiny==0.1.3
import bar
import baz # pin: foo
import baz # repin: fee
import bug # repin: free
def main():
pass
"#
.to_string();
assert_lockfile(
&db,
content,
ScriptLang::Python3,
vec!["# py311", "tiny==0.1.3"],
)
.await;
}
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_extra_requirements_python(db: Pool<Postgres>) {
{
let content = r#"
# py311
# extra_requirements:
# tiny
import f.system.extra_requirements
import tiny # pin: tiny==0.1.0
import tiny # pin: tiny==0.1.1
import tiny # repin: tiny==0.1.2
def main():
pass
"#
.to_string();
assert_lockfile(
&db,
content,
ScriptLang::Python3,
vec!["# py311", "bottle==0.13.2", "tiny==0.1.2"],
)
.await;
}
}
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_extra_requirements_python2(db: Pool<Postgres>) {
let content = r#"
# py311
# extra_requirements:
# tiny==0.1.3
import simplejson # pin: simplejson==3.20.1
def main():
pass
"#
.to_string();
assert_lockfile(
&db,
content,
ScriptLang::Python3,
vec![
"# py311",
"simplejson==3.20.1",
"tiny==0.1.3"
],
)
.await;
}
#[sqlx::test(fixtures("base", "lockfile_python"))]
async fn test_pins_python(db: Pool<Postgres>) {
let content = r#"
# py311
# extra_requirements:
# tiny==0.1.3
import f.system.requirements
import f.system.pins
import tiny # repin: bottle==0.13.0
import simplejson
def main():
pass
"#
.to_string();
assert_lockfile(
&db,
content,
ScriptLang::Python3,
vec![
"# py311",
"bottle==0.13.0",
"microdot==2.2.0",
"simplejson==3.19.3",
"tiny==0.1.3"
],
)
.await;
}
#[sqlx::test(fixtures("base", "result_format"))]
async fn test_result_format(db: Pool<Postgres>) {
let ordered_result_job_id = "1eecb96a-c8b0-4a3d-b1b6-087878c55e41";
+22 -14
View File
@@ -1439,6 +1439,7 @@ async fn handle_python_deps(
.clone();
let mut requirements;
let compilation_error_hint;
let mut annotated_pyv = None;
let mut annotated_pyv_numeric = None;
let is_deployed = requirements_o.is_some();
@@ -1449,23 +1450,26 @@ async fn handle_python_deps(
None => {
let mut already_visited = vec![];
requirements = match conn {
Connection::Sql(db) => windmill_parser_py_imports::parse_python_imports(
inner_content,
w_id,
script_path,
db,
&mut already_visited,
&mut annotated_pyv_numeric,
)
.await?
.join("\n"),
(requirements, compilation_error_hint) = match conn {
Connection::Sql(db) => {
let (r, h) = windmill_parser_py_imports::parse_python_imports(
inner_content,
w_id,
script_path,
db,
&mut already_visited,
&mut annotated_pyv_numeric,
)
.await?;
(r.join("\n"), h)
}
Connection::Http(_) => match precomputed_agent_info {
Some(PrecomputedAgentInfo::Python { py_version, requirements }) => {
annotated_pyv_numeric = py_version;
requirements.clone().unwrap_or_else(|| "".to_string())
(requirements.clone().unwrap_or_else(|| "".to_string()), None)
}
_ => "".to_string(),
_ => ("".to_string(), None),
},
};
@@ -1487,7 +1491,11 @@ async fn handle_python_deps(
)
.await
.map_err(|e| {
Error::ExecutionErr(format!("pip compile failed: {}", e.to_string()))
Error::ExecutionErr(format!(
"pip compile failed: {}{}",
e.to_string(),
compilation_error_hint.unwrap_or_default()
))
})?;
}
&requirements
@@ -1853,6 +1853,7 @@ async fn capture_dependency_job(
&mut annotated_pyv_numeric,
)
.await?
.0
.join("\n")
};