Adapt the structure to parse the initial pip commands

This commit is contained in:
Reinier Criel 2025-10-22 15:28:27 -07:00
parent 982da4aa77
commit 1b82aeb6b0
7 changed files with 273 additions and 22 deletions

View file

@ -1,6 +1,11 @@
import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
import { nullScanner } from "./dependencyScanner/nullScanner.js";
import { runPip } from "./runPipCommand.js";
import {
getPipCommandForArgs,
pipInstallCommand,
pipUninstallCommand,
} from "./utils/pipCommands.js";
/**
* Creates a package manager interface for Python's pip package installer
@ -8,28 +13,40 @@ import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/reg
* @param {string} [command="pip"] - The pip command to use (e.g., "pip", "pip3") defaults to "pip"
*/
export function createPipPackageManager(command = "pip") {
return {
runCommand: (args) => runPipCommand(command, args),
function isSupportedCommand(args) {
const scanner = findDependencyScannerForCommand(
commandScannerMapping,
args
);
return scanner.shouldScan(args);
}
// For pip, set proxy server
isSupportedCommand: () => false,
getDependencyUpdatesForCommand: () => [],
function getDependencyUpdatesForCommand(args) {
const scanner = findDependencyScannerForCommand(
commandScannerMapping,
args
);
return scanner.scan(args);
}
return {
runCommand: (args) => runPip(command, args),
isSupportedCommand,
getDependencyUpdatesForCommand,
};
}
async function runPipCommand(command, args) {
try {
const result = await safeSpawn(command, args, {
stdio: "inherit",
env: mergeSafeChainProxyEnvironmentVariables(process.env),
});
return { status: result.status };
} catch (error) {
if (error.status) {
return { status: error.status };
} else {
ui.writeError("Error executing command:", error.message);
return { status: 1 };
}
const commandScannerMapping = {
[pipInstallCommand]: commandArgumentScanner(),
[pipUninstallCommand]: nullScanner(), // Uninstall doesn't need scanning
};
function findDependencyScannerForCommand(scanners, args) {
const command = getPipCommandForArgs(args);
if (!command) {
return nullScanner();
}
const scanner = scanners[command];
return scanner ? scanner : nullScanner();
}

View file

@ -0,0 +1,27 @@
/**
* Scanner for pip command arguments to detect package installations
*
* @param {Object} options - Scanner options
* @param {boolean} [options.ignoreDryRun=false] - Whether to ignore dry-run flag
* @returns {Object} Scanner interface
*/
export function commandArgumentScanner(options = {}) {
const { ignoreDryRun = false } = options;
function shouldScan(args) {
// For now, pip scanning is not yet implemented
// This would need to detect 'install' commands and package arguments
return false;
}
function scan(args) {
// Future implementation would parse pip install arguments
// and return array of {name, version, type} objects
return [];
}
return {
shouldScan,
scan,
};
}

View file

@ -0,0 +1,10 @@
/**
* Null scanner that returns no dependencies
* Used when a command is not supported for scanning
*/
export function nullScanner() {
return {
shouldScan: () => false,
scan: () => [],
};
}

View file

@ -0,0 +1,62 @@
/**
* Parses package specifications from pip install arguments
*
* Pip supports various package specification formats:
* - package_name
* - package_name==version
* - package_name>=version
* - package_name~=version
* - git+https://...
* - -r requirements.txt
* - . (local directory)
*
* @param {string[]} args - pip install command arguments
* @returns {Array<{name: string, version?: string, type: string}>} Array of package specifications
*/
export function parsePackagesFromInstallArgs(args) {
const packages = [];
let skipNext = false;
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (skipNext) {
skipNext = false;
continue;
}
// Skip the command itself (install, uninstall, etc.)
if (i === 0 && !arg.startsWith("-")) {
continue;
}
// Skip flags and their values
if (arg.startsWith("-")) {
// Some flags take a value, skip the next arg for those
if (arg === "-r" || arg === "--requirement" ||
arg === "-c" || arg === "--constraint" ||
arg === "-e" || arg === "--editable" ||
arg === "-t" || arg === "--target" ||
arg === "-i" || arg === "--index-url" ||
arg === "--extra-index-url") {
skipNext = true;
}
continue;
}
// TODO: Implement full parsing logic
// For now, this is a placeholder that would need to handle:
// - Version specifiers (==, >=, <=, ~=, !=, <, >)
// - VCS urls (git+, hg+, svn+, bzr+)
// - Local file paths
// - Requirements files (-r, --requirement)
// - Extras (package[extra1,extra2])
packages.push({
name: arg,
type: "add",
});
}
return packages;
}

View file

@ -0,0 +1,40 @@
import { describe, it } from "node:test";
import assert from "node:assert";
import { parsePackagesFromInstallArgs } from "./parsePackagesFromInstallArgs.js";
describe("parsePackagesFromInstallArgs", () => {
it("should parse simple package name", () => {
const result = parsePackagesFromInstallArgs(["install", "requests"]);
assert.deepEqual(result, [
{ name: "requests", type: "add" },
]);
});
it("should parse package with version specifier", () => {
const result = parsePackagesFromInstallArgs(["install", "requests==2.28.0"]);
assert.deepEqual(result, [
{ name: "requests==2.28.0", type: "add" },
]);
});
it("should skip flags", () => {
const result = parsePackagesFromInstallArgs(["install", "--upgrade", "requests"]);
assert.deepEqual(result, [
{ name: "requests", type: "add" },
]);
});
it("should parse multiple packages", () => {
const result = parsePackagesFromInstallArgs(["install", "requests", "flask", "django==4.0"]);
assert.deepEqual(result, [
{ name: "requests", type: "add" },
{ name: "flask", type: "add" },
{ name: "django==4.0", type: "add" },
]);
});
it("should return empty array for no packages", () => {
const result = parsePackagesFromInstallArgs(["install", "--help"]);
assert.deepEqual(result, []);
});
});

View file

@ -0,0 +1,66 @@
import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
/**
* Runs a pip command with the specified arguments
*
* @param {string} command - The pip command to use (e.g., "pip", "pip3")
* @param {string[]} args - Command arguments
* @returns {Promise<{status: number}>} Result object with status code
*/
export async function runPip(command, args) {
try {
const result = await safeSpawn(command, args, {
stdio: "inherit",
env: mergeSafeChainProxyEnvironmentVariables(process.env),
});
return { status: result.status };
} catch (error) {
if (error.status) {
return { status: error.status };
} else {
ui.writeError("Error executing command:", error.message);
return { status: 1 };
}
}
}
/**
* Runs a pip command in dry-run mode and captures output
* Note: pip doesn't have a native --dry-run flag, so this may need adjustment
*
* @param {string} command - The pip command to use
* @param {string[]} args - Command arguments
* @returns {Promise<{status: number, output: string}>} Result with status and output
*/
export async function dryRunPipCommandAndOutput(command, args) {
try {
// Note: pip doesn't have a --dry-run flag like npm
// This would need to be implemented differently if dry-run functionality is needed
const result = await safeSpawn(
command,
args,
{
stdio: "pipe",
env: mergeSafeChainProxyEnvironmentVariables(process.env),
}
);
return {
status: result.status,
output: result.status === 0 ? result.stdout : result.stderr,
};
} catch (error) {
if (error.status) {
const output =
error.stdout?.toString() ??
error.stderr?.toString() ??
error.message ??
"";
return { status: error.status, output };
} else {
ui.writeError("Error executing command:", error.message);
return { status: 1 };
}
}
}

View file

@ -0,0 +1,29 @@
/**
* Pip command constants
*/
export const pipInstallCommand = "install";
export const pipUninstallCommand = "uninstall";
export const pipListCommand = "list";
export const pipShowCommand = "show";
export const pipFreeze = "freeze";
/**
* Gets the pip command from the arguments array
*
* @param {string[]} args - Command line arguments
* @returns {string|null} The pip command or null if not found
*/
export function getPipCommandForArgs(args) {
if (!args || args.length === 0) {
return null;
}
// The first non-flag argument is typically the command
for (const arg of args) {
if (!arg.startsWith("-")) {
return arg;
}
}
return null;
}