Skip to content

Commit 0384a58

Browse files
authored
fix: correctly return custom python invocation error messages (#185)
1 parent 3c0ab07 commit 0384a58

File tree

16 files changed

+168
-203
lines changed

16 files changed

+168
-203
lines changed

src/providers/python_controller.js

Lines changed: 75 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,26 @@
1-
import {execSync} from "node:child_process";
21
import fs from "node:fs";
32
import path from 'node:path';
43
import os, {EOL} from "os";
5-
import {environmentVariableIsPopulated,getCustom, handleSpacesInPath} from "../tools.js";
6-
4+
import {environmentVariableIsPopulated,getCustom, invokeCommand} from "../tools.js";
75

86
function getPipFreezeOutput() {
9-
return environmentVariableIsPopulated("EXHORT_PIP_FREEZE") ? new Buffer(process.env["EXHORT_PIP_FREEZE"],'base64').toString('ascii') : execSync(`${handleSpacesInPath(this.pathToPipBin)} freeze --all`, err => {
10-
if (err) {
11-
throw new Error('fail invoking pip freeze to fetch all installed dependencies in environment --> ' + err.message)
12-
}
13-
}).toString();
7+
try {
8+
return environmentVariableIsPopulated("EXHORT_PIP_FREEZE") ? new Buffer.from(process.env["EXHORT_PIP_FREEZE"], 'base64').toString('ascii') : invokeCommand(this.pathToPipBin, ['freeze', '--all']).toString();
9+
} catch (error) {
10+
throw new Error('Failed invoking \'pip freeze\' to list all installed packages in environment', {cause: error})
11+
}
1412
}
1513

1614
function getPipShowOutput(depNames) {
17-
18-
return environmentVariableIsPopulated("EXHORT_PIP_SHOW") ? new Buffer(process.env["EXHORT_PIP_SHOW"],'base64').toString('ascii') : execSync(`${handleSpacesInPath(this.pathToPipBin)} show ${depNames}`, err => {
19-
if (err) {
20-
throw new Error('fail invoking pip show to fetch all installed dependencies metadata --> ' + err.message)
21-
}
22-
}).toString();
15+
try {
16+
return environmentVariableIsPopulated("EXHORT_PIP_SHOW") ? new Buffer.from(process.env["EXHORT_PIP_SHOW"], 'base64').toString('ascii') : invokeCommand(this.pathToPipBin, ['show', ...depNames]).toString();
17+
} catch (error) {
18+
throw new Error('fail invoking \'pip show\' to fetch metadata for all installed packages in environment', {cause: error})
19+
}
2320
}
2421

2522
/** @typedef {{name: string, version: string, dependencies: DependencyEntry[]}} DependencyEntry */
2623

27-
28-
2924
export default class Python_controller {
3025

3126
pythonEnvDir
@@ -51,26 +46,23 @@ export default class Python_controller {
5146
this.pathToRequirements = pathToRequirements
5247
this.options = options
5348
}
54-
prepareEnvironment()
55-
{
49+
prepareEnvironment() {
5650
if(!this.realEnvironment) {
57-
this.pythonEnvDir = path.join(path.sep,"tmp","exhort_env_js")
58-
execSync(`${handleSpacesInPath(this.pathToPythonBin)} -m venv ${handleSpacesInPath(this.pythonEnvDir)} `, err => {
59-
if (err) {
60-
throw new Error('failed creating virtual python environment - ' + err.message)
61-
}
62-
})
63-
if(this.pathToPythonBin.includes("python3"))
64-
{
51+
this.pythonEnvDir = path.join(path.sep, "tmp", "exhort_env_js")
52+
try {
53+
invokeCommand(this.pathToPythonBin, ['-m', 'venv', this.pythonEnvDir])
54+
} catch (error) {
55+
throw new Error('Failed creating virtual python environment', {cause: error})
56+
}
57+
if(this.pathToPythonBin.includes("python3")) {
6558
this.pathToPipBin = path.join(path.sep,this.pythonEnvDir,os.platform() === 'win32' ? "Scripts" : "bin",this.#decideIfWindowsOrLinuxPath("pip3"))
6659
this.pathToPythonBin = path.join(path.sep,this.pythonEnvDir,os.platform() === 'win32' ? "Scripts" : "bin",this.#decideIfWindowsOrLinuxPath("python3"))
6760
if(os.platform() === 'win32') {
6861
let driveLetter = path.parse(process.cwd()).root
6962
this.pathToPythonBin = `${driveLetter}${this.pathToPythonBin.substring(1)}`
7063
this.pathToPipBin = `${driveLetter}${this.pathToPipBin.substring(1)}`
7164
}
72-
}
73-
else {
65+
} else {
7466
this.pathToPipBin = path.join(path.sep,this.pythonEnvDir,os.platform() === 'win32' ? "Scripts" : "bin",this.#decideIfWindowsOrLinuxPath("pip"));
7567
this.pathToPythonBin = path.join(path.sep,this.pythonEnvDir,os.platform() === 'win32' ? "Scripts" : "bin",this.#decideIfWindowsOrLinuxPath("python"))
7668
if(os.platform() === 'win32') {
@@ -80,18 +72,15 @@ export default class Python_controller {
8072
}
8173
}
8274
// upgrade pip version to latest
83-
execSync(`${handleSpacesInPath(this.pathToPythonBin)} -m pip install --upgrade pip `, err => {
84-
if (err) {
85-
throw new Error('failed upgrading pip version on virtual python environment - ' + err.message)
86-
}
87-
})
88-
}
89-
else{
75+
try {
76+
invokeCommand(this.pathToPythonBin, ['-m', 'pip', 'install', '--upgrade', 'pip'])
77+
} catch (error) {
78+
throw new Error('Failed upgrading pip version in virtual python environment', {cause: error})
79+
}
80+
} else {
9081
if(this.pathToPythonBin.startsWith("python")) {
9182
this.pythonEnvDir = process.cwd()
92-
}
93-
else
94-
{
83+
} else {
9584
this.pythonEnvDir = path.dirname(this.pathToPythonBin)
9685
}
9786
}
@@ -100,8 +89,7 @@ export default class Python_controller {
10089
#decideIfWindowsOrLinuxPath(fileName) {
10190
if (os.platform() === "win32") {
10291
return fileName + ".exe"
103-
}
104-
else {
92+
} else {
10593
return fileName
10694
}
10795
}
@@ -110,8 +98,7 @@ export default class Python_controller {
11098
* @param {boolean} includeTransitive - whether to return include in returned object transitive dependencies or not
11199
* @return {[DependencyEntry]}
112100
*/
113-
getDependencies(includeTransitive)
114-
{
101+
getDependencies(includeTransitive) {
115102
let startingTime
116103
let endingTime
117104
if (process.env["EXHORT_DEBUG"] === "true") {
@@ -120,21 +107,19 @@ export default class Python_controller {
120107
}
121108
if(!this.realEnvironment) {
122109
let installBestEfforts = getCustom("EXHORT_PYTHON_INSTALL_BEST_EFFORTS","false",this.options);
123-
if(installBestEfforts === "false")
124-
{
125-
execSync(`${handleSpacesInPath(this.pathToPipBin)} install -r ${handleSpacesInPath(this.pathToRequirements)}`, err =>{
126-
if (err) {
127-
throw new Error('fail installing requirements.txt manifest in created virtual python environment --> ' + err.message)
128-
}
129-
})
110+
if(installBestEfforts === "false") {
111+
try {
112+
invokeCommand(this.pathToPipBin, ['install', '-r', this.pathToRequirements])
113+
} catch (error) {
114+
throw new Error('Failed installing requirements.txt manifest in virtual python environment', {cause: error})
115+
}
130116
}
131117
// make best efforts to install the requirements.txt on the virtual environment created from the python3 passed in.
132118
// that means that it will install the packages without referring to the versions, but will let pip choose the version
133119
// tailored for version of the python environment( and of pip package manager) for each package.
134120
else {
135121
let matchManifestVersions = getCustom("MATCH_MANIFEST_VERSIONS","true",this.options);
136-
if(matchManifestVersions === "true")
137-
{
122+
if(matchManifestVersions === "true") {
138123
throw new Error("Conflicting settings, EXHORT_PYTHON_INSTALL_BEST_EFFORTS=true can only work with MATCH_MANIFEST_VERSIONS=false")
139124
}
140125
this.#installingRequirementsOneByOne()
@@ -156,27 +141,26 @@ export default class Python_controller {
156141
let requirementsRows = requirementsContent.toString().split(EOL);
157142
requirementsRows.filter((line) => !line.trim().startsWith("#")).filter((line) => line.trim() !== "").forEach( (dependency) => {
158143
let dependencyName = getDependencyName(dependency);
159-
execSync(`${handleSpacesInPath(this.pathToPipBin)} install ${dependencyName}`, err =>{
160-
if (err) {
161-
throw new Error(`Best efforts process - failed installing ${dependencyName} in created virtual python environment --> error message: ` + err.message)
162-
}
163-
})
164-
} )
144+
try {
145+
invokeCommand(this.pathToPipBin, ['install', dependencyName])
146+
} catch (error) {
147+
throw new Error(`Failed in best-effort installing ${dependencyName} in virtual python environment`, {cause: error})
148+
}
149+
})
165150
}
166151
/**
167152
* @private
168153
*/
169-
#cleanEnvironment()
170-
{
171-
if(!this.realEnvironment)
172-
{
173-
execSync(`${handleSpacesInPath(this.pathToPipBin)} uninstall -y -r ${handleSpacesInPath(this.pathToRequirements)}`, err =>{
174-
if (err) {
175-
throw new Error('fail uninstalling requirements.txt in created virtual python environment --> ' + err.message)
176-
}
177-
})
154+
#cleanEnvironment() {
155+
if(!this.realEnvironment) {
156+
try {
157+
invokeCommand(this.pathToPipBin, ['uninstall', '-y', '-r', this.pathToRequirements])
158+
} catch (error) {
159+
throw new Error('Failed uninstalling requirements.txt in virtual python environment', {cause: error})
160+
}
178161
}
179162
}
163+
180164
#getDependenciesImpl(includeTransitive) {
181165
let dependencies = new Array()
182166
let usePipDepTree = getCustom("EXHORT_PIP_USE_DEP_TREE","false",this.options);
@@ -189,7 +173,7 @@ export default class Python_controller {
189173
if(usePipDepTree !== "true") {
190174
freezeOutput = getPipFreezeOutput.call(this);
191175
lines = freezeOutput.split(EOL)
192-
depNames = lines.map( line => getDependencyName(line)).join(" ")
176+
depNames = lines.map( line => getDependencyName(line))
193177
}
194178
else {
195179
pipDepTreeJsonArrayOutput = getDependencyTreeJsonFromPipDepTree(this.pathToPipBin,this.pathToPythonBin)
@@ -198,7 +182,7 @@ export default class Python_controller {
198182

199183
if(usePipDepTree !== "true") {
200184
pipShowOutput = getPipShowOutput.call(this, depNames);
201-
allPipShowDeps = pipShowOutput.split( EOL +"---" + EOL);
185+
allPipShowDeps = pipShowOutput.split( EOL + "---" + EOL);
202186
}
203187
//debug
204188
// pipShowOutput = "alternative pip show output goes here for debugging"
@@ -213,8 +197,7 @@ export default class Python_controller {
213197
CachedEnvironmentDeps[dependencyName.replace("-", "_")] = record
214198
CachedEnvironmentDeps[dependencyName.replace("_", "-")] = record
215199
})
216-
}
217-
else {
200+
} else {
218201
pipDepTreeJsonArrayOutput.forEach( depTreeEntry => {
219202
let packageName = depTreeEntry["package"]["package_name"].toLowerCase()
220203
let pipDepTreeEntryForCache = {
@@ -229,18 +212,15 @@ export default class Python_controller {
229212
}
230213
linesOfRequirements.forEach( (dep) => {
231214
// if matchManifestVersions setting is turned on , then
232-
if(matchManifestVersions === "true")
233-
{
215+
if(matchManifestVersions === "true") {
234216
let dependencyName
235217
let manifestVersion
236218
let installedVersion
237219
let doubleEqualSignPosition
238-
if(dep.includes("=="))
239-
{
220+
if(dep.includes("==")) {
240221
doubleEqualSignPosition = dep.indexOf("==")
241222
manifestVersion = dep.substring(doubleEqualSignPosition + 2).trim()
242-
if(manifestVersion.includes("#"))
243-
{
223+
if(manifestVersion.includes("#")) {
244224
let hashCharIndex = manifestVersion.indexOf("#");
245225
manifestVersion = manifestVersion.substring(0,hashCharIndex)
246226
}
@@ -249,17 +229,15 @@ export default class Python_controller {
249229
if(CachedEnvironmentDeps[dependencyName.toLowerCase()] !== undefined) {
250230
if(usePipDepTree !== "true") {
251231
installedVersion = getDependencyVersion(CachedEnvironmentDeps[dependencyName.toLowerCase()])
252-
}
253-
else {
232+
} else {
254233
installedVersion = CachedEnvironmentDeps[dependencyName.toLowerCase()].version
255234
}
256235
}
257236
if(installedVersion) {
258237
if (manifestVersion.trim() !== installedVersion.trim()) {
259-
throw new Error(`Can't continue with analysis - versions mismatch for dependency name ${dependencyName}, manifest version=${manifestVersion}, installed Version=${installedVersion}, if you want to allow version mismatch for analysis between installed and requested packages, set environment variable/setting - MATCH_MANIFEST_VERSIONS=false`)
238+
throw new Error(`Can't continue with analysis - versions mismatch for dependency name ${dependencyName} (manifest version=${manifestVersion}, installed version=${installedVersion}).If you want to allow version mismatch for analysis between installed and requested packages, set environment variable/setting MATCH_MANIFEST_VERSIONS=false`)
260239
}
261240
}
262-
263241
}
264242
}
265243
let path = new Array()
@@ -274,11 +252,11 @@ export default class Python_controller {
274252
if(DEP1 < DEP2) {
275253
return -1;
276254
}
277-
if(DEP1 > DEP2)
278-
{
255+
if(DEP1 > DEP2) {
279256
return 1;
280257
}
281-
return 0;})
258+
return 0;
259+
})
282260
return dependencies
283261
}
284262
}
@@ -321,8 +299,7 @@ function getDependencyName(depLine) {
321299
const regex = /[\w\s-_.]+/g;
322300
if(depLine.match(regex)) {
323301
result = depLine.match(regex)[0]
324-
}
325-
else {
302+
} else {
326303
result = depLine
327304
}
328305
}
@@ -358,10 +335,7 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
358335
}
359336
let record = cachedEnvironmentDeps[dependencyName.toLowerCase()]
360337
if(record === null || record === undefined) {
361-
throw new Error(`Package name=>${dependencyName} is not installed in your python environment,
362-
either install it ( better to install requirements.txt altogether) or set
363-
the setting EXHORT_PYTHON_VIRTUAL_ENV to true to automatically install
364-
it in virtual environment (please note that this may slow down the analysis) `)
338+
throw new Error(`Package ${dependencyName} is not installed in your python environment, either install it (better to install requirements.txt altogether) or set the setting EXHORT_PYTHON_VIRTUAL_ENV=true to automatically install it in virtual environment (please note that this may slow down the analysis)`)
365339
}
366340
let depName
367341
let version;
@@ -370,8 +344,7 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
370344
depName = getDependencyNameShow(record)
371345
version = getDependencyVersion(record);
372346
directDeps = getDepsList(record)
373-
}
374-
else {
347+
} else {
375348
depName = record.name
376349
version = record.version
377350
directDeps = record.dependencies
@@ -398,11 +371,11 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
398371
if(DEP1 < DEP2) {
399372
return -1;
400373
}
401-
if(DEP1 > DEP2)
402-
{
374+
if(DEP1 > DEP2) {
403375
return 1;
404376
}
405-
return 0;})
377+
return 0;
378+
})
406379

407380
entry["dependencies"] = targetDeps
408381
})
@@ -418,20 +391,19 @@ function bringAllDependencies(dependencies, dependencyName, cachedEnvironmentDep
418391
function getDependencyTreeJsonFromPipDepTree(pipPath,pythonPath) {
419392
let dependencyTree
420393
try {
421-
execSync(`${handleSpacesInPath(pipPath)} install pipdeptree`)
422-
} catch (e) {
423-
throw new Error(`Couldn't install pipdeptree utility, reason: ${e.getMessage}`)
394+
invokeCommand(pipPath, ['install', 'pipdeptree'])
395+
} catch (error) {
396+
throw new Error(`Failed installing pipdeptree utility`, {cause: error})
424397
}
425398

426399
try {
427400
if(pythonPath.startsWith("python")) {
428-
dependencyTree = execSync(`pipdeptree --json`).toString()
429-
}
430-
else {
431-
dependencyTree = execSync(`pipdeptree --json --python ${handleSpacesInPath(pythonPath)} `).toString()
401+
dependencyTree = invokeCommand('pipdeptree', ['--json']).toString()
402+
} else {
403+
dependencyTree = invokeCommand('pipdeptree', ['--json', '--python', pythonPath]).toString()
432404
}
433-
} catch (e) {
434-
throw new Error(`couldn't produce dependency tree using pipdeptree tool, stop analysis, message -> ${e.getMessage}`)
405+
} catch (error) {
406+
throw new Error(`Failed building dependency tree using pipdeptree tool, stopping analysis`, {cause: error})
435407
}
436408

437409
return JSON.parse(dependencyTree)

0 commit comments

Comments
 (0)