diff --git a/README.md b/README.md index 66c835ec..444de9c1 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,7 @@ Here is a simple example to call Python's `math.sin` function and compare it to the built-in Julia `sin`: using PyCall - @pyimport math + math = pyimport("math") math.sin(math.pi / 4) - sin(pi / 4) # returns 0.0 Type conversions are automatically performed for numeric, boolean, @@ -104,12 +104,6 @@ arrays/lists, and dictionaries of these types. (Python functions can be converted/passed to Julia functions and vice versa!) Other types are supported via the generic PyObject type, below. -Python submodules must be imported by a separate `@pyimport` call, and -in this case you must supply an identifier to to use in Julia. For example - - @pyimport numpy.random as nr - nr.rand(3,4) - Multidimensional arrays exploit the NumPy array interface for conversions between Python and Julia. By default, they are passed from Julia to Python without making a copy, but from Python to Julia a @@ -120,7 +114,7 @@ Keyword arguments can also be passed. For example, matplotlib's [pyplot](http://matplotlib.org/) uses keyword arguments to specify plot options, and this functionality is accessed from Julia by: - @pyimport matplotlib.pyplot as plt + plt = pyimport("matplotlib.pyplot") x = linspace(0,2*pi,1000); y = sin(3*x + 4*cos(2*x)); plt.plot(x, y, color="red", linewidth=2.0, linestyle="--") plt.show() @@ -133,7 +127,7 @@ Arbitrary Julia functions can be passed to Python routines taking function arguments. For example, to find the root of cos(x) - x, we could call the Newton solver in scipy.optimize via: - @pyimport scipy.optimize as so + so = pyimport("scipy.optimize") so.newton(x -> cos(x) - x, 1) A macro exists for mimicking Python's "with statement". For example: @@ -147,23 +141,6 @@ conversion, use `f::PyObject`). Similarly, if the context manager returns a type which is automatically converted to a Julia type, you will have override this via `@pywith EXPR::PyObject ...`. - -**Important:** The biggest difference from Python is that object attributes/members are -accessed with `o[:attribute]` rather than `o.attribute`, so that `o.method(...)` in -Python is replaced by `o[:method](...)` in Julia. Also, you use -`get(o, key)` rather than `o[key]`. (However, you can access integer -indices via `o[i]` as in Python, albeit with 1-based Julian indices rather -than 0-based Python indices.) (This will be changed to `o.method` once Julia -0.6 support is dropped, now that Julia supports `.` overloading.) See also the section on -`PyObject` below, as well as the `pywrap` function to create anonymous -modules that simulate `.` access (this is what `@pyimport` does). For -example, using [Biopython](http://biopython.org/wiki/Seq) we can do: - - @pyimport Bio.Seq as s - @pyimport Bio.Alphabet as a - my_dna = s.Seq("AGTACACTGGT", a.generic_dna) - my_dna.find("ACT") - ## Troubleshooting Here are solutions to some common problems: @@ -172,8 +149,8 @@ Here are solutions to some common problems: ## Python object interfaces -The `@pyimport` macro is built on top of several routines for -manipulating Python objects in Julia, via a type `PyObject` described +PyCall provides many routines for +manipulating Python objects in Julia via a type `PyObject` described below. These can be used to have greater control over the types and data passed between Julia and Python, as well as to access additional Python functionality (especially in conjunction with the low-level interfaces @@ -311,7 +288,7 @@ and the fact that the Julia JIT compiler can no longer infer the type). ### Calling Python -In most cases, the `@pyimport` macro automatically makes the +In most cases, PyCall automatically makes the appropriate type conversions to Julia types based on runtime inspection of the Python objects. However greater control over these type conversions (e.g. to use a no-copy `PyArray` for a Python @@ -331,13 +308,6 @@ and also by providing more type information to the Julia compiler. `@pycall function(args...)::returntype` into `pycall(function,returntype,args...)`. -* `pyimport(s)`: Import the Python module `s` (a string or symbol) and - return a pointer to it (a `PyObject`). Functions or other symbols - in the module may then be looked up by `s.name` where `name` is a - string (for the raw `PyObject`) or symbol (for automatic - type-conversion). Unlike the `@pyimport` macro, this does not - define a Julia module. - * `py"..."` evaluates `"..."` as a Python string, equivalent to Python's [`eval`](https://docs.python.org/2/library/functions.html#eval) function, and returns the result converted to `PyAny`. Alternatively, `py"..."o` returns the raw `PyObject` @@ -363,18 +333,6 @@ and also by providing more type information to the Julia compiler. symbol it returns the builtin converted to `PyAny`. (You can also use `py"s"` to look up builtins or other Python globas.) -* `pywrap(o::PyObject)` returns a wrapper `w` that is an anonymous - module which provides (read) access to converted versions of `o`'s - members as `w.member`. (For example, `@pyimport module as name` is - equivalent to `name = pywrap(pyimport("module"))`.) If the Python - module contains identifiers that are reserved words in Julia - (e.g. `function`), they cannot be accessed as `w.member`; one must - instead use `w.pymember(:member)` (for the `PyAny` conversion) or - `w.pymember("member")` (for the raw `PyObject`). `pywrap` is rather - inefficient since it converts *every* member of `o` at once; you - are generally encouraged to simply access members via `o.member` - rather than using `pywrap`. - Occasionally, you may need to pass a keyword argument to Python that is a [reserved word](https://en.wikipedia.org/wiki/Reserved_word) in Julia. For example, calling `f(x, function=g)` will fail because `function` is @@ -403,7 +361,7 @@ documentation `?pyfunction` and `?pyfunctionret` for more information. `@pydef` creates a Python class whose methods are implemented in Julia. For instance, - @pyimport numpy.polynomial as P + P = pyimport("numpy.polynomial") @pydef mutable struct Doubler <: P.Polynomial function __init__(self, x=10) self.x = x @@ -441,7 +399,7 @@ The method arguments and return values are automatically converted between Julia Here's another example using [Tkinter](https://wiki.python.org/moin/TkInter): using PyCall - @pyimport Tkinter as tk + tk = pyimport("Tkinter") @pydef mutable struct SampleApp <: tk.Tk __init__(self, args...; kwargs...) = begin @@ -579,14 +537,12 @@ end end ``` -Then you can access the `scipy.optimize` functions as `scipy_opt[:newton]` +Then you can access the `scipy.optimize` functions as `scipy_opt.newton` and so on. Here, instead of `pyimport`, we have used the function `pyimport_conda`. The second argument is the name of the [Anaconda package](https://docs.continuum.io/anaconda/pkg-docs) that provides this module. This way, if importing `scipy.optimize` fails because the user hasn't installed `scipy`, it will either (a) automatically install `scipy` and retry the `pyimport` if PyCall is configured to use the [Conda](https://github.com/Luthaf/Conda.jl) Python install (or any other Anaconda-based Python distro for which the user has installation privileges), or (b) throw an error explaining that `scipy` needs to be installed, and explain how to configure PyCall to use Conda so that it can be installed automatically. More generally, you can call `pyimport(module, package, channel)` to specify an optional Anaconda "channel" for installing non-standard Anaconda packages. -(Note that you cannot use `@pyimport` safely with precompilation, because that declares a global constant that internally has a pointer to the module. You can use `pywrap(pyimport(...))` in your `__init__` function to a assign a global variable using the `.` notation like `@pyimport`, however, albeit without the type stability of the global `const` as above.) - ## Author This package was written by [Steven G. Johnson](http://math.mit.edu/~stevenj/). diff --git a/src/PyCall.jl b/src/PyCall.jl index 63cfe6bf..0d807e4b 100644 --- a/src/PyCall.jl +++ b/src/PyCall.jl @@ -79,7 +79,7 @@ PyPtr(o::PyObject) = getfield(o, :o) """ ≛(x, y) -`PyPtr` based comparison of `x` and `y`, which can be of type `PyObject` or `PyPtr`. +`PyPtr` based comparison of `x` and `y`, which can be of type `PyObject` or `PyPtr`. """ ≛(o1::Union{PyObject,PyPtr}, o2::Union{PyObject,PyPtr}) = PyPtr(o1) == PyPtr(o2) @@ -303,7 +303,7 @@ end getproperty(o::PyObject, s::Symbol) = convert(PyAny, getproperty(o, string(s))) -propertynames(o::PyObject) = map(x->Symbol(first(x)), +propertynames(o::PyObject) = map(x->Symbol(first(x)), pycall(inspect."getmembers", PyObject, o)) # avoiding method ambiguity @@ -361,6 +361,22 @@ keys(o::PyObject) = Symbol[m[1] for m in pycall(inspect."getmembers", # we skip wrapping Julia reserved words (which cannot be type members) const reserved = Set{String}(["while", "if", "for", "try", "return", "break", "continue", "function", "macro", "quote", "let", "local", "global", "const", "abstract", "typealias", "type", "bitstype", "immutable", "ccall", "do", "module", "baremodule", "using", "import", "export", "importall", "pymember", "false", "true", "Tuple"]) +function _pywrap(o::PyObject, mname::Symbol) + members = convert(Vector{Tuple{AbstractString,PyObject}}, + pycall(inspect."getmembers", PyObject, o)) + filter!(m -> !(m[1] in reserved), members) + m = Module(mname, false) + consts = [Expr(:const, Expr(:(=), Symbol(x[1]), convert(PyAny, x[2]))) for x in members] + exports = try + convert(Vector{Symbol}, o."__all__") + catch + [Symbol(x[1]) for x in filter(x -> x[1][1] != '_', members)] + end + Core.eval(m, Expr(:toplevel, consts..., :(pymember(s) = $(getindex)($(o), s)), + Expr(:export, exports...))) + m +end + """ pywrap(o::PyObject) @@ -371,19 +387,13 @@ For example, `@pyimport module as name` is equivalent to `const name = pywrap(py If the Python module contains identifiers that are reserved words in Julia (e.g. function), they cannot be accessed as `w.member`; one must instead use `w.pymember(:member)` (for the PyAny conversion) or w.pymember("member") (for the raw PyObject). """ function pywrap(o::PyObject, mname::Symbol=:__anon__) - members = convert(Vector{Tuple{AbstractString,PyObject}}, - pycall(inspect."getmembers", PyObject, o)) - filter!(m -> !(m[1] in reserved), members) - m = Module(mname, false) - consts = [Expr(:const, Expr(:(=), Symbol(x[1]), convert(PyAny, x[2]))) for x in members] - exports = try - convert(Vector{Symbol}, o."__all__") - catch - [Symbol(x[1]) for x in filter(x -> x[1][1] != '_', members)] - end - Core.eval(m, Expr(:toplevel, consts..., :(pymember(s) = $(getindex)($(o), s)), - Expr(:export, exports...))) - m + Base.depwarn("`pywrap(o)`` is deprecated in favor of direct property access `o.foo`.", :pywrap) + return _pywrap(o, mname) +end + +function _pywrap_pyimport(o::PyObject, mname::Symbol=:__anon__) + Base.depwarn("`@pyimport foo` is deprecated in favor of `foo = pyimport(\"foo\")`.", :_pywrap_pyimport) + return _pywrap(o, mname) end ######################################################################### @@ -563,7 +573,7 @@ macro pyimport(name, optional_varname...) quoteName = Expr(:quote, Name) quote if !isdefined($__module__, $quoteName) - const $(esc(Name)) = pywrap(pyimport($mname)) + const $(esc(Name)) = _pywrap_pyimport(pyimport($mname)) elseif !isa($(esc(Name)), Module) error("@pyimport: ", $(Expr(:quote, Name)), " already defined") end diff --git a/src/pyclass.jl b/src/pyclass.jl index e2211808..cae31e72 100644 --- a/src/pyclass.jl +++ b/src/pyclass.jl @@ -79,7 +79,7 @@ function parse_pydef(expr) if isfunction(line) def_dict = splitdef(line) py_f = def_dict[:name] - # The dictionary of the new Julia-side definition. + # The dictionary of the new Julia-side definition. jl_def_dict = copy(def_dict) if isa(py_f, Symbol) # Method definition @@ -133,7 +133,7 @@ end `@pydef` creates a Python class whose methods are implemented in Julia. For instance, - @pyimport numpy.polynomial as P + P = pyimport("numpy.polynomial") @pydef type Doubler <: P.Polynomial __init__(self, x=10) = (self.x = x) my_method(self, arg1::Number) = arg1 + 20 diff --git a/test/runtests.jl b/test/runtests.jl index 95e496fe..3eb542fa 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -15,7 +15,7 @@ roundtrip(x) = roundtrip(PyAny, x) roundtripeq(T, x) = roundtrip(T, x) == x roundtripeq(x) = roundtrip(x) == x -@pyimport math +const math = pyimport("math") struct TestConstruct x @@ -128,10 +128,6 @@ const PyInt = pyversion < v"3" ? Int : Clonglong @test collect(PyObject([1,"hello",5])) == [1,"hello",5] - @test try @eval (@pyimport os.path) catch ex - isa((ex::LoadError).error, ArgumentError) - end - @test PyObject("hello") == PyObject("hello") @test PyObject("hello") != PyObject("hellö") @test PyObject(hash) == PyObject(hash)