Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to render Python with PythonCall #180

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/QuartoNotebookWorker/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
PlotlyBase = "a03496cd-edff-5a9b-9e67-9cda94a718b5"
PlotlyJS = "f0f68f2c-4968-5e81-91da-67840de0976a"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d"
RCall = "6f49c342-dc21-5d91-9882-a32aef131414"
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"
SymPyCore = "458b697b-88f0-4a86-b56b-78b75cfb3531"
Expand All @@ -35,6 +36,7 @@ QuartoNotebookWorkerMakieExt = "Makie"
QuartoNotebookWorkerPlotlyBaseExt = "PlotlyBase"
QuartoNotebookWorkerPlotlyJSExt = "PlotlyJS"
QuartoNotebookWorkerPlotsExt = "Plots"
QuartoNotebookWorkerPythonCallExt = "PythonCall"
QuartoNotebookWorkerRCallExt = "RCall"
QuartoNotebookWorkerReviseExt = "Revise"
QuartoNotebookWorkerSymPyCoreExt = "SymPyCore"
Expand Down
17 changes: 17 additions & 0 deletions src/QuartoNotebookWorker/ext/QuartoNotebookWorkerPythonCallExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module QuartoNotebookWorkerPythonCallExt

import QuartoNotebookWorker
import PythonCall

function __init__()
if ccall(:jl_generating_output, Cint, ()) == 0
#RCall_temp_files_ref[] = mktempdir()
#configure()
#QuartoNotebookWorker.add_package_loading_hook!(configure)
#QuartoNotebookWorker.add_package_refresh_hook!(refresh)
#QuartoNotebookWorker.add_post_eval_hook!(display_plots)
#QuartoNotebookWorker.add_post_error_hook!(cleanup_temp_files)
end
end

end
45 changes: 42 additions & 3 deletions src/server.jl
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ function raw_markdown_chunks_from_string(path::String, markdown::String)
terminal_line = 1
code_cells = false
for (node, enter) in ast
if enter && (is_julia_toplevel(node) || is_r_toplevel(node))
if enter && (is_julia_toplevel(node) || is_python_toplevel(node) || is_r_toplevel(node))
code_cells = true
line = node.sourcepos[1][1]
md = join(source_lines[terminal_line:(line-1)], "\n")
Expand All @@ -341,6 +341,7 @@ function raw_markdown_chunks_from_string(path::String, markdown::String)
end
language =
is_julia_toplevel(node) ? :julia :
is_python_toplevel(node) ? :python :
is_r_toplevel(node) ? :r : error("Unhandled code block language")
push!(
raw_chunks,
Expand Down Expand Up @@ -775,6 +776,28 @@ function evaluate_raw_cells!(
# We also need to hide the real code cell in this case, which contains possible formatting
# settings in its YAML front-matter and which can therefore not be omitted entirely.
cell_options["echo"] = false
elseif chunk.language === :python
# Same reasoning as :r
push!(
cells,
(;
id = string(
expand_cell ? string(nth, "_", mth) : string(nth),
"_code_prefix",
),
cell_type = :markdown,
metadata = (;),
source = process_cell_source(
"""
```python
$(strip_cell_options(chunk.source))
```
""",
Dict(),
),
),
)
cell_options["echo"] = false
end

source = expand_cell ? remote.code : chunk.source
Expand All @@ -793,16 +816,18 @@ function evaluate_raw_cells!(
end
end
elseif chunk.type === :markdown
marker = r"{(?:julia|r)} "
marker = r"{(?:julia|r|python)} "
source = chunk.source
if contains(chunk.source, r"`{(?:julia|r)} ")
if contains(chunk.source, r"`{(?:julia|r|python)} ")
parser = Parser()
for (node, enter) in parser(chunk.source)
if enter && node.t isa CommonMark.Code
if startswith(node.literal, marker)
source_code = replace(node.literal, marker => "")
if startswith(node.literal, "{r}")
source_code = wrap_with_r_boilerplate(source_code)
elseif startswith(node.literal, "{python}")
source_code = wrap_with_python_boilerplate(source_code)
end
expr = :(render(
$(source_code),
Expand Down Expand Up @@ -907,6 +932,13 @@ function strip_cell_options(source::AbstractString)
join(lines[keep_from:end])
end

function wrap_with_python_boilerplate(code)
"""
@isdefined(PythonCall) && PythonCall isa Module && Base.PkgId(PythonCall).uuid == Base.UUID("6099a3de-0909-46bc-b1f4-468b9a2dfc0d") || error("PythonCall must be imported to execute Python code cells with QuartoNotebookRunner")
@py $code
"""
end

function wrap_with_r_boilerplate(code)
"""
@isdefined(RCall) && RCall isa Module && Base.PkgId(RCall).uuid == Base.UUID("6f49c342-dc21-5d91-9882-a32aef131414") || error("RCall must be imported to execute R code cells with QuartoNotebookRunner")
Expand All @@ -921,6 +953,8 @@ function transform_source(chunk)
chunk.source
elseif chunk.language === :r
wrap_with_r_boilerplate(chunk.source)
elseif chunk.language === :python
wrap_with_python_boilerplate(chunk.source)
else
error("Unhandled code chunk language $(chunk.language)")
end
Expand Down Expand Up @@ -1064,6 +1098,11 @@ is_julia_toplevel(node) =
node.t.info == "{julia}" &&
node.parent.t isa CommonMark.Document

is_python_toplevel(node) =
node.t isa CommonMark.CodeBlock &&
node.t.info == "{python}" &&
node.parent.t isa CommonMark.Document

is_r_toplevel(node) =
node.t isa CommonMark.CodeBlock &&
node.t.info == "{r}" &&
Expand Down
20 changes: 20 additions & 0 deletions test/examples/integrations/PythonCall.qmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: PythonCall integration
julia:
exeflags: ["--project=PythonCall"]
---

This should fail since we didn't import PythonCall yet.

```{python}
import re
```

```{julia}
using PythonCall
```

```{python}
import re
words = re.findall("[a-zA-Z]+", "PythonCall.jl is very useful!")
```
2 changes: 2 additions & 0 deletions test/examples/integrations/PythonCall/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[deps]
PythonCall = "6099a3de-0909-46bc-b1f4-468b9a2dfc0d"
10 changes: 10 additions & 0 deletions test/testsets/integrations/PythonCall.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
include("../../utilities/prelude.jl")

test_example(joinpath(@__DIR__, "../../examples/integrations/PythonCall.qmd")) do json
cells = json["cells"]

@test occursin("PythonCall must be imported", cells[3]["outputs"][1]["traceback"][1])

@test cells[8]["data"]["outputs"][1]["text/plain"] ==
"Python: ['PythonCall', 'jl', 'is', 'very', 'useful']"
end