diff --git a/.github/workflows/rust_coverage.yaml b/.github/workflows/rust_coverage.yaml new file mode 100644 index 0000000..c5a5f9e --- /dev/null +++ b/.github/workflows/rust_coverage.yaml @@ -0,0 +1,23 @@ +name: RUST code coverage + +on: [pull_request, push] + +jobs: + coverage: + runs-on: ubuntu-latest + env: + CARGO_TERM_COLOR: always + steps: + - uses: actions/checkout@v3 + - name: Install Rust + run: rustup update stable + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@cargo-llvm-cov + - name: Generate code coverage + run: cargo llvm-cov --all-features --workspace --codecov --output-path codecov.json + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + with: + token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos + files: codecov.json + fail_ci_if_error: true diff --git a/.gitignore b/.gitignore index 2dc53ca..deb74ca 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,4 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. .idea/ +.vscode/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1a16e09 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "iotext" +authors = ["Marcin Bielak "] +version = "0.1.0" +rust-version = "1.57.0" +edition = "2021" + +[lib] +name = "iotext" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { version = "0.19.2", features = ["extension-module"] } +iotext_rs = { git = "https://github.com/bieli/IoText-rs.git" } +rayon = "1.0.2" + +#[lib] +#name = "_iotext" +#crate-type = ["cdylib"] + +#pyo3 = { version = "0.19.2", features = ["auto-initialize", "extension-module"] } +#iotext_rs = { git = "https://github.com/bieli/IoText-rs.git" } +# dependencies +#fancy-regex = "0.11.0" +#regex = "1.9.4" +#rustc-hash = "1.1.0" +#bstr = "1.6.1" + +#[profile.release] +#incremental = true diff --git a/iotext/__init__.py b/iotext/__init__.py new file mode 100644 index 0000000..29d5e75 --- /dev/null +++ b/iotext/__init__.py @@ -0,0 +1,15 @@ +from .iotext import search, decode, return_myclass, \ + MyClass, return_myiotextclass, MyIoTextClass, length, \ + Person, give_me_a_person + +__all__ = [ + "search", + "decode", + "return_myclass", + "MyClass", + "return_myiotextclass", + "MyIoTextClass", + "length", + "Person", + "give_me_a_person" +] diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 0000000..39230d2 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,18 @@ +import nox + +nox.options.sessions = ["test"] + + +@nox.session +def test(session): + session.install("-rrequirements-dev.txt") + session.install("maturin") + session.run_always("maturin", "develop") + session.run("pytest") + + +@nox.session +def bench(session): + session.install("-rrequirements-dev.txt") + session.install(".") + session.run("pytest", "--benchmark-enable") diff --git a/requirements-dev.txt b/requirements-dev.txt index 168cb73..21d2f40 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,10 @@ +nox mypy black==22.3.0 parameterized==0.9.0 pytest-benchmark==4.0.0 aiohttp==3.8.5 -pytest-aiohttp==0.3.0 \ No newline at end of file +pytest-aiohttp==0.3.0 + +# required for RUST development +maturin==1.2.1 diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d60fe98 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,203 @@ +// This check is new and seems buggy (possibly with PyO3 interaction) +#![allow(clippy::borrow_deref_ref)] + +extern crate pyo3; + +use std::collections::{HashSet, hash_map, HashMap}; + +use iotext_rs::IoTextDataRow; +// use iotext_rs::IoTextData; +// use iotext_rs::IoTextDataRow; +use pyo3::PyResult; +use pyo3::exceptions::PyTypeError; +// use pyo3::PyErr; +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyList, PyTuple, IntoPyDict}; +use pyo3::wrap_pyfunction; +use rayon::prelude::*; + + +#[pyfunction] +fn length(py: Python, obj: PyObject) -> PyResult { + if let Ok(s) = obj.extract::(py) { + return Ok(s.len().to_object(py)); + } + if let Ok(s) = obj.extract::>(py) { + return Ok(s.len().to_object(py)); + } + Err(PyTypeError::new_err("Not Supported")) +} + + +/// Searches for the word, parallelized by rayon +#[pyfunction] +fn search(contents: &str, needle: &str) -> usize { + contents + .par_lines() + .map(|line| count_line(line, needle)) + .sum() +} + +#[pyfunction] +fn decode(py: Python, obj: PyObject) -> PyResult { + if let Ok(s) = obj.extract::(py) { + return Ok(s.len().to_object(py)); + } + Err(PyTypeError::new_err("Not Supported")) +// let data_obj: IoTextDataRow = IoTextDataRow::default(); +// return PyObject(data_obj.parse_iotext_str(iot_ext_data_row)).into_py() +} + +//#[pyfunction] +//fn decode() -> PyResult { +// Ok(IoTextDataRow::default()) +//} + +//#[pyfunction] +//fn decode2() -> PyResult> { +// // let gil = Python::acquire_gil(); +// // let py = gil.python(); +// +// Py::new(py, IoTextDataRow::default()).into() +//} + +#[pyclass] +struct Nonzero { + value: i32, +} + +//#[pymethods] +//impl Nonzero { +// #[new] +// fn py_new(value: i32) -> PyResult { +// if value == 0 { +// Err(PyErr::new("cannot be zero")) +// } else { +// Ok(Nonzero { value: value }) +// } +// } +//} + +#[pyclass] +struct MyClass { + num: i32, +} + +#[pyfunction] +fn return_myclass() -> Py { + Python::with_gil(|py| Py::new(py, MyClass { num: 1 }).unwrap()) +} + + +#[pyclass] +#[derive(Default)] +struct MyIoTextClass { + value: IoTextDataRow, +} + +#[pymethods] +impl MyIoTextClass { + #[new] + fn new() -> Self { + MyIoTextClass { value: IoTextDataRow::default() } + } + + pub fn example_list(&mut self, py: Python) -> PyResult { + //let l: &PyList = PyList::empty(py); + let elements: Vec<&str> = vec!["a", "b", "c"]; + let l: &PyList = PyList::new(py, elements); + Ok(l.into()) + } + + //pub fn example_dict_1(&mut self, py: Python) -> PyResult { + //let l: &PyList = PyList::empty(py); + //let elements: HashMap<&str, &str> = (0..10).map(|i| (i.to_string(), i.to_string())).collect(); + //let l: &PyDict = PyDict::new(py); + // let key_vals: Vec<(&str, PyObject)> = vec![ + // ("num", 8.to_object(py)), ("str", "asd".to_object(py)) + // ]; + //let dict = key_vals.into_py_dict(py); + + // Ok(key_vals.into_py_dict(py)) + //} + + /// Formats the sum of two numbers as string. + pub fn get_result(&mut self, py: Python) -> PyResult> { + let mut result = HashMap::new(); + result.insert("name".to_string(), "kushal".to_string()); + result.insert("age".to_string(), "36".to_string()); + Ok(result) + } + + + //fn method1() -> PyResult<&PyDict> { + //} + + //#[getter] + //fn value(&self) -> PyResult { + // Ok(self.value) + //} +} + +#[pyfunction] +fn return_myiotextclass() -> Py { + Python::with_gil(|py| Py::new(py, MyIoTextClass { value: IoTextDataRow::default() }).unwrap()) +} + + + + + + +/// Count the occurrences of needle in line, case insensitive +fn count_line(line: &str, needle: &str) -> usize { + let mut total = 0; + for word in line.split(' ') { + if word == needle { + total += 1; + } + } + total +} + +#[pyfunction] +// Returns a Person class, takes a dict with {"name": "age", "age": 100} format. +fn give_me_a_person(data: &PyDict) -> PyResult { + let name: String = data.get_item("name").unwrap().extract().unwrap(); + let age: i64 = data.get_item("age").unwrap().extract().unwrap(); + + let p: Person = Person::new(name, age); + Ok(p) +} + +#[pyclass] +#[derive(Debug)] +struct Person { + #[pyo3(get, set)] + name: String, + #[pyo3(get, set)] + age: i64, +} + +#[pymethods] +impl Person { + #[new] + fn new(name: String, age: i64) -> Self { + Person { name, age } + } +} + +#[pymodule] +fn iotext(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(search, m)?)?; + m.add_function(wrap_pyfunction!(return_myclass, m)?)?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(return_myiotextclass, m)?)?; + m.add_class::()?; + m.add_wrapped(wrap_pyfunction!(length))?; + m.add_wrapped(wrap_pyfunction!(decode))?; + m.add_wrapped(wrap_pyfunction!(give_me_a_person))?; + m.add_class::()?; + + Ok(()) +} \ No newline at end of file