diff --git a/README.md b/README.md index 12882ab3..1a508c83 100644 --- a/README.md +++ b/README.md @@ -145,45 +145,45 @@ which is automatically converted to a Julia type, you will have override this via `@pywith EXPR::PyObject ...`. If you are already familiar with Python, it perhaps is easier to use -`py"..."` and `py"""..."""` which are equivalent to Python's +`` py`...` `` and ` py```...``` ` which are equivalent to Python's [`eval`](https://docs.python.org/3/library/functions.html#eval) and [`exec`](https://docs.python.org/3/library/functions.html#exec), respectively: -```julia -py""" +````julia +py``` import numpy as np def sinpi(x): return np.sin(np.pi * x) -""" -py"sinpi"(1) ``` +py`sinpi`(1) +```` When creating a Julia module, it is a useful pattern to define Python functions or classes in Julia's `__init__` and then use it in Julia -function with `py"..."`. +function with `` py`...` ``. -```julia +````julia module MyModule using PyCall function __init__() - py""" + py``` import numpy as np def one(x): return np.sin(x) ** 2 + np.cos(x) ** 2 - """ + ``` end -two(x) = py"one"(x) + py"one"(x) +two(x) = py`one`(x) + py`one`(x) end -``` +```` -Note that Python code in `py"..."` of above example is evaluated in a +Note that Python code in `` py`...` `` of above example is evaluated in a Python namespace dedicated to `MyModule`. Thus, Python function `one` cannot be accessed outside `MyModule`. @@ -355,38 +355,38 @@ and also by providing more type information to the Julia compiler. `@pycall function(args...)::returntype` into `pycall(function,returntype,args...)`. -* `py"..."` evaluates `"..."` as Python code, equivalent to +* `` py`...` `` evaluates `"..."` as Python code, equivalent to Python's [`eval`](https://docs.python.org/3/library/functions.html#eval) function, and returns the result - converted to `PyAny`. Alternatively, `py"..."o` returns the raw `PyObject` + converted to `PyAny`. Alternatively, `` py`...`o `` returns the raw `PyObject` (which can then be manually converted if desired). You can interpolate Julia variables and other expressions into the Python code with `$`, which interpolates the *value* (converted to `PyObject`) of the given expression---data is not passed as a string, so this is different from - ordinary Julia string interpolation. e.g. `py"sum($([1,2,3]))"` calls the + ordinary Julia string interpolation. e.g. `` py`sum($([1,2,3]))` `` calls the Python `sum` function on the Julia array `[1,2,3]`, returning `6`. In contrast, if you use `$$` before the interpolated expression, then the value of the expression is inserted as a string into the Python code, allowing you to generate Python code itself via Julia expressions. - For example, if `x="1+1"` in Julia, then `py"$x"` returns the string `"1+1"`, - but `py"$$x"` returns `2`. - If you use `py"""..."""` to pass a *multi-line* string, the string can + For example, if `x="1+1"` in Julia, then `` py`$x` `` returns the string `"1+1"`, + but `` py`$$x` `` returns `2`. + If you use ` py```...``` ` to pass a *multi-line* string, the string can contain arbitrary Python code (not just a single expression) to be evaluated, but the return value is `nothing`; this is useful e.g. to define pure-Python functions, and is equivalent to Python's [`exec`](https://docs.python.org/3/library/functions.html#exec) function. - (If you define a Python global `g` in a multiline `py"""..."""` - string, you can retrieve it in Julia by subsequently evaluating `py"g"`.) + (If you define a Python global `g` in a multiline ` py```...``` ` + string, you can retrieve it in Julia by subsequently evaluating `` py`g` ``.) - When `py"..."` is used inside a Julia module, it uses a Python namespace + When `` py`...` `` is used inside a Julia module, it uses a Python namespace dedicated to this Julia module. Thus, you can define Python function - using `py"""...."""` in your module without worrying about name clash + using ` py```....``` ` in your module without worrying about name clash with other Python code. Note that Python functions _must_ be defined in `__init__`. Side-effect in Python occurred at top-level Julia scope cannot be used at run-time for precompiled modules. * `pybuiltin(s)`: Look up `s` (a string or symbol) among the global Python builtins. If `s` is a string it returns a `PyObject`, while if `s` is a - symbol it returns the builtin converted to `PyAny`. (You can also use `py"s"` + symbol it returns the builtin converted to `PyAny`. (You can also use `` py`s` `` to look up builtins or other Python globas.) Occasionally, you may need to pass a keyword argument to Python that diff --git a/src/PyCall.jl b/src/PyCall.jl index 76bb5735..868c3a87 100644 --- a/src/PyCall.jl +++ b/src/PyCall.jl @@ -13,8 +13,8 @@ export pycall, pycall!, pyimport, pyimport_e, pybuiltin, PyObject, PyReverseDims pyisinstance, pywrap, pytypeof, pyeval, PyVector, pystring, pystr, pyrepr, pyraise, pytype_mapping, pygui, pygui_start, pygui_stop, pygui_stop_all, @pylab, set!, PyTextIO, @pysym, PyNULL, ispynull, @pydef, - pyimport_conda, @py_str, @pywith, @pycall, pybytes, pyfunction, pyfunctionret, - pywrapfn, pysetarg!, pysetargs! + pyimport_conda, @py_cmd, @py_str, @pywith, @pycall, pybytes, pyfunction, + pyfunctionret, pywrapfn, pysetarg!, pysetargs! import Base: size, ndims, similar, copy, getindex, setindex!, stride, convert, pointer, summary, convert, show, haskey, keys, values, diff --git a/src/pyeval.jl b/src/pyeval.jl index 4eca19d5..adc026e4 100644 --- a/src/pyeval.jl +++ b/src/pyeval.jl @@ -53,7 +53,7 @@ For example, `pyeval("x + y", x=1, y=2)` returns 3. function pyeval(s::AbstractString, returntype::TypeTuple=PyAny, locals=PyDict{AbstractString, PyObject}(), input_type=Py_eval_input; kwargs...) - # construct deprecation warning in favor of py"..." strings + # construct deprecation warning in favor of py`...` strings depbuf = IOBuffer() q = input_type==Py_eval_input ? "\"" : "\"\"\"\n" qr = reverse(q) @@ -177,17 +177,18 @@ function interpolate_pycode(code::AbstractString) end """ - py".....python code....." + py`.....python code.....`[o] + py".....python code....."[o] -Evaluate the given Python code string in the main Python module. +Evaluate the given Python code in the main Python module. -If the string is a single line (no newlines), then the Python +If the input is a single line (no newlines), then the Python expression is evaluated and the result is returned. -If the string is multiple lines (contains a newline), then the Python +If the input has multiple lines (contains a newline), then the Python code is compiled and evaluated in the `__main__` Python module and nothing is returned. -If the `o` option is appended to the string, as in `py"..."o`, then the +If the `o` option is appended to the command, as in ``` py`...`o ```, then the return value is an unconverted `PyObject`; otherwise, it is automatically converted to a native Julia type if possible. @@ -196,12 +197,21 @@ Any `\$var` or `\$(expr)` expressions that appear in the Python code and passed to Python via auto-generated global variables. This allows you to "interpolate" Julia values into Python code. -Similarly, ny `\$\$var` or `\$\$(expr)` expressions in the Python code +Similarly, `\$\$var` or `\$\$(expr)` expressions in the Python code are evaluated in Julia, converted to strings via `string`, and are -pasted into the Python code. This allows you to evaluate code +pasted into the Python code. This allows you to evaluate code where the code itself is generated by a Julia expression. """ +macro py_cmd(code, options...) + return py_cmd(__module__, code, options...) +end + +@doc (@doc @py_cmd) macro py_str(code, options...) + return py_cmd(__module__, code, options...) +end + +function py_cmd(__module__, code, options...) T = length(options) == 1 && 'o' in options[1] ? PyObject : PyAny code, locals = interpolate_pycode(code) input_type = '\n' in code ? Py_file_input : Py_eval_input diff --git a/test/runtests.jl b/test/runtests.jl index eef1360e..4f56c2b4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -155,7 +155,7 @@ const PyInt = pyversion < v"3" ? Int : Clonglong # issue #92: let x = PyVector(PyAny[]) - py"lambda x: x.append(\"bar\")"(x) + py`lambda x: x.append("bar")`(x) @test x == ["bar"] end @@ -267,21 +267,21 @@ const PyInt = pyversion < v"3" ? Int : Clonglong @test convert(BigInt, PyObject(1234)) == 1234 # hasproperty, getproperty, and propertynames - py""" + py``` class A: class B: C = 1 - """ - A = py"A" + ``` + A = py`A` @test hasproperty(A, "B") - @test getproperty(A, "B") == py"A.B" + @test getproperty(A, "B") == py`A.B` @test :B in propertynames(A) @static if VERSION >= v"0.7-" @test A.B.C == 1 @test_throws KeyError A.X end - setproperty!(py"A.B", "C", 2) - @test py"A.B.C" == 2 + setproperty!(py`A.B`, "C", 2) + @test py`A.B.C` == 2 # buffers let b = PyCall.PyBuffer(pyutf8("test string")) @@ -373,23 +373,23 @@ const PyInt = pyversion < v"3" ? Int : Clonglong end let x = 7 - py""" + py``` def myfun(x): return x + $x - """ - @test py"1 + 2" == 3 - @test py"1 + $x" == 8 - @test py"1 + $(x^2)" == 50 - @test py"myfun"(10) == 17 + ``` + @test py`1 + 2` == 3 + @test py`1 + $x` == 8 + @test py`1 + $(x^2)` == 50 + @test py`myfun`(10) == 17 end # issue #352 let x = "1+1" - @test py"$x" == "1+1" - @test py"$$x" == py"$$(x)" == 2 - @test py"7 - $$x - 7" == 0 # evaluates "7 - 1 + 1 - 7" - @test py"7 - ($$x) - 7" == -2 # evaluates "7 - (1 + 1) - 7" - @test py"1 + $$(x[1:2]) 3" == 5 # evals 1 + 1+ 3 + @test py`$x` == "1+1" + @test py`$$x` == py`$$(x)` == 2 + @test py`7 - $$x - 7` == 0 # evaluates "7 - 1 + 1 - 7" + @test py`7 - ($$x) - 7` == -2 # evaluates "7 - (1 + 1) - 7" + @test py`1 + $$(x[1:2]) 3` == 5 # evals 1 + 1+ 3 end # Float16 support: @@ -429,15 +429,15 @@ const PyInt = pyversion < v"3" ? Int : Clonglong end # Expose python docs to Julia doc system - py""" + py``` def foo(): "foo docstring" return 0 class bar: foo = foo - """ - global foo354 = py"foo" - global barclass = py"bar" + ``` + global foo354 = py`foo` + global barclass = py`bar` # use 'content' since `Text` objects test equality by object identity @test @doc(foo354).content == "foo docstring" @test @doc(barclass.foo).content == "foo docstring" @@ -518,7 +518,7 @@ const PyInt = pyversion < v"3" ? Int : Clonglong end @test occursin("integer", Base.Docs.doc(PyObject(1)).content) - @test occursin("no docstring", Base.Docs.doc(PyObject(py"lambda x: x+1")).content) + @test occursin("no docstring", Base.Docs.doc(PyObject(py`lambda x: x+1`)).content) let b = rand(UInt8, 1000) @test(convert(Vector{UInt8}, pybytes(b)) == b @@ -555,9 +555,9 @@ const PyInt = pyversion < v"3" ? Int : Clonglong @test_throws KeyError PyObject(TestConstruct(1)).y # iterating over Julia objects in Python: - @test py"[x**2 for x in $(PyCall.pyjlwrap_new(1:4))]" == - py"[x**2 for x in $(x for x in 1:4)]" == - py"[x**2 for x in $(PyCall.jlwrap_iterator(1:4))]" == + @test py`[x**2 for x in $(PyCall.pyjlwrap_new(1:4))]` == + py`[x**2 for x in $(x for x in 1:4)]` == + py`[x**2 for x in $(PyCall.jlwrap_iterator(1:4))]` == [1,4,9,16] let o = PyObject("foo") @@ -580,7 +580,7 @@ const PyInt = pyversion < v"3" ? Int : Clonglong end # issue #533 - @test py"lambda x,y,z: (x,y,z)"(3:6,4:10,5:11) === (PyInt(3):PyInt(6), PyInt(4):PyInt(10), PyInt(5):PyInt(11)) + @test py`lambda x,y,z: (x,y,z)`(3:6,4:10,5:11) === (PyInt(3):PyInt(6), PyInt(4):PyInt(10), PyInt(5):PyInt(11)) @test float(PyObject(1)) === 1.0 @test float(PyObject(1+2im)) === 1.0 + 2.0im @@ -650,12 +650,12 @@ end using PyCall obj = pyimport("sys") # get some PyObject end) - py""" + py``` ns = {} def set(name): ns[name] = $include_string($anonymous, name) - """ - py"set"("obj") + ``` + py`set`("obj") @test anonymous.obj != PyNULL() # Test above for pyjlwrap_getattr too: @@ -668,12 +668,12 @@ end end obj = S(pyimport("sys")) end) - py""" + py``` ns = {} def set(name): ns[name] = $include_string($anonymous, name).x - """ - py"set"("obj") + ``` + py`set`("obj") @test anonymous.obj.x != PyNULL() # Test above for pyjlwrap_iternext too: @@ -684,12 +684,12 @@ end sys = pyimport("sys") obj = (sys for _ in 1:1) end) - py""" + py``` ns = {} def set(name): ns[name] = list(iter($include_string($anonymous, name))) - """ - py"set"("obj") + ``` + py`set`("obj") @test anonymous.sys != PyNULL() end @@ -697,28 +697,28 @@ struct Unprintable end Base.show(::IO, ::Unprintable) = error("show(::IO, ::Unprintable) called") Base.show(::IO, ::Type{Unprintable}) = error("show(::IO, ::Type{Unprintable}) called") -py""" +py``` def try_repr(x): try: return repr(x) except Exception as err: return err -""" +``` -py""" +py``` def try_call(f): try: return f() except Exception as err: return err -""" +``` @testset "throwing show" begin unp = Unprintable() @test_throws Exception show(unp) - @test py"try_repr"("printable") isa String - @test pyisinstance(py"try_repr"(unp), pybuiltin("Exception")) - @test pyisinstance(py"try_call"(() -> throw(Unprintable())), + @test py`try_repr`("printable") isa String + @test pyisinstance(py`try_repr`(unp), pybuiltin("Exception")) + @test pyisinstance(py`try_call`(() -> throw(Unprintable())), pybuiltin("Exception")) end @@ -749,25 +749,25 @@ end @test Base.IteratorSize(PyCall.PyIterator(PyObject([1]))) == Base.HasLength() # 594 - @test collect(zip(py"iter([1, 2, 3])", 1:3)) == + @test collect(zip(py`iter([1, 2, 3])`, 1:3)) == [(1, 1), (2, 2), (3, 3)] - @test collect(zip(PyCall.PyIterator{Int}(py"iter([1, 2, 3])"), 1:3)) == + @test collect(zip(PyCall.PyIterator{Int}(py`iter([1, 2, 3])`), 1:3)) == [(1, 1), (2, 2), (3, 3)] - @test collect(zip(PyCall.PyIterator(py"[1, 2, 3]"o), 1:3)) == + @test collect(zip(PyCall.PyIterator(py`[1, 2, 3]`o), 1:3)) == [(1, 1), (2, 2), (3, 3)] end -@test_throws PyCall.PyError py"__testing_pynamespace" +@test_throws PyCall.PyError py`__testing_pynamespace` module __isolated_namespace using PyCall -py""" +py``` __testing_pynamespace = True -""" -get_testing_pynamespace() = py"__testing_pynamespace" +``` +get_testing_pynamespace() = py`__testing_pynamespace` end -@test_throws PyCall.PyError py"__testing_pynamespace" +@test_throws PyCall.PyError py`__testing_pynamespace` @test __isolated_namespace.get_testing_pynamespace() @testset "atexit" begin diff --git a/test/test_pyfncall.jl b/test/test_pyfncall.jl index 87374bb7..a67ed3fe 100644 --- a/test/test_pyfncall.jl +++ b/test/test_pyfncall.jl @@ -1,13 +1,13 @@ using Test, PyCall -py""" +py``` def mklist(*args, **kwargs): l = list(args) l.extend(kwargs.items()) return l -""" +``` @testset "pycall!" begin - pymklist = py"mklist" + pymklist = py`mklist` ret = PyNULL() function pycall_checks(res, pyf, RetType, args...; kwargs...)