|
| 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