pi-lens
Version:
Real-time code feedback for pi — LSP, linters, formatters, type-checking, structural analysis & booboo
170 lines (167 loc) • 6.25 kB
JavaScript
import { createRequire as __pilensCreateRequire } from "node:module"; const require = __pilensCreateRequire(import.meta.url);
import {
extractTomlTableSection,
hasTomlTable,
parseTomlStringArray
} from "./chunk-PAK6IDME.js";
import {
recordDegradationOnce
} from "./chunk-N3YQJI6O.js";
import {
UV_WORKSPACE_EXCLUDE_DIALECT,
UV_WORKSPACE_MEMBERS_DIALECT,
isAtOrAboveHomeDir,
matchesWorkspaceMemberPattern,
toPosix,
walkUpDirs
} from "./chunk-Q5U6FMDI.js";
// dist/clients/python-environment.js
import { access, readFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
var UV_WORKSPACE_TABLE = "tool\\.uv\\.workspace";
async function findUvWorkspace(startDir, homeDir) {
const resolvedStart = path.resolve(startDir);
let nearestProject;
for (const dir of walkUpDirs(startDir)) {
if (isAtOrAboveHomeDir(dir, homeDir))
break;
let content;
try {
content = await readFile(path.join(dir, "pyproject.toml"), "utf8");
} catch {
continue;
}
if (!nearestProject)
nearestProject = dir;
if (hasTomlTable(content, UV_WORKSPACE_TABLE)) {
const workspaceTable = extractTomlTableSection(content, UV_WORKSPACE_TABLE);
return {
root: dir,
explicit: true,
projectRoot: nearestProject,
isStartDir: nearestProject === resolvedStart,
members: parseTomlStringArray(workspaceTable, "members"),
exclude: parseTomlStringArray(workspaceTable, "exclude")
};
}
}
return nearestProject ? {
root: nearestProject,
explicit: false,
projectRoot: nearestProject,
isStartDir: nearestProject === resolvedStart,
members: [],
exclude: []
} : void 0;
}
function isUvWorkspaceMember(workspace, projectRoot) {
const relative = toPosix(path.relative(workspace.root, projectRoot));
if (relative.length === 0 || relative === ".." || relative.startsWith("../") || path.isAbsolute(relative)) {
return false;
}
const matches = (dialect) => (pattern) => matchesWorkspaceMemberPattern(pattern, relative, dialect, () => recordDegradationOnce({
kind: "workspace-glob-cap",
subject: String(pattern.length),
reason: "workspace glob matcher exceeded its memo-table cell cap"
}));
return !workspace.exclude.some(matches(UV_WORKSPACE_EXCLUDE_DIALECT)) && workspace.members.some(matches(UV_WORKSPACE_MEMBERS_DIALECT));
}
var AMBIENT_ENVIRONMENT_VARIABLES = {
"uv-project-environment": "UV_PROJECT_ENVIRONMENT",
"virtual-env": "VIRTUAL_ENV",
conda: "CONDA_PREFIX"
};
function hasAmbientPythonEnvironment(env = process.env) {
return Object.values(AMBIENT_ENVIRONMENT_VARIABLES).some((name) => Boolean(env[name]));
}
async function detectPythonEnvironment(projectRoot, homeDir = os.homedir(), options = {}) {
const root = path.resolve(projectRoot);
const uvWorkspace = await findUvWorkspace(root, homeDir);
const isProjectRoot = uvWorkspace?.isStartDir === true;
const memberWorkspaceRoot = isProjectRoot && uvWorkspace.explicit && isUvWorkspaceMember(uvWorkspace, root) ? uvWorkspace.root : void 0;
const uvEnvironmentRoot = isProjectRoot ? memberWorkspaceRoot ?? uvWorkspace.projectRoot : void 0;
const uvProjectEnvironment = process.env.UV_PROJECT_ENVIRONMENT;
const allowAmbient = options.allowAmbient ?? true;
const isWithinProjectRoot = (candidateRoot) => {
const relative = path.relative(root, path.resolve(candidateRoot));
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && // #3871 V1: another Windows drive has no relative path, so
// `path.relative` returns the absolute target: no `..`, not inside.
!path.isAbsolute(relative);
};
const candidates = [
...uvEnvironmentRoot !== void 0 && uvProjectEnvironment ? [
{
// `path.resolve` leaves an absolute value untouched and
// anchors a relative one at the project's workspace root.
root: path.resolve(uvEnvironmentRoot, uvProjectEnvironment),
source: "uv-project-environment"
}
] : [],
...memberWorkspaceRoot !== void 0 ? [
{
root: path.join(memberWorkspaceRoot, ".venv"),
source: "uv-workspace"
}
] : [],
{ root: process.env.VIRTUAL_ENV, source: "virtual-env" },
{ root: process.env.CONDA_PREFIX, source: "conda" },
{ root: path.join(root, ".venv"), source: "project-dot-venv" },
{ root: path.join(root, "venv"), source: "project-venv" }
];
for (const candidate of candidates) {
if (!candidate.root)
continue;
if (!allowAmbient && candidate.source in AMBIENT_ENVIRONMENT_VARIABLES && !isWithinProjectRoot(candidate.root))
continue;
const binDir = path.join(candidate.root, process.platform === "win32" ? "Scripts" : "bin");
const pythonPath = path.join(binDir, process.platform === "win32" ? "python.exe" : "python");
try {
await access(pythonPath);
return {
root: candidate.root,
binDir,
pythonPath,
source: candidate.source
};
} catch {
}
}
return void 0;
}
async function detectPythonVenv(projectRoot) {
return (await detectPythonEnvironment(projectRoot))?.pythonPath;
}
function augmentPythonEnvironment(baseEnvironment, environment) {
if (!environment)
return baseEnvironment;
const inheritedPath = baseEnvironment.PATH ?? baseEnvironment.Path ?? baseEnvironment.path ?? "";
const augmentedPath = inheritedPath ? `${environment.binDir}${path.delimiter}${inheritedPath}` : environment.binDir;
const childEnvironment = {
...baseEnvironment,
PATH: augmentedPath,
VIRTUAL_ENV: environment.root
};
if (process.platform === "win32")
childEnvironment.Path = augmentedPath;
return childEnvironment;
}
function pythonEnvironmentToolCandidates(environment, command) {
if (!environment)
return [];
if (process.platform !== "win32") {
return [path.join(environment.binDir, command)];
}
return [
path.join(environment.binDir, `${command}.exe`),
path.join(environment.binDir, `${command}.cmd`),
path.join(environment.binDir, command)
];
}
export {
hasAmbientPythonEnvironment,
detectPythonEnvironment,
detectPythonVenv,
augmentPythonEnvironment,
pythonEnvironmentToolCandidates
};