dgrunwald / rust-cpython

Rust <-> Python bindings
MIT License
1.81k stars 136 forks source link

no method named `call` found for type `cpython::PyObject` in the current scope #121

Open nilday opened 6 years ago

nilday commented 6 years ago

I want to pass a python function to rust code and use rust to call it. As there is no PyFunction struct in rust-cpython, I think I should use PyObject to do it. But every time i use a pyObject.call(py, args, None), I got: no method named call found for type cpython::PyObject in the current scope

How should I solve this problem?

boydjohnson commented 6 years ago

Hi, @nilday. To get call and call_method on a PyOjbect you need the trait cpython::ObjectProtocol, so use cpython::ObjectProtocol is necessary.

That being said, to call a python function, first you have to import the module:

let gil = cpython::Python::acquire_gil();
let py = gil.python();

let module = cpython::PyModule::import(py, "path.to.python.module")
                                 .unwrap();

module.call(py, "function_name", cpython::NoArgs, None).unwrap()
hasanOryx commented 6 years ago

I wrote the below code:

myapp/src/main.rs

extern crate cpython;

use cpython::{Python};
fn main() {
    let gil = Python::acquire_gil();
    println!("Hello from Rust!");
    let py = gil.python();
    let module = cpython::PyModule::import(py, "fibo").unwrap();

    module.call(py, "fib", (1,), None).unwrap();
}

And saved the python module as myapp/pyth/fibo.py

But got the below error:

thread 'main' panicked at 'called Result::unwrap() on an Err value: PyErr { ptype: <class 'ModuleNotFoundEr ror'>, pvalue: Some(ModuleNotFoundError("No module named 'fibo'",)), ptraceback: None }', libcore/result.rs:945 :5

ssokolow commented 6 years ago

I'm not sure why you'd think that myapp/pyth would automatically be in your Python import path.

This is no different from getting an ImportError in Python because a file isn't in your import path.

Try adding these before the attempt to import fibo and you'll see what I mean:

println!("");
let exe_path = ::std::env::current_exe().unwrap();
println!("Root for relative imports if the contents of Python's os.curdir (usually '.') is in the import path:\n\t{}", exe_path.display());

let sys = cpython::PyModule::import(py, "sys").unwrap();
let sys_path = sys.get(py, "path").unwrap();
println!("Import Path:\n\t{:?}\n", sys_path);

In case you don't mind having to recompile the Rust every time you change the Python, bundling the Python code within your Rust source at compile time avoids the whole "import path" mess.

You can do that by using this code instead:

// Load the contents of ../pyth/fibo.py into a string literal named "fibo_str" at compile time
let fibo_str = include_str!("../pyth/fibo.py");  

// Create a new empty module named "fibo"
let module = cpython::PyModule::new(py, "fibo").unwrap();

// Run the contents of `fibo_str` in the context of `module` to populate the module
py.run(fibo_str, Some(&module.dict(py)), None).unwrap();

This is the rust-cpython equivalent to the Python "import a string as a module" example on this StackOverflow answer.

If you want a rust-cpython equivalent to the "To ignore any next attempt to import, add the module to sys" line, here it is:

let sys = cpython::PyModule::import(py, "sys").unwrap();
sys.get(py, "modules").unwrap().set_item(py, "fibo", &module).unwrap();
hasanOryx commented 6 years ago

Thanks @ssokolow

I wrote the below code:

extern crate cpython;

use cpython::{Python, PyResult, PyModule};

// Load the contents of ../pyth/fibo.py into a string literal named "fibo_str" at compile time
const FIBO_STR: &'static str = include_str!("../pyth/fibo.py");

fn main() {
    let gil = Python::acquire_gil();
    let py = gil.python();

    example(py).unwrap();
}

fn example(py: Python<'_>) -> PyResult<()> {

    let m = module_from_str(py, "fibo", FIBO_STR)?;

    let out: Vec<i32>  = m.call(py, "fib", (2,), None)?.extract(py)?;
    println!("successfully found fibo.py at compiletime.  Output: {:?}", out);

    Ok(())
}

fn module_from_str(py: Python<'_>, name: &str, source: &str) -> PyResult<PyModule> {
    // Create a new empty module named "fibo"
    let module = cpython::PyModule::new(py, name).unwrap();

    // Run the contents of `fibo_str` in the context of `module` to populate the module
    py.run(source, Some(&module.dict(py)), None).unwrap();

    Ok(module)
}

But got the below error:

thread 'main' panicked at 'called Result::unwrap() on an Err value: PyErr { ptype: <class 'NameError'>, pvalue: Some("name 'print' is not defined"), ptraceback: Some(<traceback object at 0x1031a5cc8>) }', libcore/result.rs:945:5

The fibo.py is:

def fib(n):   # return Fibonacci series up to n
    print('Hello from python!')
    result = []
    a, b = 0, 1
    while a < n:
        result.append(a)
        a, b = b, a+b
    return result

Moreover, when I tried using sys.get(py, "modules").unwrap().set_item(py, "fibo", &module).unwrap(); I got that no method namedset_itemfound for typecpython::PyObjectin the current scope

ssokolow commented 6 years ago

Moreover, when I tried using sys.get(py, "modules").unwrap().set_item(py, "fibo", &module).unwrap(); I got that no method namedset_itemfound for typecpython::PyObjectin the current scope

Oops. Sorry about that. I forgot to mention this line in the code I pasted for that snip:

use cpython::ObjectProtocol;

But got the below error:

thread 'main' panicked at 'called Result::unwrap() on an Err value: PyErr { ptype: <class 'NameError'>, pvalue: Some("name 'print' is not defined"), ptraceback: Some(<traceback object at 0x1031a5cc8>) }', libcore/result.rs:945:5

Yeah. It looks like exposing the contents of the builtins module as globals is optional and non-default in rust-cpython. I'll get back to you on that some time after I've had lunch.

ssokolow commented 6 years ago

I figured it out while waiting for the pot to boil.

According to the Python docs for exec()...

If the globals dictionary does not contain a value for the key __builtins__, a reference to the dictionary of the built-in module builtins is inserted under that key. That way you can control what builtins are available to the executed code by inserting your own __builtins__ dictionary into globals before passing it to exec().

It turned out all that was necessary was to add this line after the let module = ... line:

    let builtins = cpython::PyModule::import(py, "builtins").unwrap();
    module.dict(py).set_item(py, "__builtins__", &builtins).unwrap();

...though, given the way you're using it, I'll have to see if I can use lazy_static for the builtins object after I've had my food.

hasanOryx commented 6 years ago

Thanks @ssokolow , I'll check it once I be home, hope you enjoyed your breakfast :)

hasanOryx commented 6 years ago

Thanks a lot, it is working as expected, may you need to add simple example in the codes for the new users :)

ssokolow commented 6 years ago

Never mind. I forgot about the need to pass the GIL lock into PyModule::import. That makes every use of lazy_static I could think of quite ugly.