-
-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ext/pip): pip install and import (#43)
- Loading branch information
1 parent
d90d96d
commit a82cafa
Showing
5 changed files
with
228 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
.vscode/ | ||
/*.bat | ||
deno.lock | ||
plug/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
import { pip } from "../ext/pip.ts"; | ||
|
||
const np = await pip.import("numpy"); | ||
const plt = await pip.import("matplotlib", "matplotlib.pyplot"); | ||
|
||
const xpoints = np.array([1, 8]); | ||
const ypoints = np.array([3, 10]); | ||
|
||
plt.plot(xpoints, ypoints); | ||
plt.show(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
import { kw, python, PythonError } from "../mod.ts"; | ||
|
||
import { join } from "https://deno.land/[email protected]/path/mod.ts"; | ||
import { ensureDir } from "https://deno.land/[email protected]/fs/mod.ts"; | ||
import { green, yellow } from "https://deno.land/[email protected]/fmt/colors.ts"; | ||
|
||
import type { CacheLocation } from "https://deno.land/x/[email protected]/types.ts"; | ||
import { ensureCacheLocation } from "https://deno.land/x/[email protected]/download.ts"; | ||
import { hash } from "https://deno.land/x/[email protected]/util.ts"; | ||
|
||
const sys = python.import("sys"); | ||
const runpy = python.import("runpy"); | ||
const importlib = python.import("importlib"); | ||
|
||
// https://packaging.python.org/en/latest/specifications/name-normalization/ | ||
const MODULE_REGEX = | ||
/^([a-z0-9]|[a-z0-9][a-z0-9._-]*[a-z0-9])([^a-z0-9._-].*)?$/i; | ||
|
||
function normalizeModuleName(name: string) { | ||
return name.replaceAll(/[-_.]+/g, "-").toLowerCase(); | ||
} | ||
|
||
function getModuleNameAndVersion(module: string): { | ||
name: string; | ||
version?: string; | ||
} { | ||
const match = module.match(MODULE_REGEX); | ||
const name = match?.[1]; | ||
const version = match?.[2]; | ||
|
||
if (name == null) { | ||
throw new TypeError("Could not match any valid pip module name"); | ||
} | ||
|
||
return { | ||
name: normalizeModuleName(name), | ||
version, | ||
}; | ||
} | ||
|
||
export class Pip { | ||
#cacheLocation: Promise<string>; | ||
|
||
constructor(location: CacheLocation) { | ||
this.#cacheLocation = Promise.all([ | ||
ensureCacheLocation(location), | ||
globalThis.location !== undefined | ||
? hash(globalThis.location.href) | ||
: Promise.resolve("pip"), | ||
]).then(async (parts) => { | ||
const cacheLocation = join(...parts); | ||
await ensureDir(cacheLocation); | ||
|
||
if (!(cacheLocation in sys.path)) { | ||
sys.path.insert(0, cacheLocation); | ||
} | ||
|
||
return cacheLocation; | ||
}); | ||
} | ||
|
||
/** | ||
* Install a Python module using the `pip` package manager. | ||
* | ||
* @param module The Python module which you wish to install | ||
* | ||
* @example | ||
* ```ts | ||
* import { python } from "https://deno.land/x/python/mod.ts"; | ||
* import { install } from "https://deno.land/x/python/ext/pip.ts"; | ||
* | ||
* await install("numpy"); | ||
* const numpy = python.import("numpy"); | ||
* | ||
* ``` | ||
*/ | ||
async install(module: string) { | ||
const argv = sys.argv; | ||
sys.argv = [ | ||
"pip", | ||
"install", | ||
"-q", | ||
"-t", | ||
await this.#cacheLocation, | ||
module, | ||
]; | ||
|
||
console.log(`${green("Installing")} ${module}`); | ||
|
||
try { | ||
runpy.run_module("pip", kw`run_name=${"__main__"}`); | ||
} catch (error) { | ||
if ( | ||
!( | ||
error instanceof PythonError && | ||
error.type.isInstance(python.builtins.SystemExit()) && | ||
error.value.asLong() === 0 | ||
) | ||
) { | ||
throw error; | ||
} | ||
} finally { | ||
sys.argv = argv; | ||
} | ||
} | ||
|
||
/** | ||
* Install and import a Python module using the `pip` package manager. | ||
* | ||
* @param module The Python module which you wish to install | ||
* | ||
* @example | ||
* ```ts | ||
* import { python } from "https://deno.land/x/python/mod.ts"; | ||
* import { pip } from "https://deno.land/x/python/ext/pip.ts"; | ||
* | ||
* const numpy = await pip.import("numpy==1.25.2"); | ||
* | ||
* ``` | ||
*/ | ||
async import(module: string, entrypoint?: string) { | ||
const { name } = getModuleNameAndVersion(module); | ||
|
||
await this.install(module); | ||
|
||
if (entrypoint) { | ||
return python.import(entrypoint); | ||
} | ||
|
||
const packages = importlib.metadata.packages_distributions(); | ||
const entrypoints = []; | ||
|
||
for (const entry of packages) { | ||
if (packages[entry].valueOf().includes(name)) { | ||
entrypoints.push(entry.valueOf()); | ||
} | ||
} | ||
|
||
if (entrypoints.length === 0) { | ||
throw new TypeError( | ||
`Failed to import module ${module}, could not find import name ${name}`, | ||
); | ||
} | ||
|
||
entrypoint = entrypoints[0]; | ||
|
||
if (entrypoints.length > 1) { | ||
if (entrypoints.includes(name)) { | ||
entrypoint = entrypoints[entrypoints.indexOf(name)]; | ||
} else { | ||
console.warn( | ||
`${ | ||
yellow( | ||
"Warning", | ||
) | ||
} could not determine a single entrypoint for module ${module}, please specify one of: ${ | ||
entrypoints.join( | ||
", ", | ||
) | ||
}. Importing ${entrypoint}`, | ||
); | ||
} | ||
} | ||
|
||
return python.import(entrypoint!); | ||
} | ||
} | ||
|
||
export const pip = new Pip(); | ||
export default pip; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
export * from "https://deno.land/std@0.178.0/testing/asserts.ts"; | ||
export * from "https://deno.land/std@0.198.0/testing/asserts.ts"; |