Skip to content

Commit beb32da

Browse files
author
Christopher Doris
committed
adds pyeval and pyexec
1 parent a12d34a commit beb32da

File tree

2 files changed

+260
-0
lines changed

2 files changed

+260
-0
lines changed

src/PythonCall.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ include("concrete/type.jl")
3939
include("concrete/fraction.jl")
4040
include("concrete/method.jl")
4141
include("concrete/datetime.jl")
42+
include("concrete/code.jl")
4243
# @py
4344
# anything below can depend on @py, anything above cannot
4445
include("py_macro.jl")

src/concrete/code.jl

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
const MODULE_GLOBALS = Dict{Module,Py}()
2+
3+
function _pyeval_args(globals, locals)
4+
if globals isa Module
5+
globals_ = get!(pydict, MODULE_GLOBALS, globals)
6+
elseif ispy(globals)
7+
globals_ = globals
8+
else
9+
ArgumentError("globals must be a module or a Python dict")
10+
end
11+
if locals === nothing
12+
locals_ = Py(globals_)
13+
elseif ispy(locals)
14+
locals_ = Py(locals)
15+
else
16+
locals_ = pydict(locals)
17+
end
18+
return (globals_, locals_)
19+
end
20+
21+
"""
22+
pyeval([T=Py], code, globals, locals=nothing)
23+
24+
Evaluate the given Python `code`, returning the result as a `T`.
25+
26+
If `globals` is a `Module`, then a persistent `dict` unique to that module is used.
27+
28+
If `locals` is not specified, then it is set to `globals`, i.e. the code runs in global scope.
29+
30+
For example the following computes `1.1+2.2` in the `Main` module as a `Float64`:
31+
```
32+
pyeval(Float64, "x+y", Main, (x=1.1, y=2.2))
33+
```
34+
35+
See also [`@pyeval`](@ref).
36+
"""
37+
function pyeval(::Type{T}, code, globals, locals=nothing) where {T}
38+
globals_, locals_ = _pyeval_args(globals, locals)
39+
ans = pybuiltins.eval(code, globals_, locals_)
40+
pydel!(locals_)
41+
return T == Py ? ans : pyconvert_and_del(T, ans)
42+
end
43+
pyeval(code, globals, locals=nothing) = pyeval(Py, code, globals, locals)
44+
export pyeval
45+
46+
_pyexec_ans(::Type{Nothing}, globals, locals) = nothing
47+
@generated function _pyexec_ans(::Type{NamedTuple{names, types}}, globals, locals) where {names, types}
48+
# TODO: use precomputed interned strings
49+
# TODO: try to load from globals too
50+
n = length(names)
51+
code = []
52+
vars = Symbol[]
53+
for i in 1:n
54+
v = Symbol(:ans, i)
55+
push!(vars, v)
56+
push!(code, :($v = pyconvert_and_del($(types.parameters[i]), pygetitem(locals, $(string(names[i]))))))
57+
end
58+
push!(code, :(return $(NamedTuple{names, types})(($(vars...),))))
59+
return Expr(:block, code...)
60+
end
61+
62+
"""
63+
pyexec([T=Nothing], code, globals, locals=nothing)
64+
65+
Execute the given Python `code`.
66+
67+
If `globals` is a `Module`, then a persistent `dict` unique to that module is used.
68+
69+
If `locals` is not specified, then it is set to `globals`, i.e. the code runs in global scope.
70+
71+
If `T==Nothing` then returns `nothing`. Otherwise `T` must be a concrete `NamedTuple` type
72+
and the corresponding items from `locals` are extracted and returned.
73+
74+
For example the following computes `1.1+2.2` in the `Main` module as a `Float64`:
75+
```
76+
pyeval(@NamedTuple{ans::Float64}, "ans=x+y", Main, (x=1.1, y=2.2))
77+
```
78+
79+
See also [`@pyexec`](@ref).
80+
"""
81+
function pyexec(::Type{T}, code, globals, locals=nothing) where {T}
82+
globals_, locals_ = _pyeval_args(globals, locals)
83+
pydel!(pybuiltins.exec(code, globals_, locals_))
84+
ans = _pyexec_ans(T, globals_, locals_)
85+
pydel!(locals_)
86+
return ans
87+
end
88+
pyexec(code, globals, locals=nothing) = pyexec(Nothing, code, globals, locals)
89+
export pyexec
90+
91+
function _pyeval_macro_code(arg)
92+
if arg isa String
93+
return arg
94+
elseif arg isa Expr && arg.head === :macrocall && arg.args[1] == :(`foo`).args[1]
95+
return arg.args[3]
96+
else
97+
return nothing
98+
end
99+
end
100+
101+
function _pyeval_macro_args(arg, filename, mode)
102+
# separate out inputs => code => outputs (with only code being required)
103+
if @capture(arg, inputs_ => code_ => outputs_)
104+
code = _pyeval_macro_code(code)
105+
code === nothing && error("invalid code")
106+
elseif @capture(arg, lhs_ => rhs_)
107+
code = _pyeval_macro_code(lhs)
108+
if code === nothing
109+
code = _pyeval_macro_code(rhs)
110+
code === nothing && error("invalid code")
111+
inputs = lhs
112+
outputs = nothing
113+
else
114+
inputs = nothing
115+
outputs = rhs
116+
end
117+
else
118+
code = _pyeval_macro_code(arg)
119+
code === nothing && error("invalid code")
120+
inputs = outputs = nothing
121+
end
122+
# precompile the code
123+
codestr = code
124+
codeobj = pynew()
125+
codeready = Ref(false)
126+
code = quote
127+
if !$codeready[]
128+
$pycopy!($codeobj, $pybuiltins.compile($codestr, $filename, $mode))
129+
$codeready[] = true
130+
end
131+
$codeobj
132+
end
133+
# convert inputs to locals
134+
if inputs === :GLOBAL
135+
locals = nothing
136+
elseif inputs === nothing
137+
locals = ()
138+
else
139+
if inputs isa Expr && inputs.head === :tuple
140+
inputs = inputs.args
141+
else
142+
inputs = [inputs]
143+
end
144+
locals = []
145+
for input in inputs
146+
if @capture(input, var_Symbol)
147+
push!(locals, var => var)
148+
elseif @capture(input, var_Symbol = ex_)
149+
push!(locals, var => ex)
150+
else
151+
error("invalid input: $input")
152+
end
153+
end
154+
locals = :(($([:($var = $ex) for (var,ex) in locals]...),))
155+
end
156+
# done
157+
return locals, code, outputs
158+
end
159+
160+
"""
161+
@pyeval [inputs =>] code [=> T]
162+
163+
Evaluate the given `code` in a scope unique to the current module and return the answer as a `T`.
164+
165+
The `code` must be a literal string or command.
166+
167+
The `inputs` is a tuple of inputs of the form `v=expr` to be included in a temporary new
168+
`locals` dict. Only `v` is required, `expr` defaults to `v`.
169+
170+
As a special case, if `inputs` is `GLOBAL` then the code is run in global scope.
171+
172+
For example the following computes `1.1+2.2` and returns a `Float64`:
173+
```
174+
@pyeval (x=1.1, y=2.2) => `x+y` => Float64
175+
```
176+
"""
177+
macro pyeval(arg)
178+
locals, code, outputs = _pyeval_macro_args(arg, "$(__source__.file):$(__source__.line)", "eval")
179+
if outputs === nothing
180+
outputs = Py
181+
end
182+
esc(:($pyeval($outputs, $code, $__module__, $locals)))
183+
end
184+
export @pyeval
185+
186+
"""
187+
@pyexec [inputs =>] code [=> outputs]
188+
189+
Execute the given `code` in a scope unique to the current module.
190+
191+
The `code` must be a literal string or command.
192+
193+
The `inputs` is a tuple of inputs of the form `v=expr` to be included in a temporary new
194+
`locals` dict. Only `v` is required, `expr` defaults to `v`.
195+
196+
As a special case, if `inputs` is `GLOBAL` then the code is run in global scope.
197+
198+
The `outputs` is a tuple of outputs of the form `x::T=v`, meaning that `v` is extracted from
199+
locals, converted to `T` and assigned to `x`. Only `x` is required: `T` defaults to `Py`
200+
and `v` defaults to `x`.
201+
202+
For example the following computes `1.1+2.2` and assigns its value to `ans` as a `Float64`:
203+
```
204+
@pyexec (x=1.1, y=2.2) => `ans=x+y` => ans::Float64
205+
```
206+
"""
207+
macro pyexec(arg)
208+
locals, code, outputs = _pyeval_macro_args(arg, "$(__source__.file):$(__source__.line)", "exec")
209+
if outputs === nothing
210+
outputs = Nothing
211+
esc(:($pyexec(Nothing, $code, $__module__, $locals)))
212+
else
213+
if outputs isa Expr && outputs.head === :tuple
214+
oneoutput = false
215+
outputs = outputs.args
216+
else
217+
oneoutput = true
218+
outputs = [outputs]
219+
end
220+
pyvars = Symbol[]
221+
jlvars = Symbol[]
222+
types = []
223+
for output in outputs
224+
if @capture(output, lhs_ = rhs_)
225+
rhs isa Symbol || error("invalid output: $output")
226+
output = lhs
227+
pyvar = rhs
228+
else
229+
pyvar = missing
230+
end
231+
if @capture(output, lhs_ :: rhs_)
232+
outtype = rhs
233+
output = lhs
234+
else
235+
outtype = Py
236+
end
237+
output isa Symbol || error("invalid output: $output")
238+
if pyvar === missing
239+
pyvar = output
240+
end
241+
push!(pyvars, pyvar)
242+
push!(jlvars, output)
243+
push!(types, outtype)
244+
end
245+
outtype = :($NamedTuple{($(map(QuoteNode, pyvars)...),), Tuple{$(types...),}})
246+
ans = :($pyexec($outtype, $code, $__module__, $locals))
247+
if oneoutput
248+
ans = :($(jlvars[1]) = $ans[1])
249+
else
250+
if pyvars != jlvars
251+
outtype2 = :($NamedTuple{($(map(QuoteNode, jlvars)...),), Tuple{$(types...),}})
252+
ans = :($outtype2($ans))
253+
end
254+
ans = :(($(jlvars...),) = $ans)
255+
end
256+
esc(ans)
257+
end
258+
end
259+
export @pyexec

0 commit comments

Comments
 (0)