Skip to content

Commit 5b7ac03

Browse files
committed
Merge branch 'main' of github.com:quarto-dev/quarto-cli
2 parents 9bec89c + 0ae67eb commit 5b7ac03

File tree

12 files changed

+220
-112
lines changed

12 files changed

+220
-112
lines changed

news/changelog-1.1.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Don't run `ipynb-filters` for qmd source files (only run them for ipynb source files)
88
- More gracefully handle cell outputs with no data (don't print warning, just ignore)
99
- Handle non-textual data from jupyter's plain text output more robustly (#1874)
10+
- Use IJulia's built-in conda environment / jupyter install for julia notebooks/qmds
1011

1112
## Knitr
1213

@@ -106,6 +107,7 @@
106107
- Extension YAML files `_extension.yml` are now validated at render time. (#1268)
107108
- Support boolean values in Shortcode `meta` access
108109
- Make `quarto.base64` module available to extensions
110+
- Support installing extensions from any GitHub tag or branch (#1836)
109111

110112
## Publishing
111113

src/command/check/check.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ async function checkJupyterInstallation(services: RenderServices) {
119119
info("");
120120
}
121121
} else {
122-
info(await jupyterInstallationMessage(caps, kIndent));
122+
info(jupyterInstallationMessage(caps, kIndent));
123123
info("");
124124
const envMessage = jupyterUnactivatedEnvMessage(caps, kIndent);
125125
if (envMessage) {

src/command/use/commands/template.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,14 @@ async function useTemplate(
5656
tempContext: TempContext,
5757
) {
5858
// Resolve extension host and trust
59-
const source = extensionSource(target);
59+
const source = await extensionSource(target);
60+
// Is this source valid?
61+
if (!source) {
62+
info(
63+
`Extension not found in local or remote sources`,
64+
);
65+
return;
66+
}
6067
const trusted = await isTrusted(source, options.prompt !== false);
6168
if (trusted) {
6269
// Resolve target directory
@@ -125,7 +132,9 @@ async function stageTemplate(
125132
ensureDirSync(archiveDir);
126133

127134
// The filename
128-
const filename = source.resolvedTarget.split("/").pop() || "extension.zip";
135+
const filename = (typeof (source.resolvedTarget) === "string"
136+
? source.resolvedTarget
137+
: source.resolvedTarget.url).split("/").pop() || "extension.zip";
129138

130139
// The tarball path
131140
const toFile = join(archiveDir, filename);
@@ -142,6 +151,12 @@ async function stageTemplate(
142151
return archiveDir;
143152
}
144153
} else {
154+
if (typeof source.resolvedTarget !== "string") {
155+
throw new Error(
156+
"Internal error: local resolved extension should always have a string target.",
157+
);
158+
}
159+
145160
if (Deno.statSync(source.resolvedTarget).isDirectory) {
146161
// copy the contents of the directory, filtered by quartoignore
147162
return source.resolvedTarget;

src/core/download.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,19 @@ export interface DownloadError extends Error {
1414
}
1515

1616
export async function downloadWithProgress(
17-
url: string,
17+
url: string | Response,
1818
msg: string,
1919
toFile: string,
2020
) {
2121
// Fetch the data
22-
const response = await fetch(
23-
url,
24-
{
25-
redirect: "follow",
26-
},
27-
);
22+
const response = await (typeof url === "string"
23+
? fetch(
24+
url,
25+
{
26+
redirect: "follow",
27+
},
28+
)
29+
: url);
2830

2931
// Write the data to a file
3032
if (response.status === 200 && response.body) {

src/core/jupyter/capabilities.ts

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -14,42 +14,82 @@ import { execProcess } from "../process.ts";
1414
import { resourcePath } from "../resources.ts";
1515
import { readYamlFromString } from "../yaml.ts";
1616

17-
import { JupyterCapabilities } from "./types.ts";
17+
import { JupyterCapabilities, JupyterKernelspec } from "./types.ts";
1818

19-
// cache capabiliites per-process
20-
let cachedJupyterCaps: JupyterCapabilities | undefined;
19+
// cache capabilities per language
20+
const kNoLanguage = "(none)";
21+
const jupyterCapsCache = new Map<string, JupyterCapabilities>();
22+
23+
export async function jupyterCapabilities(kernelspec?: JupyterKernelspec) {
24+
const language = kernelspec?.language || kNoLanguage;
25+
26+
if (!jupyterCapsCache.has(language)) {
27+
28+
// if we are targeting julia then prefer the julia installed miniconda
29+
const juliaCaps = await getVerifiedJuliaCondaJupyterCapabilities();
30+
if (language === "julia" && juliaCaps) {
31+
jupyterCapsCache.set(language, juliaCaps);
32+
return juliaCaps;
33+
}
2134

22-
export async function jupyterCapabilities() {
23-
if (!cachedJupyterCaps) {
2435
// if there is an explicit python requested then use it
25-
cachedJupyterCaps = await getQuartoJupyterCapabilities();
26-
if (cachedJupyterCaps) {
27-
return cachedJupyterCaps;
36+
const quartoCaps = await getQuartoJupyterCapabilities();
37+
if (quartoCaps) {
38+
jupyterCapsCache.set(language, quartoCaps);
39+
return quartoCaps;
2840
}
2941

3042
// if we are on windows and have PY_PYTHON defined then use the launcher
3143
if (isWindows() && pyPython()) {
32-
cachedJupyterCaps = await getPyLauncherJupyterCapabilities();
44+
const pyLauncherCaps = await getPyLauncherJupyterCapabilities();
45+
if (pyLauncherCaps) {
46+
jupyterCapsCache.set(language, pyLauncherCaps);
47+
}
3348
}
3449

3550
// default handling (also a fallthrough if launcher didn't work out)
36-
if (!cachedJupyterCaps) {
51+
if (!jupyterCapsCache.has(language)) {
3752
// look for python from conda (conda doesn't provide python3 on windows or mac)
38-
cachedJupyterCaps = await getJupyterCapabilities(["python"]);
39-
40-
// if it's not conda then probe explicitly for python 3
41-
if (!cachedJupyterCaps?.conda) {
53+
const condaCaps = await getJupyterCapabilities(["python"]);
54+
if (condaCaps?.conda) {
55+
jupyterCapsCache.set(language, condaCaps);
56+
} else {
4257
const caps = isWindows()
4358
? await getPyLauncherJupyterCapabilities()
4459
: await getJupyterCapabilities(["python3"]);
4560
if (caps) {
46-
cachedJupyterCaps = caps;
61+
jupyterCapsCache.set(language, caps);
4762
}
4863
}
64+
65+
// if the version we discovered doesn't have jupyter and we have a julia provided
66+
// jupyter then go ahead and use that
67+
if (!jupyterCapsCache.get(language)?.jupyter_core && juliaCaps) {
68+
jupyterCapsCache.set(language, juliaCaps);
69+
}
4970
}
5071
}
5172

52-
return cachedJupyterCaps;
73+
return jupyterCapsCache.get(language);
74+
}
75+
76+
async function getVerifiedJuliaCondaJupyterCapabilities() {
77+
const home = isWindows() ? Deno.env.get("USERPROFILE") : Deno.env.get("HOME");
78+
if (home) {
79+
const juliaPython = join(
80+
home,
81+
".julia",
82+
"conda",
83+
"3",
84+
isWindows() ? "python.exe" : join("bin", "python3")
85+
);
86+
if (existsSync(juliaPython)) {
87+
const caps = await getJupyterCapabilities([juliaPython]);
88+
if (caps?.jupyter_core) {
89+
return caps;
90+
}
91+
}
92+
}
5393
}
5494

5595
function getQuartoJupyterCapabilities() {

src/core/jupyter/exec.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,20 @@
77

88
import { isWindows } from "../platform.ts";
99
import { jupyterCapabilities } from "./capabilities.ts";
10+
import { JupyterCapabilities, JupyterKernelspec } from "./types.ts";
1011

11-
export async function pythonExec(binaryOnly = false): Promise<string[]> {
12-
const caps = await jupyterCapabilities();
12+
export async function pythonExec(
13+
kernelspec?: JupyterKernelspec,
14+
binaryOnly = false,
15+
): Promise<string[]> {
16+
const caps = await jupyterCapabilities(kernelspec);
17+
return pythonExecForCaps(caps, binaryOnly);
18+
}
19+
20+
export function pythonExecForCaps(
21+
caps?: JupyterCapabilities,
22+
binaryOnly = false,
23+
) {
1324
if (caps?.pyLauncher) {
1425
return ["py"];
1526
} else if (isWindows()) {
@@ -19,9 +30,11 @@ export async function pythonExec(binaryOnly = false): Promise<string[]> {
1930
}
2031
}
2132

22-
export async function jupyterExec(): Promise<string[]> {
33+
export async function jupyterExec(
34+
kernelspec?: JupyterKernelspec,
35+
): Promise<string[]> {
2336
return [
24-
...(await pythonExec()),
37+
...(await pythonExec(kernelspec)),
2538
"-m",
2639
"jupyter",
2740
];

src/core/jupyter/jupyter-shared.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as colors from "fmt/colors.ts";
1111

1212
import { pathWithForwardSlashes } from "../path.ts";
1313

14-
import { pythonExec } from "./exec.ts";
14+
import { pythonExecForCaps } from "./exec.ts";
1515
import { jupyterKernelspecs } from "./kernels.ts";
1616
import { JupyterCapabilities, JupyterKernelspec } from "./types.ts";
1717
import { isEnvDir } from "./capabilities.ts";
@@ -35,7 +35,7 @@ export async function jupyterCapabilitiesMessage(
3535
return lines.map((line: string) => `${indent}${line}`).join("\n");
3636
}
3737

38-
export async function jupyterInstallationMessage(
38+
export function jupyterInstallationMessage(
3939
caps: JupyterCapabilities,
4040
indent = "",
4141
) {
@@ -44,7 +44,9 @@ export async function jupyterInstallationMessage(
4444
"Install with " +
4545
colors.bold(
4646
`${
47-
caps.conda ? "conda" : (await pythonExec(true)).join(" ") + " -m pip"
47+
caps.conda
48+
? "conda"
49+
: (pythonExecForCaps(caps, true)).join(" ") + " -m pip"
4850
} install jupyter`,
4951
),
5052
];
@@ -81,9 +83,8 @@ export function jupyterUnactivatedEnvMessage(
8183
if (existsSync(join(Deno.cwd(), envFile))) {
8284
return indent + "There is a " + colors.bold(envFile) +
8385
" file in this directory. " +
84-
"Is this for a " + (envFile === kRequirementsTxt
85-
? "venv"
86-
: "conda env") +
86+
"Is this for a " +
87+
(envFile === kRequirementsTxt ? "venv" : "conda env") +
8788
" that you need to restore?";
8889
}
8990
}

src/core/jupyter/jupyter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ export async function quartoMdToJupyter(
427427
export async function jupyterKernelspecFromMarkdown(
428428
markdown: string,
429429
): Promise<[JupyterKernelspec, Metadata]> {
430-
const yaml = await readYamlFromMarkdown(markdown);
430+
const yaml = readYamlFromMarkdown(markdown);
431431
const yamlJupyter = yaml.jupyter;
432432

433433
// if there is no yaml.jupyter then detect the file's language(s) and

src/execute/jupyter/jupyter-kernel.ts

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,11 @@ export async function executeKernelOneshot(
5353

5454
trace(options, "Executing notebook with oneshot kernel");
5555
const debug = !!options.format.execute[kExecuteDebug];
56-
const result = await execJupyter("execute", { ...options, debug });
56+
const result = await execJupyter(
57+
"execute",
58+
{ ...options, debug },
59+
options.kernelspec,
60+
);
5761

5862
if (!result.success) {
5963
return Promise.reject();
@@ -121,7 +125,7 @@ export async function executeKernelKeepalive(
121125
if (msg.type === "error") {
122126
trace(options, "Error response received");
123127
error(msg.data, { colorize: false });
124-
printExecDiagnostics(msg.data);
128+
printExecDiagnostics(options.kernelspec, msg.data);
125129
return Promise.reject();
126130
} else if (msg.type == "restart") {
127131
trace(options, "Restart request received");
@@ -180,12 +184,13 @@ async function abortKernel(options: JupyterExecuteOptions) {
180184
async function execJupyter(
181185
command: string,
182186
options: Record<string, unknown>,
187+
kernelspec: JupyterKernelspec,
183188
): Promise<ProcessResult> {
184189
try {
185190
const result = await execProcess(
186191
{
187192
cmd: [
188-
...(await pythonExec()),
193+
...(await pythonExec(kernelspec)),
189194
resourcePath("jupyter/jupyter.py"),
190195
],
191196
env: {
@@ -204,26 +209,29 @@ async function execJupyter(
204209
);
205210
if (!result.success) {
206211
// forward error (print some diagnostics if python and/or jupyter couldn't be found)
207-
await printExecDiagnostics(result.stderr);
212+
await printExecDiagnostics(kernelspec, result.stderr);
208213
}
209214
return result;
210215
} catch (e) {
211216
if (e?.message) {
212217
info("");
213218
error(e.message);
214219
}
215-
await printExecDiagnostics();
220+
await printExecDiagnostics(kernelspec);
216221
return Promise.reject();
217222
}
218223
}
219224

220-
export async function printExecDiagnostics(stderr?: string) {
221-
const caps = await jupyterCapabilities();
225+
export async function printExecDiagnostics(
226+
kernelspec: JupyterKernelspec,
227+
stderr?: string,
228+
) {
229+
const caps = await jupyterCapabilities(kernelspec);
222230
if (caps && !caps.jupyter_core) {
223231
info("Python 3 installation:");
224232
info(await jupyterCapabilitiesMessage(caps, " "));
225233
info("");
226-
info(await jupyterInstallationMessage(caps));
234+
info(jupyterInstallationMessage(caps));
227235
info("");
228236
maybePrintUnactivatedEnvMessage(caps);
229237
} else if (caps && !haveRequiredPython(caps)) {
@@ -396,7 +404,7 @@ async function connectToKernel(
396404
timeout,
397405
type,
398406
debug,
399-
});
407+
}, options.kernelspec);
400408
if (!result.success) {
401409
return Promise.reject();
402410
}

src/execute/jupyter/jupyter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ export const jupyterEngine: ExecutionEngine = {
261261
}
262262
const jupyterExecOptions: JupyterExecuteOptions = {
263263
kernelspec,
264-
python_cmd: await pythonExec(),
264+
python_cmd: await pythonExec(kernelspec),
265265
...execOptions,
266266
};
267267
if (executeDaemon === false || executeDaemon === 0) {

0 commit comments

Comments
 (0)