Skip to content

Commit

Permalink
Add PythonEnvInfo-related helpers. (microsoft#14051)
Browse files Browse the repository at this point in the history
This PR adds some basic helpers that we use in a subsequent PR. The following small drive-by changes are also included:

* drop PythonEnvInfo.id property
* make some internal helpers public
  • Loading branch information
ericsnowcurrently authored and Kartik Raj committed Sep 29, 2020
1 parent bb2ed7a commit dc5ef1a
Show file tree
Hide file tree
Showing 16 changed files with 1,204 additions and 470 deletions.
47 changes: 47 additions & 0 deletions src/client/common/utils/misc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,53 @@ export function isUri(resource?: Uri | any): resource is Uri {
return typeof uri.path === 'string' && typeof uri.scheme === 'string';
}

/**
* Create a filter func that determine if the given URI and candidate match.
*
* The scheme must match, as well as path.
*
* @param checkParent - if `true`, match if the candidate is rooted under `uri`
* @param checkChild - if `true`, match if `uri` is rooted under the candidate
* @param checkExact - if `true`, match if the candidate matches `uri` exactly
*/
export function getURIFilter(
uri: Uri,
opts: {
checkParent?: boolean;
checkChild?: boolean;
checkExact?: boolean;
} = { checkExact: true }
): (u: Uri) => boolean {
let uriPath = uri.path;
while (uri.path.endsWith('/')) {
uriPath = uriPath.slice(0, -1);
}
const uriRoot = `${uriPath}/`;
function filter(candidate: Uri): boolean {
if (candidate.scheme !== uri.scheme) {
return false;
}
let candidatePath = candidate.path;
while (candidate.path.endsWith('/')) {
candidatePath = candidatePath.slice(0, -1);
}
if (opts.checkExact && candidatePath === uriPath) {
return true;
}
if (opts.checkParent && candidatePath.startsWith(uriRoot)) {
return true;
}
if (opts.checkChild) {
const candidateRoot = `{candidatePath}/`;
if (uriPath.startsWith(candidateRoot)) {
return true;
}
}
return false;
}
return filter;
}

export function isNotebookCell(documentOrUri: TextDocument | Uri): boolean {
const uri = isUri(documentOrUri) ? documentOrUri : documentOrUri.uri;
return uri.scheme.includes(NotebookCellScheme);
Expand Down
215 changes: 138 additions & 77 deletions src/client/pythonEnvironments/base/info/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,124 @@
import { cloneDeep } from 'lodash';
import * as path from 'path';
import {
FileInfo,
PythonDistroInfo,
PythonEnvInfo, PythonEnvKind, PythonVersion,
FileInfo, PythonDistroInfo, PythonEnvInfo, PythonEnvKind, PythonReleaseLevel, PythonVersion
} from '.';
import { Architecture } from '../../../common/utils/platform';
import { arePathsSame } from '../../common/externalDependencies';
import { areEqualVersions, areEquivalentVersions } from './pythonVersion';

/**
* Create a new info object with all values empty.
*
* @param init - if provided, these values are applied to the new object
*/
export function buildEnvInfo(init?: {
kind?: PythonEnvKind;
executable?: string;
location?: string;
version?: PythonVersion;
}): PythonEnvInfo {
const env = {
kind: PythonEnvKind.Unknown,
executable: {
filename: '',
sysPrefix: '',
ctime: -1,
mtime: -1,
},
name: '',
location: '',
searchLocation: undefined,
defaultDisplayName: undefined,
version: {
major: -1,
minor: -1,
micro: -1,
release: {
level: PythonReleaseLevel.Final,
serial: 0,
},
},
arch: Architecture.Unknown,
distro: {
org: '',
},
};
if (init !== undefined) {
updateEnv(env, init);
}
return env;
}

/**
* Return a deep copy of the given env info.
*
* @param updates - if provided, these values are applied to the copy
*/
export function copyEnvInfo(
env: PythonEnvInfo,
updates?: {
kind?: PythonEnvKind;
},
): PythonEnvInfo {
// We don't care whether or not extra/hidden properties
// get preserved, so we do the easy thing here.
const copied = cloneDeep(env);
if (updates !== undefined) {
updateEnv(copied, updates);
}
return copied;
}

function updateEnv(
env: PythonEnvInfo,
updates: {
kind?: PythonEnvKind;
executable?: string;
location?: string;
version?: PythonVersion;
},
): void {
if (updates.kind !== undefined) {
env.kind = updates.kind;
}
if (updates.executable !== undefined) {
env.executable.filename = updates.executable;
}
if (updates.location !== undefined) {
env.location = updates.location;
}
if (updates.version !== undefined) {
env.version = updates.version;
}
}

/**
* For the given data, build a normalized partial info object.
*
* If insufficient data is provided to generate a minimal object, such
* that it is not identifiable, then `undefined` is returned.
*/
export function getMinimalPartialInfo(env: string | Partial<PythonEnvInfo>): Partial<PythonEnvInfo> | undefined {
if (typeof env === 'string') {
if (env === '') {
return undefined;
}
return {
executable: {
filename: env, sysPrefix: '', ctime: -1, mtime: -1,
},
};
}
if (env.executable === undefined) {
return undefined;
}
if (env.executable.filename === '') {
return undefined;
}
return env;
}

/**
* Checks if two environments are same.
* @param {string | PythonEnvInfo} left: environment to compare.
Expand All @@ -24,14 +134,20 @@ import { areEqualVersions, areEquivalentVersions } from './pythonVersion';
* to be same environment. This later case is needed for comparing windows store python,
* where multiple versions of python executables are all put in the same directory.
*/
export function areSameEnvironment(
left: string | PythonEnvInfo,
right: string | PythonEnvInfo,
allowPartialMatch?: boolean,
): boolean {
const leftFilename = typeof left === 'string' ? left : left.executable.filename;
const rightFilename = typeof right === 'string' ? right : right.executable.filename;
export function areSameEnv(
left: string | Partial<PythonEnvInfo>,
right: string | Partial<PythonEnvInfo>,
allowPartialMatch = true,
): boolean | undefined {
const leftInfo = getMinimalPartialInfo(left);
const rightInfo = getMinimalPartialInfo(right);
if (leftInfo === undefined || rightInfo === undefined) {
return undefined;
}
const leftFilename = leftInfo.executable!.filename;
const rightFilename = rightInfo.executable!.filename;

// For now we assume that matching executable means they are the same.
if (arePathsSame(leftFilename, rightFilename)) {
return true;
}
Expand All @@ -58,7 +174,7 @@ export function areSameEnvironment(
* weighted by most important to least important fields.
* Wn > Wn-1 + Wn-2 + ... W0
*/
function getPythonVersionInfoHeuristic(version:PythonVersion): number {
function getPythonVersionInfoHeuristic(version: PythonVersion): number {
let infoLevel = 0;
if (version.major > 0) {
infoLevel += 20; // W4
Expand All @@ -72,11 +188,11 @@ function getPythonVersionInfoHeuristic(version:PythonVersion): number {
infoLevel += 5; // W2
}

if (version.release.level) {
if (version.release?.level) {
infoLevel += 3; // W1
}

if (version.release.serial || version.sysVersion) {
if (version.release?.serial || version.sysVersion) {
infoLevel += 1; // W0
}

Expand All @@ -90,7 +206,7 @@ function getPythonVersionInfoHeuristic(version:PythonVersion): number {
* weighted by most important to least important fields.
* Wn > Wn-1 + Wn-2 + ... W0
*/
function getFileInfoHeuristic(file:FileInfo): number {
function getFileInfoHeuristic(file: FileInfo): number {
let infoLevel = 0;
if (file.filename.length > 0) {
infoLevel += 5; // W2
Expand All @@ -114,7 +230,7 @@ function getFileInfoHeuristic(file:FileInfo): number {
* weighted by most important to least important fields.
* Wn > Wn-1 + Wn-2 + ... W0
*/
function getDistroInfoHeuristic(distro:PythonDistroInfo):number {
function getDistroInfoHeuristic(distro: PythonDistroInfo): number {
let infoLevel = 0;
if (distro.org.length > 0) {
infoLevel += 20; // W3
Expand All @@ -135,62 +251,6 @@ function getDistroInfoHeuristic(distro:PythonDistroInfo):number {
return infoLevel;
}

/**
* Gets a prioritized list of environment types for identification.
* @returns {PythonEnvKind[]} : List of environments ordered by identification priority
*
* Remarks: This is the order of detection based on how the various distributions and tools
* configure the environment, and the fall back for identification.
* Top level we have the following environment types, since they leave a unique signature
* in the environment or * use a unique path for the environments they create.
* 1. Conda
* 2. Windows Store
* 3. PipEnv
* 4. Pyenv
* 5. Poetry
*
* Next level we have the following virtual environment tools. The are here because they
* are consumed by the tools above, and can also be used independently.
* 1. venv
* 2. virtualenvwrapper
* 3. virtualenv
*
* Last category is globally installed python, or system python.
*/
export function getPrioritizedEnvironmentKind(): PythonEnvKind[] {
return [
PythonEnvKind.CondaBase,
PythonEnvKind.Conda,
PythonEnvKind.WindowsStore,
PythonEnvKind.Pipenv,
PythonEnvKind.Pyenv,
PythonEnvKind.Poetry,
PythonEnvKind.Venv,
PythonEnvKind.VirtualEnvWrapper,
PythonEnvKind.VirtualEnv,
PythonEnvKind.OtherVirtual,
PythonEnvKind.OtherGlobal,
PythonEnvKind.MacDefault,
PythonEnvKind.System,
PythonEnvKind.Custom,
PythonEnvKind.Unknown,
];
}

/**
* Selects an environment based on the environment selection priority. This should
* match the priority in the environment identifier.
*/
export function sortEnvInfoByPriority(...envs: PythonEnvInfo[]): PythonEnvInfo[] {
// tslint:disable-next-line: no-suspicious-comment
// TODO: When we consolidate the PythonEnvKind and EnvironmentType we should have
// one location where we define priority and
const envKindByPriority:PythonEnvKind[] = getPrioritizedEnvironmentKind();
return envs.sort(
(a:PythonEnvInfo, b:PythonEnvInfo) => envKindByPriority.indexOf(a.kind) - envKindByPriority.indexOf(b.kind),
);
}

/**
* Merges properties of the `target` environment and `other` environment and returns the merged environment.
* if the value in the `target` environment is not defined or has less information. This does not mutate
Expand All @@ -203,18 +263,19 @@ export function mergeEnvironments(target: PythonEnvInfo, other: PythonEnvInfo):

const version = cloneDeep(
getPythonVersionInfoHeuristic(target.version) > getPythonVersionInfoHeuristic(other.version)
? target.version : other.version,
? target.version
: other.version,
);

const executable = cloneDeep(
getFileInfoHeuristic(target.executable) > getFileInfoHeuristic(other.executable)
? target.executable : other.executable,
? target.executable
: other.executable,
);
executable.sysPrefix = target.executable.sysPrefix ?? other.executable.sysPrefix;

const distro = cloneDeep(
getDistroInfoHeuristic(target.distro) > getDistroInfoHeuristic(other.distro)
? target.distro : other.distro,
getDistroInfoHeuristic(target.distro) > getDistroInfoHeuristic(other.distro) ? target.distro : other.distro,
);

merged.arch = merged.arch === Architecture.Unknown ? other.arch : target.arch;
Expand All @@ -226,8 +287,8 @@ export function mergeEnvironments(target: PythonEnvInfo, other: PythonEnvInfo):
// preferred env based on kind.
merged.kind = target.kind;

merged.location = merged.location ?? other.location;
merged.name = merged.name ?? other.name;
merged.location = merged.location.length ? merged.location : other.location;
merged.name = merged.name.length ? merged.name : other.name;
merged.searchLocation = merged.searchLocation ?? other.searchLocation;
merged.version = version;

Expand Down
13 changes: 6 additions & 7 deletions src/client/pythonEnvironments/base/info/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export enum PythonEnvKind {
OtherVirtual = 'virt-other'
}

/**
* A (system-global) unique ID for a single Python environment.
*/
export type PythonEnvID = string;

/**
* Information about a file.
*/
Expand All @@ -44,11 +49,6 @@ export type PythonExecutableInfo = FileInfo & {
sysPrefix: string;
};

/**
* A (system-global) unique ID for a single Python environment.
*/
export type PythonEnvID = string;

/**
* The most fundamental information about a Python environment.
*
Expand All @@ -63,7 +63,6 @@ export type PythonEnvID = string;
* @prop location - the env's location (on disk), if relevant
*/
export type PythonEnvBaseInfo = {
id: PythonEnvID;
kind: PythonEnvKind;
executable: PythonExecutableInfo;
// One of (name, location) must be non-empty.
Expand Down Expand Up @@ -99,7 +98,7 @@ export type PythonVersionRelease = {
* @prop sysVersion - the raw text from `sys.version`
*/
export type PythonVersion = BasicVersionInfo & {
release: PythonVersionRelease;
release?: PythonVersionRelease;
sysVersion?: string;
};

Expand Down
Loading

0 comments on commit dc5ef1a

Please sign in to comment.