Skip to content

Commit

Permalink
Version 1.1.0.
Browse files Browse the repository at this point in the history
- Update chrono and PyO3 to the latest versions
- Rewrite the library in stable Rust
- Update manylinux to 2010
  • Loading branch information
gukoff committed Jul 20, 2020
1 parent be4cc4e commit db02515
Show file tree
Hide file tree
Showing 10 changed files with 67 additions and 97 deletions.
3 changes: 2 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ Cargo.lock
projectFilesBackup/
tests.svg
benchmark*.svg
build/
build/
venv/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ projectFilesBackup/
benchmark*.svg
build/
*py[cdo]
venv/
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 7 additions & 6 deletions build_osx_wheels.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
9 changes: 6 additions & 3 deletions dist/manylinux/Dockerfile
Original file line number Diff line number Diff line change
@@ -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
25 changes: 14 additions & 11 deletions dist/manylinux/build_wheels.sh
Original file line number Diff line number Diff line change
@@ -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
7 changes: 1 addition & 6 deletions dtparse/__init__.py
Original file line number Diff line number Diff line change
@@ -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
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
94 changes: 30 additions & 64 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<PyObject> {
#[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::<Parser>()?;
Ok(())
}
2 changes: 1 addition & 1 deletion tests/test_performance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down

0 comments on commit db02515

Please sign in to comment.