diff --git a/.travis.yml b/.travis.yml index 2def6f83..e25b5dbf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,10 +23,7 @@ matrix: env: global: - TRAVIS_RUST_VERSION=nightly - - RUSTFLAGS="-C link-dead-code" - RUST_BACKTRACE=1 - - SCCACHE_DIR="$HOME/.cargo/sccache" - - RUSTC_WRAPPER=sccache addons: apt: diff --git a/CHANGELOG.md b/CHANGELOG.md index b0d6e98b..d5e7d700 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,34 @@ # Changelog +All notable changes to this project will be documented in this file. -## 0.3.2 +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Removed + + * Conversions from tuples to PyDict. + +### Changed + + * Merged both examples into one + +## [0.3.2] - 2018-07-22 + +### Changed * Replaced `concat_idents` with mashup -## 0.3.1 +## [0.3.1] - 2018-07-18 + +### Fixed * Fixed scoping bug in pyobject_native_type that would break rust-numpy -## 0.3.0 +## [0.3.0] - 2018-07-18 + +### Changed * Upgraded to syn 0.14 which means much better error messages :tada: * 128 bit integer support by [kngwyu](https://github.com/kngwyu) ([#137](https://github.com/PyO3/pyo3/pull/173)) @@ -17,60 +37,109 @@ * Renamed the `base` option in the `pyclass` macro to `extends`. * `#[pymodinit]` uses the function name as module name, unless the name is overrriden with `#[pymodinit(name)]` * The guide is now properly versioned. + +### Added + * A few internal macros became part of the public api ([#155](https://github.com/PyO3/pyo3/pull/155), [#186](https://github.com/PyO3/pyo3/pull/186)) * Always clone in getters. This allows using the get-annotation on all Clone-Types -## 0.2.7 (2018-05-18) +## [0.2.7] - 2018-05-18 + +### Fixed * Fix nightly breakage with proc_macro_path -## 0.2.6 (2018-04-03) +## [0.2.6] - 2018-04-03 + +### Fixed * Fix compatibility with TryFrom trait #137 -## 0.2.5 (2018-02-21) +## [0.2.5] - 2018-02-21 + +### Added * CPython 3.7 support + +### Fixed + * Embedded CPython 3.7b1 crashes on initialization #110 * Generated extension functions are weakly typed #108 * call_method*() crashes when the method does not exist #113 * Allow importing exceptions from nested modules #116 -## 0.2.4 (2018-01-19) +## [0.2.4] - 2018-01-19 + +### Added * Allow to get mutable ref from PyObject #106 * Drop `RefFromPyObject` trait * Add Python::register_any() method + +### Fixed + * Fix impl `FromPyObject` for `Py` * Mark method that work with raw pointers as unsafe #95 +## [0.2.3] - 11-27-2017 -## 0.2.3 (11-27-2017) +### Fixed * Proper `c_char` usage #93 -* Remove use of now unneeded 'AsciiExt' trait + +### Changed + * Rustup to 1.23.0-nightly 2017-11-07 -## 0.2.2 (09-26-2017) +### Removed + +* Remove use of now unneeded 'AsciiExt' trait + + +## [0.2.2] - 09-26-2017 + +### Changed * Rustup to 1.22.0-nightly 2017-09-30 -## 0.2.1 (09-26-2017) +## [0.2.1] - 09-26-2017 + +### Fixed * Fix rustc const_fn nightly breakage -## 0.2.0 (08-12-2017) +## [0.2.0] - 08-12-2017 + +### Changed + +* Allow to add gc support without implementing PyGCProtocol #57 +* Refactor `PyErr` implementation. Drop `py` parameter from constructor. + +### Added * Added inheritance support #15 * Added weakref support #56 -* Allow to add gc support without implementing PyGCProtocol #57 -* Refactor `PyErr` implementation. Drop `py` parameter from constructor. * Added subclass support #64 * Added `self.__dict__` supoort #68 * Added `pyo3::prelude` module #70 * Better `Iterator` support for PyTuple, PyList, PyDict #75 * Introduce IntoPyDictPointer similar to IntoPyTuple #69 -## 0.1.0 (07-23-2017) +## [0.1.0] - 07-23-2017 + +### Added * Initial release + +[Unreleased]: https://github.com/olivierlacan/keep-a-changelog/compare/v0.3.2...HEAD +[0.3.2]: https://github.com/pyo3/pyo3/compare/v0.3.1...v0.3.2 +[0.3.1]: https://github.com/pyo3/pyo3/compare/v0.3.0...v0.3.1 +[0.3.0]: https://github.com/pyo3/pyo3/compare/v0.2.7...v0.3.0 +[0.2.7]: https://github.com/pyo3/pyo3/compare/v0.2.6...v0.2.7 +[0.2.6]: https://github.com/pyo3/pyo3/compare/v0.2.5...v0.2.6 +[0.2.5]: https://github.com/pyo3/pyo3/compare/v0.2.4...v0.2.5 +[0.2.4]: https://github.com/pyo3/pyo3/compare/v0.2.3...v0.2.4 +[0.2.3]: https://github.com/pyo3/pyo3/compare/v0.2.2...v0.2.3 +[0.2.2]: https://github.com/pyo3/pyo3/compare/v0.2.1...v0.2.2 +[0.2.1]: https://github.com/pyo3/pyo3/compare/v0.2.0...v0.2.1 +[0.2.0]: https://github.com/pyo3/pyo3/compare/v0.1.0...v0.2.0 diff --git a/README.md b/README.md index 0640386f..73917238 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,12 @@ # PyO3 -[![Build Status](https://travis-ci.org/PyO3/pyo3.svg?branch=master)](https://travis-ci.org/PyO3/pyo3) [![Build Status](https://ci.appveyor.com/api/projects/status/github/PyO3/pyo3?branch=master&svg=true)](https://ci.appveyor.com/project/fafhrd91/pyo3) [![codecov](https://codecov.io/gh/PyO3/pyo3/branch/master/graph/badge.svg)](https://codecov.io/gh/PyO3/pyo3) [![crates.io](http://meritbadge.herokuapp.com/pyo3)](https://crates.io/crates/pyo3) [![Join the dev chat](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/PyO3/Lobby) +[![Build Status](https://travis-ci.org/PyO3/pyo3.svg?branch=master)](https://travis-ci.org/PyO3/pyo3) +[![Build Status](https://ci.appveyor.com/api/projects/status/github/PyO3/pyo3?branch=master&svg=true)](https://ci.appveyor.com/project/fafhrd91/pyo3) +[![codecov](https://codecov.io/gh/PyO3/pyo3/branch/master/graph/badge.svg)](https://codecov.io/gh/PyO3/pyo3) +[![crates.io](http://meritbadge.herokuapp.com/pyo3)](https://crates.io/crates/pyo3) +[![Join the dev chat](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/PyO3/Lobby) -[Rust](http://www.rust-lang.org/) bindings for the [Python](https://www.python.org/) interpreter. This includes running and interacting with python code from a rust binaries as well as writing native python modules. +[Rust](http://www.rust-lang.org/) bindings for [Python](https://www.python.org/). This includes running and interacting with python code from a rust binaries as well as writing native python modules. * User Guide: [stable](https://pyo3.rs) | [master](https://pyo3.rs/master) * [API Documentation](https://docs.rs/crate/pyo3/) @@ -11,11 +15,72 @@ A comparison with rust-cpython can be found [in the guide](https://pyo3.rs/maste ## Usage -Pyo3 supports python 2.7 as well as python 3.5 and up. The minimum required rust version is 1.27.0-nightly 2018-05-01. +Pyo3 supports python 2.7 as well as python 3.5 and up. The minimum required rust version is 1.29.0-nightly 2018-07-16. -### From a rust binary +You can either write a native python module in rust or use python from a rust binary. -To use `pyo3`, add this to your `Cargo.toml`: +### Using rust from python + +Pyo3 can be used to generate a native python module. + +**`Cargo.toml`:** + +```toml +[package] +name = "rust-py" +version = "0.1.0" + +[lib] +name = "rust_py" +crate-type = ["cdylib"] + +[dependencies.pyo3] +version = "0.3" +features = ["extension-module"] +``` + +**`src/lib.rs`** + +```rust +#![feature(use_extern_macros, specialization)] + +#[macro_use] +extern crate pyo3; + +use pyo3::prelude::*; + +#[pyfunction] +/// Formats the sum of two numbers as string +fn sum_as_string(a: usize, b: usize) -> PyResult { + Ok((a + b).to_string()) +} + +/// This module is a python moudle implemented in Rust. +#[pymodinit] +fn rust_py(py: Python, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_function!(sum_as_string))?; + + Ok(()) +} +``` + +On windows and linux, you can build normally with `cargo build --release`. On Mac Os, you need to set additional linker arguments. One option is to compile with `cargo rustc --release -- -C link-arg=-undefined -C link-arg=dynamic_lookup`, the other is to create a `.cargo/config` with the following content: + +```toml +[target.x86_64-apple-darwin] +rustflags = [ + "-C", "link-arg=-undefined", + "-C", "link-arg=dynamic_lookup", +] +``` + +Also on macOS, you will need to rename the output from \*.dylib to \*.so. On Windows, you will need to rename the output from \*.dll to \*.pyd. + +[`setuptools-rust`](https://github.com/PyO3/setuptools-rust) can be used to generate a python package and includes the commands above by default. See [examples/word-count](examples/word-count) and the associated setup.py. + +### Using python from rust + +Add `pyo3` this to your `Cargo.toml`: ```toml [dependencies] @@ -46,72 +111,12 @@ fn main() -> PyResult<()> { } ``` -### As native module +## Examples and tooling -Pyo3 can be used to generate a python-compatible library. - -**`Cargo.toml`:** - -```toml -[package] -name = "rust2py" -version = "0.1.0" - -[lib] -name = "rust2py" -crate-type = ["cdylib"] - -[dependencies.pyo3] -version = "0.3" -features = ["extension-module"] -``` - -**`src/lib.rs`** - -```rust -#![feature(use_extern_macros, specialization)] - -extern crate pyo3; -use pyo3::prelude::*; - - - -// Add bindings to the generated python module -// N.B: names: "librust2py" must be the name of the `.so` or `.pyd` file -/// This module is implemented in Rust. -#[pymodinit] -fn rust2py(py: Python, m: &PyModule) -> PyResult<()> { - - #[pyfn(m, "sum_as_string")] - // ``#[pyfn()]` converts the arguments from Python objects to Rust values - // and the Rust return value back into a Python object. - fn sum_as_string_py(a:i64, b:i64) -> PyResult { - let out = sum_as_string(a, b); - Ok(out) - } - - Ok(()) -} - -// The logic can be implemented as a normal rust function -fn sum_as_string(a:i64, b:i64) -> String { - format!("{}", a + b).to_string() -} -``` - -On windows and linux, you can build normally with `cargo build --release`. On Mac Os, you need to set additional linker arguments. One option is to compile with `cargo rustc --release -- -C link-arg=-undefined -C link-arg=dynamic_lookup`, the other is to create a `.cargo/config` with the following content: - -```toml -[target.x86_64-apple-darwin] -rustflags = [ - "-C", "link-arg=-undefined", - "-C", "link-arg=dynamic_lookup", -] -``` - -Also on macOS, you will need to rename the output from \*.dylib to \*.so. On Windows, you will need to rename the output from \*.dll to \*.pyd. - -[`setuptools-rust`](https://github.com/PyO3/setuptools-rust) can be used to generate a python package and includes the commands above by default. See [examples/word-count](examples/word-count) and the associated setup.py. + * [examples/word-count](examples/word-count) _Counting the occurences of a word in a text file_ + * [hyperjson](https://github.com/mre/hyperjson) _A hyper-fast Python module for reading/writing JSON data using Rust's serde-json_ + * [rust-numpy](https://github.com/rust-numpy/rust-numpy) _Rust binding of NumPy C-API_ + * [pyo3-built](https://github.com/PyO3/pyo3-built) _Simple macro to expose metadata obtained with the [`built`](https://crates.io/crates/built) crate as a [`PyDict`](https://pyo3.github.io/pyo3/pyo3/struct.PyDict.html)_ ## License diff --git a/ci/travis/setup.sh b/ci/travis/setup.sh index 55bcb0ae..3ca989c9 100755 --- a/ci/travis/setup.sh +++ b/ci/travis/setup.sh @@ -20,25 +20,6 @@ curl -SsL "https://sh.rustup.rs/" | sh -s -- -y --default-toolchain=$TRAVIS_RUST export PATH=$PATH:$HOME/.cargo/bin -### Setup sccache ############################################################## - -echo -n "Fetching latest available 'sccache' version... " -INSTALLED=$(_installed sccache) -LATEST=$(_latest sccache) -echo "${LATEST} (installed: ${INSTALLED})" - -if [ "$INSTALLED" = "$LATEST" ]; then - echo "Using cached 'sccache'" -else - echo "Installing latest 'sccache' from mozilla/sccache" - URL="https://github.com/mozilla/sccache/releases/download/${LATEST}/sccache-${LATEST}-x86_64-unknown-linux-musl.tar.gz" - curl -SsL $URL | tar xzv -C /tmp - mv /tmp/sccache-${LATEST}-x86_64-unknown-linux-musl/sccache $HOME/.cargo/bin/sccache -fi - -mkdir -p $SCCACHE_DIR - - ### Setup kcov ################################################################# if [ ! -f "$HOME/.cargo/bin/kcov" ]; then diff --git a/examples/word-count-cls/Cargo.toml b/examples/word-count-cls/Cargo.toml deleted file mode 100644 index 266d5bd3..00000000 --- a/examples/word-count-cls/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -authors = ["Messense Lv "] -name = "word-count-cls" -version = "0.1.0" - -[dependencies] -rayon = "1.0" - -[dependencies.pyo3] -path = "../../" -features = ["extension-module"] - -[lib] -name = "word_count_cls" -crate-type = ["cdylib"] diff --git a/examples/word-count-cls/README.md b/examples/word-count-cls/README.md deleted file mode 100644 index d0c077bf..00000000 --- a/examples/word-count-cls/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# word-count - -## Build - -```bash -python setup.py install -``` - -## Usage - -```python -from word_count_cls import WordCounter - -WordCounter('path/to/file').search('word') -``` \ No newline at end of file diff --git a/examples/word-count-cls/setup.py b/examples/word-count-cls/setup.py deleted file mode 100644 index 4ad4bad1..00000000 --- a/examples/word-count-cls/setup.py +++ /dev/null @@ -1,54 +0,0 @@ -import sys - -from setuptools import setup -from setuptools.command.test import test as TestCommand - -try: - from setuptools_rust import RustExtension -except ImportError: - import subprocess - errno = subprocess.call([sys.executable, '-m', 'pip', 'install', 'setuptools-rust']) - if errno: - print("Please install setuptools-rust package") - raise SystemExit(errno) - else: - from setuptools_rust import RustExtension - - -class PyTest(TestCommand): - user_options = [] - - def run(self): - self.run_command("test_rust") - - import subprocess - import sys - errno = subprocess.call([sys.executable, '-m', 'pytest', 'tests']) - raise SystemExit(errno) - - -setup_requires = ['setuptools-rust>=0.10.1'] -install_requires = [] -tests_require = install_requires + ['pytest', 'pytest-benchmark'] - -setup( - name='word-count-cls', - version='0.1.0', - classifiers=[ - 'License :: OSI Approved :: MIT License', - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'Programming Language :: Python', - 'Programming Language :: Rust', - 'Operating System :: POSIX', - 'Operating System :: MacOS :: MacOS X', - ], - packages=['word_count_cls'], - rust_extensions=[RustExtension('word_count_cls.word_count_cls', 'Cargo.toml')], - install_requires=install_requires, - tests_require=tests_require, - setup_requires=setup_requires, - include_package_data=True, - zip_safe=False, - cmdclass=dict(test=PyTest) -) diff --git a/examples/word-count-cls/src/lib.rs b/examples/word-count-cls/src/lib.rs deleted file mode 100644 index 3fa0e53b..00000000 --- a/examples/word-count-cls/src/lib.rs +++ /dev/null @@ -1,86 +0,0 @@ -// Source adopted from -// https://github.com/tildeio/helix-website/blob/master/crates/word_count/src/lib.rs -#![feature(use_extern_macros, specialization)] - -extern crate pyo3; -extern crate rayon; - -use pyo3::prelude::*; -use rayon::prelude::*; -use std::fs::File; -use std::io::prelude::*; - -#[pyclass(dict)] -struct WordCounter { - path: String, - token: PyToken, -} - -#[pymethods] -impl WordCounter { - #[new] - fn __new__(obj: &PyRawObject, path: String) -> PyResult<()> { - obj.init(|t| WordCounter { path, token: t }) - } - - fn search(&self, py: Python, search: String) -> PyResult { - let mut file = File::open(self.path.as_str())?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - - let count = py.allow_threads(move || wc_parallel(&contents, &search)); - Ok(count) - } - - fn search_sequential(&self, search: String) -> PyResult { - let mut file = File::open(self.path.as_str())?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - Ok(wc_sequential(&contents, &search)) - } -} - -fn matches(word: &str, search: &str) -> bool { - let mut search = search.chars(); - for ch in word.chars().skip_while(|ch| !ch.is_alphabetic()) { - match search.next() { - None => { - return !ch.is_alphabetic(); - } - Some(expect) => { - if ch.to_lowercase().next() != Some(expect) { - return false; - } - } - } - } - return search.next().is_none(); -} - -fn wc_line(line: &str, search: &str) -> i32 { - let mut total = 0; - for word in line.split(' ') { - if matches(word, search) { - total += 1; - } - } - total -} - -fn wc_sequential(lines: &str, search: &str) -> i32 { - lines - .lines() - .map(|line| wc_line(line, search)) - .fold(0, |sum, line| sum + line) -} - -fn wc_parallel(lines: &str, search: &str) -> i32 { - lines.par_lines().map(|line| wc_line(line, search)).sum() -} - -#[pymodinit] -fn word_count_cls(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_class::()?; - - Ok(()) -} diff --git a/examples/word-count-cls/tests/test_word_count.py b/examples/word-count-cls/tests/test_word_count.py deleted file mode 100644 index cdd9413d..00000000 --- a/examples/word-count-cls/tests/test_word_count.py +++ /dev/null @@ -1,55 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -import os - -import pytest - -import word_count_cls - -current_dir = os.path.abspath(os.path.dirname(__file__)) -path = os.path.join(current_dir, 'zen-of-python.txt') - - -@pytest.fixture(scope='session', autouse=True) -def textfile(): - text = '''The Zen of Python, by Tim Peters - -Beautiful is better than ugly. -Explicit is better than implicit. -Simple is better than complex. -Complex is better than complicated. -Flat is better than nested. -Sparse is better than dense. -Readability counts. -Special cases aren't special enough to break the rules. -Although practicality beats purity. -Errors should never pass silently. -Unless explicitly silenced. -In the face of ambiguity, refuse the temptation to guess. -There should be one-- and preferably only one --obvious way to do it. -Although that way may not be obvious at first unless you're Dutch. -Now is better than never. -Although never is often better than *right* now. -If the implementation is hard to explain, it's a bad idea. -If the implementation is easy to explain, it may be a good idea. -Namespaces are one honking great idea -- let's do more of those!\n''' * 1000 - with open(path, 'w') as f: - f.write(text) - yield - os.remove(path) - - -def test_word_count_rust_parallel(benchmark): - count = benchmark(word_count_cls.WordCounter(path).search, 'is') - assert count == 10000 - - -def test_word_count_rust_sequential(benchmark): - count = benchmark(word_count_cls.WordCounter(path).search_sequential, 'is') - assert count == 10000 - - -def test_word_count_python_sequential(benchmark): - count = benchmark(word_count_cls.search_py, path, 'is') - assert count == 10000 diff --git a/examples/word-count-cls/word_count_cls/__init__.py b/examples/word-count-cls/word_count_cls/__init__.py deleted file mode 100644 index ed9beb8f..00000000 --- a/examples/word-count-cls/word_count_cls/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import - -from .word_count_cls import WordCounter - -__all__ = ['WordCounter', 'search_py'] - - -def search_py(path, needle): - total = 0 - with open(path, 'r') as f: - for line in f: - words = line.split(' ') - for word in words: - if word == needle: - total += 1 - return total diff --git a/examples/word-count/Cargo.toml b/examples/word-count/Cargo.toml index 4e7c8c13..ff23c8f8 100644 --- a/examples/word-count/Cargo.toml +++ b/examples/word-count/Cargo.toml @@ -5,9 +5,7 @@ version = "0.1.0" [dependencies] rayon = "1.0" - -[dependencies.pyo3] -path = "../../" +pyo3 = { path = "../..", features = ["extension-module"] } [lib] name = "word_count" diff --git a/examples/word-count/README.md b/examples/word-count/README.md index ecf5250d..0cd326fe 100644 --- a/examples/word-count/README.md +++ b/examples/word-count/README.md @@ -1,15 +1,27 @@ # word-count +Demonstrates searching for a file in plain python, with rust singlethreaded and with rust multithreaded. + ## Build -```bash +```shell python setup.py install ``` ## Usage ```python -from word_count import search +from word_count import search_py, WordCounter -search('path/to/file', 'word') +search_py("path/to/file", "word") +WordCounter("path/to/file").search("word") +WordCounter("path/to/file").search_sequential("word") ``` + +## Benchmark + +There is a benchmark in `tests/test_word_count.py`: + +```shell +pytest -v tests +``` \ No newline at end of file diff --git a/examples/word-count/setup.py b/examples/word-count/setup.py index 13f9bf27..9357bbfd 100644 --- a/examples/word-count/setup.py +++ b/examples/word-count/setup.py @@ -7,7 +7,8 @@ try: from setuptools_rust import RustExtension except ImportError: import subprocess - errno = subprocess.call([sys.executable, '-m', 'pip', 'install', 'setuptools-rust']) + + errno = subprocess.call([sys.executable, "-m", "pip", "install", "setuptools-rust"]) if errno: print("Please install setuptools-rust package") raise SystemExit(errno) @@ -23,32 +24,33 @@ class PyTest(TestCommand): import subprocess import sys - errno = subprocess.call([sys.executable, '-m', 'pytest', 'tests']) + + errno = subprocess.call([sys.executable, "-m", "pytest", "tests"]) raise SystemExit(errno) -setup_requires = ['setuptools-rust>=0.10.1'] +setup_requires = ["setuptools-rust>=0.10.1", "wheel"] install_requires = [] -tests_require = install_requires + ['pytest', 'pytest-benchmark'] +tests_require = install_requires + ["pytest", "pytest-benchmark"] setup( - name='word-count', - version='0.1.0', + name="word-count", + version="0.1.0", classifiers=[ - 'License :: OSI Approved :: MIT License', - 'Development Status :: 3 - Alpha', - 'Intended Audience :: Developers', - 'Programming Language :: Python', - 'Programming Language :: Rust', - 'Operating System :: POSIX', - 'Operating System :: MacOS :: MacOS X', + "License :: OSI Approved :: MIT License", + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python", + "Programming Language :: Rust", + "Operating System :: POSIX", + "Operating System :: MacOS :: MacOS X", ], - packages=['word_count'], - rust_extensions=[RustExtension('word_count.word_count', 'Cargo.toml')], + packages=["word_count"], + rust_extensions=[RustExtension("word_count.word_count", "Cargo.toml")], install_requires=install_requires, tests_require=tests_require, setup_requires=setup_requires, include_package_data=True, zip_safe=False, - cmdclass=dict(test=PyTest) + cmdclass=dict(test=PyTest), ) diff --git a/examples/word-count/src/lib.rs b/examples/word-count/src/lib.rs index bafd8894..41810b04 100644 --- a/examples/word-count/src/lib.rs +++ b/examples/word-count/src/lib.rs @@ -1,19 +1,59 @@ // Source adopted from // https://github.com/tildeio/helix-website/blob/master/crates/word_count/src/lib.rs #![feature(use_extern_macros, specialization)] + +#[macro_use] extern crate pyo3; extern crate rayon; -use std::fs::File; -use std::io::prelude::*; - use pyo3::prelude::*; use rayon::prelude::*; +use std::fs; +use std::path::PathBuf; -fn matches(word: &str, search: &str) -> bool { - let mut search = search.chars(); +#[pyclass] +/// Represents a file that can be searched +struct WordCounter { + path: PathBuf, +} + +#[pymethods] +impl WordCounter { + #[new] + fn __new__(obj: &PyRawObject, path: String) -> PyResult<()> { + obj.init(|_| WordCounter { path: PathBuf::from(path) }) + } + + /// Searches for the word, parallelized by rayon + fn search(&self, py: Python, search: String) -> PyResult { + let contents = fs::read_to_string(&self.path)?; + + let count = py.allow_threads(move || { + contents + .par_lines() + .map(|line| count_line(line, &search)) + .sum() + }); + Ok(count) + } + + /// Searches for a word in a classic sequential fashion + fn search_sequential(&self, needle: String) -> PyResult { + let contents = fs::read_to_string(&self.path)?; + + let result = contents + .lines() + .map(|line| count_line(line, &needle)) + .sum(); + + Ok(result) + } +} + +fn matches(word: &str, needle: &str) -> bool { + let mut needle = needle.chars(); for ch in word.chars().skip_while(|ch| !ch.is_alphabetic()) { - match search.next() { + match needle.next() { None => { return !ch.is_alphabetic(); } @@ -24,49 +64,25 @@ fn matches(word: &str, search: &str) -> bool { } } } - return search.next().is_none(); + return needle.next().is_none(); } -fn wc_line(line: &str, search: &str) -> i32 { +#[pyfunction] +/// Count the occurences of needle in line, case insensitive +fn count_line(line: &str, needle: &str) -> usize { let mut total = 0; for word in line.split(' ') { - if matches(word, search) { + if matches(word, needle) { total += 1; } } total } -fn wc_sequential(lines: &str, search: &str) -> i32 { - lines - .lines() - .map(|line| wc_line(line, search)) - .fold(0, |sum, line| sum + line) -} - -fn wc_parallel(lines: &str, search: &str) -> i32 { - lines.par_lines().map(|line| wc_line(line, search)).sum() -} - #[pymodinit] fn word_count(_py: Python, m: &PyModule) -> PyResult<()> { - #[pyfn(m, "search")] - fn search(py: Python, path: String, search: String) -> PyResult { - let mut file = File::open(path)?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - - let count = py.allow_threads(move || wc_parallel(&contents, &search)); - Ok(count) - } - - #[pyfn(m, "search_sequential")] - fn search_sequential(path: String, search: String) -> PyResult { - let mut file = File::open(path)?; - let mut contents = String::new(); - file.read_to_string(&mut contents)?; - Ok(wc_sequential(&contents, &search)) - } + m.add_function(wrap_function!(count_line))?; + m.add_class::()?; Ok(()) } diff --git a/examples/word-count/tests/test_word_count.py b/examples/word-count/tests/test_word_count.py index 3ed33bb3..523215a4 100644 --- a/examples/word-count/tests/test_word_count.py +++ b/examples/word-count/tests/test_word_count.py @@ -4,16 +4,16 @@ from __future__ import absolute_import import os import pytest - import word_count current_dir = os.path.abspath(os.path.dirname(__file__)) -path = os.path.join(current_dir, 'zen-of-python.txt') +path = os.path.join(current_dir, "zen-of-python.txt") -@pytest.fixture(scope='session', autouse=True) +@pytest.fixture(scope="session", autouse=True) def textfile(): - text = '''The Zen of Python, by Tim Peters + text = """ +The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. @@ -33,23 +33,25 @@ Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. -Namespaces are one honking great idea -- let's do more of those!\n''' * 1000 - with open(path, 'w') as f: - f.write(text) +Namespaces are one honking great idea -- let's do more of those! +""" + + with open(path, "w") as f: + f.write(text * 1000) yield os.remove(path) def test_word_count_rust_parallel(benchmark): - count = benchmark(word_count.search, path, 'is') + count = benchmark(word_count.WordCounter(path).search, "is") assert count == 10000 def test_word_count_rust_sequential(benchmark): - count = benchmark(word_count.search_sequential, path, 'is') + count = benchmark(word_count.WordCounter(path).search_sequential, "is") assert count == 10000 def test_word_count_python_sequential(benchmark): - count = benchmark(word_count.search_py, path, 'is') + count = benchmark(word_count.search_py, path, "is") assert count == 10000 diff --git a/examples/word-count/word_count/__init__.py b/examples/word-count/word_count/__init__.py index ce843be6..2d00b18f 100644 --- a/examples/word-count/word_count/__init__.py +++ b/examples/word-count/word_count/__init__.py @@ -1,16 +1,16 @@ # -*- coding: utf-8 -*- from __future__ import absolute_import -from .word_count import search, search_sequential +from .word_count import WordCounter, count_line -__all__ = ['search', 'search_sequential', 'search_py'] +__all__ = ["WordCounter", "count_line", "search_py"] def search_py(path, needle): total = 0 - with open(path, 'r') as f: + with open(path, "r") as f: for line in f: - words = line.split(' ') + words = line.split(" ") for word in words: if word == needle: total += 1 diff --git a/pyo3-derive-backend/src/args.rs b/pyo3-derive-backend/src/args.rs index 3522cb73..2da82b4f 100644 --- a/pyo3-derive-backend/src/args.rs +++ b/pyo3-derive-backend/src/args.rs @@ -180,7 +180,7 @@ mod test { use args::{parse_arguments, Argument}; use syn; - use proc_macro::TokenStream; + use proc_macro2::TokenStream; fn items(s: TokenStream) -> Vec { let dummy: syn::ItemFn = parse_quote!{#s fn dummy() {}}; diff --git a/src/callback.rs b/src/callback.rs index 62d0f4a4..6a9c5fe1 100644 --- a/src/callback.rs +++ b/src/callback.rs @@ -139,7 +139,7 @@ impl CallbackConverter for HashConverter } #[inline] -#[cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))] + pub unsafe fn cb_convert(_c: C, py: Python, value: PyResult) -> C::R where C: CallbackConverter { diff --git a/src/class/buffer.rs b/src/class/buffer.rs index c74aaf19..911840c4 100644 --- a/src/class/buffer.rs +++ b/src/class/buffer.rs @@ -48,7 +48,7 @@ impl PyBufferProtocolImpl for T { impl<'p, T> PyBufferProtocolImpl for T where T: PyBufferProtocol<'p> { #[inline] - #[cfg_attr(feature = "cargo-clippy", allow(needless_update))] + fn tp_as_buffer() -> Option { Some(ffi::PyBufferProcs{ bf_getbuffer: Self::cb_bf_getbuffer(), diff --git a/src/instance.rs b/src/instance.rs index 0e00efc4..7ca1ba6f 100644 --- a/src/instance.rs +++ b/src/instance.rs @@ -23,7 +23,7 @@ impl PyToken { } #[inline(always)] - #[cfg_attr(feature = "cargo-clippy", allow(inline_always))] + pub fn py(&self) -> Python { unsafe { Python::assume_gil_acquired() } } @@ -45,7 +45,7 @@ pub trait AsPyRef: Sized { fn as_ref(&self, py: Python) -> &T; /// Return mutable reference to object. - #[cfg_attr(feature = "cargo-clippy", allow(mut_from_ref))] + fn as_mut(&self, py: Python) -> &mut T; /// Acquire python gil and call closure with object reference. diff --git a/src/objectprotocol.rs b/src/objectprotocol.rs index 4b482a9b..7ee5f2ce 100644 --- a/src/objectprotocol.rs +++ b/src/objectprotocol.rs @@ -16,7 +16,7 @@ use typeob::PyTypeInfo; /// Python object model helper methods -#[cfg_attr(feature = "cargo-clippy", allow(len_without_is_empty))] + pub trait ObjectProtocol { /// Determines whether this object has the given attribute. @@ -152,7 +152,7 @@ pub trait ObjectProtocol { fn get_base(&self) -> &::BaseType where Self: PyTypeInfo; /// Gets the Python base object for this object. - #[cfg_attr(feature = "cargo-clippy", allow(mut_from_ref))] + fn get_mut_base(&self) -> &mut ::BaseType where Self: PyTypeInfo; /// Casts the PyObject to a concrete Python object type. diff --git a/src/objects/bytearray.rs b/src/objects/bytearray.rs index 235dc0f3..c4a2dfc9 100644 --- a/src/objects/bytearray.rs +++ b/src/objects/bytearray.rs @@ -54,7 +54,7 @@ impl PyByteArray { } /// Gets the Python bytearray data as byte slice. - #[cfg_attr(feature = "cargo-clippy", allow(mut_from_ref))] + pub fn data(&self) -> &mut [u8] { unsafe { let buffer = ffi::PyByteArray_AsString(self.0.as_ptr()) as *mut u8; diff --git a/src/objects/exc.rs b/src/objects/exc.rs index c5cc2dce..4ef10b12 100644 --- a/src/objects/exc.rs +++ b/src/objects/exc.rs @@ -29,7 +29,7 @@ macro_rules! exc_type( } } impl $name { - #[cfg_attr(feature = "cargo-clippy", allow(new_ret_no_self))] + pub fn new(args: V) -> PyErr { PyErr::new::<$name, V>(args) } @@ -133,7 +133,7 @@ exc_type!(WindowsError, PyExc_WindowsError); impl UnicodeDecodeError { - #[cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))] + pub fn new_err<'p>(py: Python<'p>, encoding: &CStr, input: &[u8], range: ops::Range, reason: &CStr) -> PyResult<&'p PyObjectRef> { unsafe { diff --git a/src/objects/exc_impl.rs b/src/objects/exc_impl.rs index 2b9e422c..8a05bd45 100644 --- a/src/objects/exc_impl.rs +++ b/src/objects/exc_impl.rs @@ -57,7 +57,7 @@ macro_rules! import_exception { } impl $name { - #[cfg_attr(feature = "cargo-clippy", allow(new_ret_no_self))] + pub fn new(args: T) -> $crate::PyErr where Self: $crate::typeob::PyTypeObject + Sized { diff --git a/src/objects/floatob.rs b/src/objects/floatob.rs index 69027c5a..5e2b540f 100644 --- a/src/objects/floatob.rs +++ b/src/objects/floatob.rs @@ -51,7 +51,7 @@ impl IntoPyObject for f64 { pyobject_extract!(obj to f64 => { let v = unsafe { ffi::PyFloat_AsDouble(obj.as_ptr()) }; - #[cfg_attr(feature = "cargo-clippy", allow(float_cmp))] + { if v == -1.0 && PyErr::occurred(obj.py()) { Err(PyErr::fetch(obj.py())) @@ -62,13 +62,13 @@ pyobject_extract!(obj to f64 => { }); impl ToPyObject for f32 { - #[cfg_attr(feature = "cargo-clippy", allow(cast_lossless))] + fn to_object(&self, py: Python) -> PyObject { PyFloat::new(py, *self as f64).into() } } impl IntoPyObject for f32 { - #[cfg_attr(feature = "cargo-clippy", allow(cast_lossless))] + fn into_object(self, py: Python) -> PyObject { PyFloat::new(py, self as f64).into() } diff --git a/src/objects/mod.rs b/src/objects/mod.rs index cffe3c55..ab68afeb 100644 --- a/src/objects/mod.rs +++ b/src/objects/mod.rs @@ -39,7 +39,6 @@ macro_rules! pyobject_downcast( impl<'a, $($type_param,)*> $crate::FromPyObject<'a> for &'a $name { /// Extracts `Self` from the source `PyObject`. - #[cfg_attr(feature = "cargo-clippy", allow(useless_transmute))] fn extract(ob: &'a $crate::PyObjectRef) -> $crate::PyResult { unsafe { @@ -60,7 +59,6 @@ macro_rules! pyobject_native_type_named( impl<$($type_param,)*> $crate::PyNativeType for $name {} impl<$($type_param,)*> ::std::convert::AsRef<$crate::PyObjectRef> for $name { - #[cfg_attr(feature = "cargo-clippy", allow(useless_transmute))] fn as_ref(&self) -> &$crate::PyObjectRef { unsafe{&*(self as *const $name as *const $crate::PyObjectRef)} } @@ -121,7 +119,7 @@ macro_rules! pyobject_native_type_convert( &mut $typeobject } - #[cfg_attr(feature = "cargo-clippy", allow(not_unsafe_ptr_arg_deref))] + fn is_instance(ptr: *mut $crate::ffi::PyObject) -> bool { #[allow(unused_unsafe)] unsafe { $checkfunction(ptr) > 0 } diff --git a/src/objects/num3.rs b/src/objects/num3.rs index f9033b96..6ee8a579 100644 --- a/src/objects/num3.rs +++ b/src/objects/num3.rs @@ -29,7 +29,7 @@ pyobject_native_type!(PyLong, ffi::PyLong_Type, ffi::PyLong_Check); macro_rules! int_fits_c_long( ($rust_type:ty) => ( impl ToPyObject for $rust_type { - #[cfg_attr(feature = "cargo-clippy", allow(cast_lossless))] + fn to_object(&self, py: Python) -> PyObject { unsafe { PyObject::from_owned_ptr_or_panic(py, ffi::PyLong_FromLong(*self as c_long)) @@ -37,7 +37,7 @@ macro_rules! int_fits_c_long( } } impl IntoPyObject for $rust_type { - #[cfg_attr(feature = "cargo-clippy", allow(cast_lossless))] + fn into_object(self, py: Python) -> PyObject { unsafe { PyObject::from_owned_ptr_or_panic(py, ffi::PyLong_FromLong(self as c_long)) diff --git a/src/objects/num_common.rs b/src/objects/num_common.rs index e75df4d8..bf685d70 100644 --- a/src/objects/num_common.rs +++ b/src/objects/num_common.rs @@ -4,7 +4,7 @@ use std::os::raw::c_int; use python::Python; use err::{PyErr, PyResult}; -#[cfg_attr(feature = "cargo-clippy", allow(needless_pass_by_value))] + pub(super) fn err_if_invalid_value (py: Python, invalid_value: T, actual_value: T) -> PyResult { diff --git a/src/objects/sequence.rs b/src/objects/sequence.rs index 61b508f6..48adbaf6 100644 --- a/src/objects/sequence.rs +++ b/src/objects/sequence.rs @@ -20,7 +20,7 @@ pyobject_native_type_named!(PySequence); pyobject_downcast!(PySequence, ffi::PySequence_Check); -#[cfg_attr(feature = "cargo-clippy", allow(len_without_is_empty))] + impl PySequence { /// Returns the number of objects in sequence. This is equivalent to Python `len()`. #[inline] diff --git a/src/prelude.rs b/src/prelude.rs index 78a68175..5f936c32 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -30,7 +30,3 @@ pub use pyo3cls::mod3init as pymodinit; #[cfg(not(Py_3))] pub use pyo3cls::mod2init as pymodinit; - -// Until the extern macro story is fully fleshed out, this will make wrap_function! work -//#[doc(hidden)] -//pub use mashup::*; diff --git a/src/python.rs b/src/python.rs index 734e6f9d..92d92da2 100644 --- a/src/python.rs +++ b/src/python.rs @@ -275,7 +275,7 @@ impl<'p> Python<'p> { } /// Register `ffi::PyObject` pointer in release pool - #[cfg_attr(feature = "cargo-clippy", allow(wrong_self_convention))] + pub unsafe fn from_borrowed_ptr_to_obj(self, ptr: *mut ffi::PyObject) -> &'p PyObjectRef { if ptr.is_null() { @@ -287,7 +287,7 @@ impl<'p> Python<'p> { /// Register `ffi::PyObject` pointer in release pool, /// and do unchecked downcast to specific type. - #[cfg_attr(feature = "cargo-clippy", allow(wrong_self_convention))] + pub unsafe fn from_owned_ptr(self, ptr: *mut ffi::PyObject) -> &'p T where T: PyTypeInfo { @@ -315,7 +315,7 @@ impl<'p> Python<'p> { /// Register owned `ffi::PyObject` pointer in release pool. /// Returns `Err(PyErr)` if the pointer is `null`. /// do unchecked downcast to specific type. - #[cfg_attr(feature = "cargo-clippy", allow(wrong_self_convention))] + pub unsafe fn from_owned_ptr_or_err(self, ptr: *mut ffi::PyObject) -> PyResult<&'p T> where T: PyTypeInfo { @@ -330,7 +330,7 @@ impl<'p> Python<'p> { /// Register owned `ffi::PyObject` pointer in release pool. /// Returns `None` if the pointer is `null`. /// do unchecked downcast to specific type. - #[cfg_attr(feature = "cargo-clippy", allow(wrong_self_convention))] + pub unsafe fn from_owned_ptr_or_opt(self, ptr: *mut ffi::PyObject) -> Option<&'p T> where T: PyTypeInfo { @@ -345,7 +345,7 @@ impl<'p> Python<'p> { /// Register borrowed `ffi::PyObject` pointer in release pool. /// Panics if the pointer is `null`. /// do unchecked downcast to specific type. - #[cfg_attr(feature = "cargo-clippy", allow(wrong_self_convention))] + pub unsafe fn from_borrowed_ptr(self, ptr: *mut ffi::PyObject) -> &'p T where T: PyTypeInfo { @@ -370,7 +370,7 @@ impl<'p> Python<'p> { /// Register borrowed `ffi::PyObject` pointer in release pool. /// Returns `Err(PyErr)` if the pointer is `null`. /// do unchecked downcast to specific type. - #[cfg_attr(feature = "cargo-clippy", allow(wrong_self_convention))] + pub unsafe fn from_borrowed_ptr_or_err(self, ptr: *mut ffi::PyObject) -> PyResult<&'p T> where T: PyTypeInfo { @@ -385,7 +385,7 @@ impl<'p> Python<'p> { /// Register borrowed `ffi::PyObject` pointer in release pool. /// Returns `None` if the pointer is `null`. /// do unchecked downcast to specific `T`. - #[cfg_attr(feature = "cargo-clippy", allow(wrong_self_convention))] + pub unsafe fn from_borrowed_ptr_or_opt(self, ptr: *mut ffi::PyObject) -> Option<&'p T> where T: PyTypeInfo { @@ -418,7 +418,7 @@ impl<'p> Python<'p> { /// Release `ffi::PyObject` pointer. /// Undefined behavior if the pointer is invalid. #[inline] - #[cfg_attr(feature = "cargo-clippy", allow(not_unsafe_ptr_arg_deref))] + pub fn xdecref(self, ptr: *mut ffi::PyObject) { if !ptr.is_null() { unsafe {ffi::Py_DECREF(ptr)}; diff --git a/src/typeob.rs b/src/typeob.rs index 6b7d3a97..4d2cae15 100644 --- a/src/typeob.rs +++ b/src/typeob.rs @@ -42,13 +42,13 @@ pub trait PyTypeInfo { unsafe fn type_object() -> &'static mut ffi::PyTypeObject; /// Check if `*mut ffi::PyObject` is instance of this type - #[cfg_attr(feature = "cargo-clippy", allow(not_unsafe_ptr_arg_deref))] + fn is_instance(ptr: *mut ffi::PyObject) -> bool { unsafe {ffi::PyObject_TypeCheck(ptr, Self::type_object()) != 0} } /// Check if `*mut ffi::PyObject` is exact instance of this type - #[cfg_attr(feature = "cargo-clippy", allow(not_unsafe_ptr_arg_deref))] + fn is_exact_instance(ptr: *mut ffi::PyObject) -> bool { unsafe { (*ptr).ob_type == Self::type_object() @@ -186,7 +186,7 @@ impl PyRawObject { } /// Return reference to object. - #[cfg_attr(feature = "cargo-clippy", allow(should_implement_trait))] + pub fn as_ref(&self) -> &T { // TODO: check is object initialized unsafe { @@ -204,7 +204,7 @@ impl IntoPyPointer for PyRawObject { } impl PyObjectWithToken for PyRawObject { - #[cfg_attr(feature = "cargo-clippy", allow(inline_always))] + #[inline(always)] fn py(&self) -> Python { unsafe { Python::assume_gil_acquired() } @@ -338,7 +338,7 @@ impl PyTypeObject for T where T: PyObjectAlloc + PyTypeInfo { let py = gil.python(); initialize_type::(py, None).expect( - format!("An error occurred while initializing class {}", T::NAME).as_ref()); + &format!("An error occurred while initializing class {}", T::NAME)); } } } @@ -528,7 +528,7 @@ fn py_class_flags(type_object: &mut ffi::PyTypeObject) { } } -#[cfg_attr(feature = "cargo-clippy", allow(type_complexity))] + fn py_class_method_defs() -> PyResult<(Option, Option, Option, diff --git a/tests/test_module.rs b/tests/test_module.rs index 7c666f26..e3f01d28 100644 --- a/tests/test_module.rs +++ b/tests/test_module.rs @@ -13,6 +13,7 @@ fn sum_as_string(a: i64, b: i64) -> String { } #[pyfunction] +/// Doubles the given value fn double(x: usize) -> usize { x * 2 } @@ -60,7 +61,9 @@ fn test_module_with_functions() { run("assert module_with_functions.foo == 'bar'"); run("assert module_with_functions.EmptyClass != None"); run("assert module_with_functions.double(3) == 6"); + run("assert module_with_functions.double.__doc__ == 'Doubles the given value'"); run("assert module_with_functions.also_double(3) == 6"); + run("assert module_with_functions.also_double.__doc__ == 'Doubles the given value'"); } #[pymodinit(other_name)]