Skip to content

Commit 2552b6e

Browse files
authored
Merge pull request #11807 from PumasAI/jk/engines-config
Allow to reorder engine prioritization with `engines` key in _quarto.yml
2 parents 0070f6f + 18aefad commit 2552b6e

File tree

12 files changed

+122
-15
lines changed

12 files changed

+122
-15
lines changed

src/execute/engine.ts

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,11 @@ export function engineValidExtensions(): string[] {
9696
);
9797
}
9898

99-
export function markdownExecutionEngine(markdown: string, flags?: RenderFlags) {
99+
export function markdownExecutionEngine(
100+
markdown: string,
101+
reorderedEngines: Map<string, ExecutionEngine>,
102+
flags?: RenderFlags,
103+
) {
100104
// read yaml and see if the engine is declared in yaml
101105
// (note that if the file were a non text-file like ipynb
102106
// it would have already been claimed via extension)
@@ -106,7 +110,7 @@ export function markdownExecutionEngine(markdown: string, flags?: RenderFlags) {
106110
if (yaml) {
107111
// merge in command line fags
108112
yaml = mergeConfigs(yaml, flags?.metadata);
109-
for (const [_, engine] of kEngines) {
113+
for (const [_, engine] of reorderedEngines) {
110114
if (yaml[engine.name]) {
111115
return engine;
112116
}
@@ -123,7 +127,7 @@ export function markdownExecutionEngine(markdown: string, flags?: RenderFlags) {
123127

124128
// see if there is an engine that claims this language
125129
for (const language of languages) {
126-
for (const [_, engine] of kEngines) {
130+
for (const [_, engine] of reorderedEngines) {
127131
if (engine.claimsLanguage(language)) {
128132
return engine;
129133
}
@@ -143,6 +147,37 @@ export function markdownExecutionEngine(markdown: string, flags?: RenderFlags) {
143147
return markdownEngine;
144148
}
145149

150+
function reorderEngines(project: ProjectContext) {
151+
const userSpecifiedOrder: string[] =
152+
project.config?.engines as string[] | undefined ?? [];
153+
154+
for (const key of userSpecifiedOrder) {
155+
if (!kEngines.has(key)) {
156+
throw new Error(
157+
`'${key}' was specified in the list of engines in the project settings but it is not a valid engine. Available engines are ${
158+
Array.from(kEngines.keys()).join(", ")
159+
}`,
160+
);
161+
}
162+
}
163+
164+
const reorderedEngines = new Map<string, ExecutionEngine>();
165+
166+
// Add keys in the order of userSpecifiedOrder first
167+
for (const key of userSpecifiedOrder) {
168+
reorderedEngines.set(key, kEngines.get(key)!); // Non-null assertion since we verified the keys are in the map
169+
}
170+
171+
// Add the rest of the keys from the original map
172+
for (const [key, value] of kEngines) {
173+
if (!reorderedEngines.has(key)) {
174+
reorderedEngines.set(key, value);
175+
}
176+
}
177+
178+
return reorderedEngines;
179+
}
180+
146181
export async function fileExecutionEngine(
147182
file: string,
148183
flags: RenderFlags | undefined,
@@ -158,8 +193,10 @@ export async function fileExecutionEngine(
158193
return undefined;
159194
}
160195

196+
const reorderedEngines = reorderEngines(project);
197+
161198
// try to find an engine that claims this extension outright
162-
for (const [_, engine] of kEngines) {
199+
for (const [_, engine] of reorderedEngines) {
163200
if (engine.claimsFile(file, ext)) {
164201
return engine;
165202
}
@@ -175,6 +212,7 @@ export async function fileExecutionEngine(
175212
try {
176213
return markdownExecutionEngine(
177214
markdown ? markdown.value : Deno.readTextFileSync(file),
215+
reorderedEngines,
178216
flags,
179217
);
180218
} catch (error) {

src/execute/julia.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { join } from "../deno_ral/path.ts";
33
import { MappedString, mappedStringFromFile } from "../core/mapped-text.ts";
44
import { partitionMarkdown } from "../core/pandoc/pandoc-partition.ts";
55
import { readYamlFromMarkdown } from "../core/yaml.ts";
6+
import { asMappedString } from "../core/lib/mapped-text.ts";
67
import { ProjectContext } from "../project/types.ts";
78
import {
89
DependenciesOptions,
@@ -46,13 +47,21 @@ import {
4647
executeResultIncludes,
4748
} from "./jupyter/jupyter.ts";
4849
import { isWindows } from "../deno_ral/platform.ts";
50+
import {
51+
isJupyterPercentScript,
52+
markdownFromJupyterPercentScript,
53+
} from "./jupyter/percent.ts";
4954

5055
export interface JuliaExecuteOptions extends ExecuteOptions {
5156
julia_cmd: string;
5257
oneShot: boolean; // if true, the file's worker process is closed before and after running
5358
supervisor_pid?: number;
5459
}
5560

61+
function isJuliaPercentScript(file: string) {
62+
return isJupyterPercentScript(file, [".jl"]);
63+
}
64+
5665
export const juliaEngine: ExecutionEngine = {
5766
name: kJuliaEngine,
5867

@@ -68,12 +77,12 @@ export const juliaEngine: ExecutionEngine = {
6877

6978
validExtensions: () => [],
7079

71-
claimsFile: (file: string, ext: string) => false,
80+
claimsFile: (file: string, _ext: string) => {
81+
return isJuliaPercentScript(file);
82+
},
7283

7384
claimsLanguage: (language: string) => {
74-
// we don't claim `julia` so the old behavior of using the jupyter
75-
// backend by default stays intact
76-
return false; // language.toLowerCase() === "julia";
85+
return language.toLowerCase() === "julia";
7786
},
7887

7988
partitionedMarkdown: async (file: string) => {
@@ -109,7 +118,13 @@ export const juliaEngine: ExecutionEngine = {
109118
},
110119

111120
markdownForFile(file: string): Promise<MappedString> {
112-
return Promise.resolve(mappedStringFromFile(file));
121+
if (isJuliaPercentScript(file)) {
122+
return Promise.resolve(
123+
asMappedString(markdownFromJupyterPercentScript(file)),
124+
);
125+
} else {
126+
return Promise.resolve(mappedStringFromFile(file));
127+
}
113128
},
114129

115130
execute: async (options: ExecuteOptions): Promise<ExecuteResult> => {

src/execute/jupyter/jupyter.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,8 +150,10 @@ export const jupyterEngine: ExecutionEngine = {
150150
isJupyterPercentScript(file);
151151
},
152152

153-
claimsLanguage: (_language: string) => {
154-
return false;
153+
claimsLanguage: (language: string) => {
154+
// jupyter has to claim julia so that julia may also claim it without changing the old behavior
155+
// of preferring jupyter over julia engine by default
156+
return language.toLowerCase() === "julia";
155157
},
156158

157159
markdownForFile(file: string): Promise<MappedString> {

src/execute/jupyter/percent.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ export const kJupyterPercentScriptExtensions = [
2020
".r",
2121
];
2222

23-
export function isJupyterPercentScript(file: string) {
23+
export function isJupyterPercentScript(file: string, extensions?: string[]) {
2424
const ext = extname(file).toLowerCase();
25-
if (kJupyterPercentScriptExtensions.includes(ext)) {
25+
const availableExtensions = extensions ?? kJupyterPercentScriptExtensions;
26+
if (availableExtensions.includes(ext)) {
2627
const text = Deno.readTextFileSync(file);
2728
return !!text.match(/^\s*#\s*%%+\s+\[markdown|raw\]/);
2829
} else {
@@ -77,10 +78,10 @@ export function markdownFromJupyterPercentScript(file: string) {
7778
let rawContent = cellContent(cellLines);
7879
const format = cell.header?.metadata?.["format"];
7980
const mimeType = cell.header.metadata?.[kCellRawMimeType];
80-
if (typeof (mimeType) === "string") {
81+
if (typeof mimeType === "string") {
8182
const rawBlock = mdRawOutput(mimeType, lines(rawContent));
8283
rawContent = rawBlock || rawContent;
83-
} else if (typeof (format) === "string") {
84+
} else if (typeof format === "string") {
8485
rawContent = mdFormatOutput(format, lines(rawContent));
8586
}
8687
markdown += rawContent;

src/resources/schema/project.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,8 @@
8484
#
8585
# In general, full json schema would allow negative assertions,
8686
# but that makes our error localization heuristics worse. So we hack.
87+
88+
- name: engines
89+
schema:
90+
arrayOf: string
91+
description: "List execution engines you want to give priority when determining which engine should render a notebook. If two engines have support for a notebook, the one listed earlier will be chosen. Quarto's default order is 'knitr', 'jupyter', 'markdown', 'julia'."
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
engines: ["invalid-engine"]

tests/docs/engine/invalid-project/notebook.qmd

Whitespace-only changes.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
project:
2+
type: default
3+
engines: ["julia"]
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
```{julia}
2+
using Test
3+
@test haskey(
4+
Base.loaded_modules,
5+
Base.PkgId(
6+
Base.UUID("38328d9c-a911-4051-bc06-3f7f556ffeda"),
7+
"QuartoNotebookWorker",
8+
)
9+
)
10+
```
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
project:
2+
type: default
3+
engines: ["jupyter"]

0 commit comments

Comments
 (0)