From db02515c91912083ceb7ba192c16cdb29fbca923 Mon Sep 17 00:00:00 2001 From: Konstantin Gukov Date: Mon, 20 Jul 2020 18:22:10 +0200 Subject: [PATCH] Version 1.1.0. - Update chrono and PyO3 to the latest versions - Rewrite the library in stable Rust - Update manylinux to 2010 --- .dockerignore | 3 +- .gitignore | 1 + Cargo.toml | 4 +- build_osx_wheels.sh | 13 ++--- dist/manylinux/Dockerfile | 9 ++-- dist/manylinux/build_wheels.sh | 25 +++++---- dtparse/__init__.py | 7 +-- setup.py | 6 +-- src/lib.rs | 94 +++++++++++----------------------- tests/test_performance.py | 2 +- 10 files changed, 67 insertions(+), 97 deletions(-) diff --git a/.dockerignore b/.dockerignore index 04617b4..04772d1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,4 +11,5 @@ Cargo.lock projectFilesBackup/ tests.svg benchmark*.svg -build/ \ No newline at end of file +build/ +venv/ diff --git a/.gitignore b/.gitignore index 505e66f..9cefa53 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ projectFilesBackup/ benchmark*.svg build/ *py[cdo] +venv/ diff --git a/Cargo.toml b/Cargo.toml index a09a1f2..c038b1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,8 @@ name = "python-dtparse" version = "1.0.0" [dependencies] -pyo3 = "0.2.6" -chrono = "0.4.2" +pyo3 = "0.11.1" +chrono = "0.4.13" [lib] name = "dtparse" diff --git a/build_osx_wheels.sh b/build_osx_wheels.sh index 4c9778d..f9214f2 100755 --- a/build_osx_wheels.sh +++ b/build_osx_wheels.sh @@ -4,13 +4,14 @@ set -e eval "$(pyenv init -)" # This is an example of how the wheels can be built locally on OSX. -# It expects pyenv to be installed and the following virtualenvs to be created: -# pyenv virtualenv 2.7.14 rust2.7 -# pyenv virtualenv 3.5.3 rust3.5 -# pyenv virtualenv 3.6.3 rust3.6 -# pyenv virtualenv 3.7-dev rust3.7 +# It expects pyenv to be installed and the following virtualenvs to be created, e.g.: +# pyenv virtualenv 3.5.9 rust3.5 +# pyenv virtualenv 3.6.10 rust3.6 +# pyenv virtualenv 3.7.7 rust3.7 +# pyenv virtualenv 3.8.2 rust3.8 +# pyenv virtualenv 3.9-dev rust3.9 -for venv in rust2.7 rust3.5 rust3.6 rust3.7; do +for venv in rust3.5 rust3.6 rust3.7 rust3.8 rust3.9; do pyenv activate "$venv" pip install -U pip setuptools setuptools-rust wheel delocate pip wheel . -w ./dist/wheels/ diff --git a/dist/manylinux/Dockerfile b/dist/manylinux/Dockerfile index 6e57d98..8eea857 100644 --- a/dist/manylinux/Dockerfile +++ b/dist/manylinux/Dockerfile @@ -1,8 +1,11 @@ -FROM quay.io/pypa/manylinux1_x86_64 +FROM quay.io/pypa/manylinux2010_x86_64 RUN mkdir ~/rust-installer -RUN curl -sL https://static.rust-lang.org/rustup.sh -o ~/rust-installer/rustup.sh -RUN sh ~/rust-installer/rustup.sh --prefix=~/rust --channel=nightly -y --disable-sudo + +RUN curl https://sh.rustup.rs -sSf | bash -s -- -y +RUN echo "source $HOME/.cargo/env" >> "$HOME/.bashrc" +RUN echo "source $HOME/.cargo/env" >> "$HOME/.bash_profile" + COPY . /app ENTRYPOINT /app/dist/manylinux/build_wheels.sh diff --git a/dist/manylinux/build_wheels.sh b/dist/manylinux/build_wheels.sh index 5559535..1ef0489 100755 --- a/dist/manylinux/build_wheels.sh +++ b/dist/manylinux/build_wheels.sh @@ -1,26 +1,29 @@ #!/bin/sh set -e -x -export PATH="$HOME/rust/bin:$PATH" +export PATH="$HOME/.cargo/bin:$PATH" export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$HOME/rust/lib" -mkdir /tmp/wheels +WHEELS_TMP_DIR=/tmp/wheels + # Compile wheels -for PYBIN in /opt/python/cp{27,35,36}*/bin; do +for PYBIN in /opt/python/cp{35,36,37,38,39}*/bin; do export PYTHON_SYS_EXECUTABLE="$PYBIN/python" export PYTHON_LIB=$("${PYBIN}/python" -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR'))") export LIBRARY_PATH="$LIBRARY_PATH:$PYTHON_LIB" export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$PYTHON_LIB" "${PYBIN}/pip" install -U setuptools setuptools-rust wheel - "${PYBIN}/pip" wheel /app/ -w /tmp/wheels -done -# `auditwheel repair` copies the external shared libraries into the wheel itself -# and automatically modifies the appropriate RPATH entries such that these libraries -# will be picked up at runtime. This accomplishes a similar result as if the libraries -# had been statically linked without requiring changes to the build system. -for whl in /tmp/wheels/*; do - auditwheel repair "$whl" -w /app/dist/wheels + # `auditwheel repair` copies the external shared libraries into the wheel itself + # and automatically modifies the appropriate RPATH entries such that these libraries + # will be picked up at runtime. This accomplishes a similar result as if the libraries + # had been statically linked without requiring changes to the build system. + mkdir $WHEELS_TMP_DIR + "${PYBIN}/pip" wheel /app/ -w $WHEELS_TMP_DIR + for whl in $WHEELS_TMP_DIR/*; do + auditwheel repair "$whl" -w /app/dist/wheels + done + rm -rf $WHEELS_TMP_DIR done diff --git a/dtparse/__init__.py b/dtparse/__init__.py index 460c3a4..09d2145 100644 --- a/dtparse/__init__.py +++ b/dtparse/__init__.py @@ -1,10 +1,5 @@ from __future__ import absolute_import -from datetime import datetime -from ._dtparse import Parser # Import Parser from the rust binary +from ._dtparse import parse # Import Parser from the rust binary __all__ = ['parse'] - -# It doesn't make sense to create a Parser instance every time. -# We'll create just one and put it's parse method into the global scope. -parse = Parser(datetime).parse diff --git a/setup.py b/setup.py index 877a219..fc45178 100644 --- a/setup.py +++ b/setup.py @@ -27,13 +27,13 @@ def run(self): raise SystemExit(errno) -setup_requires = ['setuptools-rust>=0.6.0'] +setup_requires = ['setuptools-rust>=0.10.3'] install_requires = [] -tests_require = install_requires + ['pytest', 'pytest-benchmark'] +tests_require = install_requires + ['ciso8601', 'pytest', 'pytest-benchmark[histogram]'] setup( name='dtparse', - version='1.0.0', + version='1.1.0', classifiers=[ 'License :: OSI Approved :: MIT License', 'Development Status :: 3 - Alpha', diff --git a/src/lib.rs b/src/lib.rs index 37cbffb..9446a1c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,84 +1,50 @@ -#![feature(proc_macro, specialization, const_fn)] -#![feature(const_fn, const_align_of, const_size_of, const_ptr_null, const_ptr_null_mut)] extern crate pyo3; extern crate chrono; use chrono::prelude::*; +use pyo3::exceptions; use pyo3::prelude::*; -use std::error::Error; +use pyo3::types::*; +// https://pyo3.rs/v0.11.1/module.html +// This macro makes Rust compile a _dtparse.so binary in Python-compatible format. +// Such a binary can be imported from Python just like a regular Python module. +#[pymodule(_dtparse)] +fn init_mod(_py: Python, m: &PyModule) -> PyResult<()> { + // We fill this module with everything we want to make visible from Python. -// https://pyo3.github.io/pyo3/guide/class.html#define-new-class -// py::class macro transforms a Rust's Parser struct into a Python class -#[py::class] -struct Parser { - // we keep the datetime class in the structure, because we can't import it into the global - // scope in Rust. We should either accept it in constructor, or accept it as a parameter - // for parse, or import it at runtime. This looks like the most sensible way of three. - datetime_class: PyObject, - // token is needed to create a "rich" Python class, which has access to Python interpreter. - token: PyToken, -} - - -// https://pyo3.github.io/pyo3/guide/class.html#instance-methods -// py::methods macro generates Python-compatible wrappers for functions in the impl block. -#[py::methods] -impl Parser { - // https://pyo3.github.io/pyo3/guide/class.html#constructor - // Constructor is not created by default. - #[new] - fn __new__(obj: &PyRawObject, datetime_class: PyObject) -> PyResult<()> { - obj.init(| token| - Parser { datetime_class, token } - ) - } - - // This function will be transformed into a Python method. - // It has a special argument py: Python. If specified, it gets passed by PyO3 implicitly. - // It contains the Python interpreter - we're going to use it to create Python objects. - fn parse(&self, py: Python, str_datetime: String, fmt: String) -> PyResult { + #[pyfn(m, "parse")] + fn parse(_py: Python, str_datetime: String, fmt: String) -> PyResult<&PyDateTime> { // Call chrono and ask it to parse the datetime for us - let result = Utc.datetime_from_str( - str_datetime.as_str(), fmt.as_str() - ); + let chrono_dt = Utc.datetime_from_str(str_datetime.as_str(), fmt.as_str()); - // In case chrono couldn't parse datetime, raise a ValueError with chrono's error message. - // Because there are no exceptions in Rust, we return an exc::ValueError instance here. + // In case chrono couldn't parse a datetime, raise a ValueError with chrono's error message. + // Because there are no exceptions in Rust, we return a ValueError instance here. // By convention, it will make PyO3 wrapper raise an exception in Python interpreter. - // https://pyo3.github.io/pyo3/guide/exception.html#raise-an-exception - if result.is_err() { - return Err(exc::ValueError::new( - result.err().unwrap().description().to_owned() + // https://pyo3.rs/v0.11.1/exception.html + if chrono_dt.is_err() { + return Err(exceptions::ValueError::py_err( + chrono_dt.err().unwrap().to_string().to_owned(), )); } // In case everything's fine, get Rust datetime out of the result and transform - // it into a Python datetime. We use Python here to create a tuple of arguments - // and the datetime itself. - let dt = result.unwrap(); - let args = PyTuple::new( - py, &[ - dt.year(), - dt.month() as i32, - dt.day() as i32, - dt.hour() as i32, - dt.minute() as i32, - dt.second() as i32, - ] + // it into a Python datetime. + let dt = chrono_dt.unwrap(); + let result = PyDateTime::new( + _py, + dt.year(), + dt.month() as u8, + dt.day() as u8, + dt.hour() as u8, + dt.minute() as u8, + dt.second() as u8, + 0, + None, ); - Ok(self.datetime_class.call1(py, args)?) + Ok(result?) } -} - -// https://pyo3.github.io/pyo3/guide/module.html -// This macro will make Rust compile a _dtparse.so binary in Python-compatible format. -// Such binary could be imported in Python just like a normal Python module. -#[py::modinit(_dtparse)] -fn init_mod(_py: Python, m: &PyModule) -> PyResult<()> { - // Here we fill an empty module with everything we want to make visible from Python. - m.add_class::()?; Ok(()) } diff --git a/tests/test_performance.py b/tests/test_performance.py index c61b793..e2200e5 100644 --- a/tests/test_performance.py +++ b/tests/test_performance.py @@ -9,7 +9,7 @@ def test_ciso8601(benchmark): assert benchmark.pedantic( - ciso8601.parse_datetime_unaware, args=('2018-12-31T23:59:58', ), + ciso8601.parse_datetime, args=('2018-12-31T23:59:58', ), rounds=10 ** 6, iterations=100 ) == datetime(2018, 12, 31, 23, 59, 58)