From fc2962e04e65bec345d3245ca192f539788d9998 Mon Sep 17 00:00:00 2001 From: JeanArhancet Date: Mon, 19 Aug 2024 16:35:14 +0200 Subject: [PATCH] ci: integrate pyo3 ci: add manylinux fix: maturin build error ci: add wheels upload ci: use venv --- .github/workflows/python.yml | 47 ++++++++++++++++++++ .gitignore | 1 + bindings/python/Cargo.toml | 2 +- bindings/python/limbo/__init__.py | 26 +++++------ bindings/python/pyproject.toml | 4 +- bindings/python/requirements-dev.txt | 2 - bindings/python/requirements.txt | 8 ++++ bindings/python/src/lib.rs | 60 ++++++++++++++++++-------- bindings/python/tests/test_database.py | 39 +++++++++-------- 9 files changed, 134 insertions(+), 55 deletions(-) create mode 100644 .github/workflows/python.yml create mode 100644 bindings/python/requirements.txt diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml new file mode 100644 index 000000000..e22d1a040 --- /dev/null +++ b/.github/workflows/python.yml @@ -0,0 +1,47 @@ +name: Python + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + test: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ['3.12'] + runs-on: ${{ matrix.os }} + env: + working-directory: ./bindings/python + + defaults: + run: + working-directory: ${{ env.working-directory }} + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: install rust stable + uses: dtolnay/rust-toolchain@stable + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: pip install -r requirements.txt && pip install -r requirements-dev.txt + + - name: Run Ruff check + run: ruff check limbo tests + + - name: Install limbo + run: pip install -e . + + - name: Run Pytest + run: pytest tests \ No newline at end of file diff --git a/.gitignore b/.gitignore index 347592abe..9e065fc75 100644 --- a/.gitignore +++ b/.gitignore @@ -15,6 +15,7 @@ venv env .env .venv +dist/ # OS .DS_Store diff --git a/bindings/python/Cargo.toml b/bindings/python/Cargo.toml index 6b18c703a..ee3aca106 100644 --- a/bindings/python/Cargo.toml +++ b/bindings/python/Cargo.toml @@ -17,7 +17,7 @@ extension-module = ["pyo3/extension-module"] [dependencies] anyhow = "1.0" limbo_core = { path = "../../core" } -pyo3 = { version = "0.22.2", features = ["anyhow", "auto-initialize"] } +pyo3 = { version = "0.22.2", features = ["anyhow"] } [build-dependencies] version_check = "0.9.5" diff --git a/bindings/python/limbo/__init__.py b/bindings/python/limbo/__init__.py index 5cb8efaef..5bdebcab2 100644 --- a/bindings/python/limbo/__init__.py +++ b/bindings/python/limbo/__init__.py @@ -1,4 +1,4 @@ -from _limbo import ( +from ._limbo import ( Connection, Cursor, DatabaseError, @@ -14,16 +14,16 @@ from _limbo import ( ) __all__ = [ - "__version__", - "Connection", - "Cursor", - "InterfaceError", - "DatabaseError", - "DataError", - "OperationalError", - "IntegrityError", - "InternalError", - "ProgrammingError", - "NotSupportedError", - "connect", + '__version__', + 'Connection', + 'Cursor', + 'InterfaceError', + 'DatabaseError', + 'DataError', + 'OperationalError', + 'IntegrityError', + 'InternalError', + 'ProgrammingError', + 'NotSupportedError', + 'connect', ] diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index e9417af84..c6a564fab 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -35,7 +35,6 @@ dynamic = [ [project.optional-dependencies] dev = [ - "maturin==1.7.0", "black==24.4.2", "isort==5.13.2", "mypy==1.11.0", @@ -50,6 +49,8 @@ Source = "https://github.com/penberg/limbo" [tool.maturin] bindings = 'pyo3' +module-name = "limbo._limbo" +features = ["pyo3/extension-module"] [tool.ruff] line-length = 120 @@ -70,7 +71,6 @@ quote-style = 'single' testpaths = 'tests' log_format = '%(name)s %(levelname)s: %(message)s' - [tool.coverage.run] source = ['limbo'] branch = true diff --git a/bindings/python/requirements-dev.txt b/bindings/python/requirements-dev.txt index acad1afb9..f3fcc6bed 100644 --- a/bindings/python/requirements-dev.txt +++ b/bindings/python/requirements-dev.txt @@ -14,8 +14,6 @@ iniconfig==2.0.0 # via pytest isort==5.13.2 # via limbo (pyproject.toml) -maturin==1.7.0 - # via limbo (pyproject.toml) mypy==1.11.0 # via limbo (pyproject.toml) mypy-extensions==1.0.0 diff --git a/bindings/python/requirements.txt b/bindings/python/requirements.txt new file mode 100644 index 000000000..297f1f7cd --- /dev/null +++ b/bindings/python/requirements.txt @@ -0,0 +1,8 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --strip-extras pyproject.toml +# +typing-extensions==4.12.2 + # via limbo (pyproject.toml) diff --git a/bindings/python/src/lib.rs b/bindings/python/src/lib.rs index 7c1028772..ba66adbad 100644 --- a/bindings/python/src/lib.rs +++ b/bindings/python/src/lib.rs @@ -119,22 +119,23 @@ impl Cursor { let mut smt_lock = smt.lock().map_err(|_| { PyErr::new::("Failed to acquire statement lock") })?; - - match smt_lock - .step() - .map_err(|e| PyErr::new::(format!("Step error: {:?}", e)))? - { - limbo_core::RowResult::Row(row) => { - let py_row = row_to_py(py, &row); - Ok(Some(py_row)) + loop { + match smt_lock.step().map_err(|e| { + PyErr::new::(format!("Step error: {:?}", e)) + })? { + limbo_core::RowResult::Row(row) => { + let py_row = row_to_py(py, &row); + return Ok(Some(py_row)); + } + limbo_core::RowResult::IO => { + self.conn.io.run_once().map_err(|e| { + PyErr::new::(format!("IO error: {:?}", e)) + })?; + } + limbo_core::RowResult::Done => { + return Ok(None); + } } - limbo_core::RowResult::IO => { - self.conn.io.run_once().map_err(|e| { - PyErr::new::(format!("IO error: {:?}", e)) - })?; - Ok(None) - } - limbo_core::RowResult::Done => Ok(None), } } else { Err(PyErr::new::("No statement prepared for execution").into()) @@ -143,10 +144,33 @@ impl Cursor { pub fn fetchall(&mut self, py: Python) -> Result> { let mut results = Vec::new(); - while let Some(row) = self.fetchone(py)? { - results.push(row); + + if let Some(smt) = &self.smt { + let mut smt_lock = smt.lock().map_err(|_| { + PyErr::new::("Failed to acquire statement lock") + })?; + + loop { + match smt_lock.step().map_err(|e| { + PyErr::new::(format!("Step error: {:?}", e)) + })? { + limbo_core::RowResult::Row(row) => { + let py_row = row_to_py(py, &row); + results.push(py_row); + } + limbo_core::RowResult::IO => { + self.conn.io.run_once().map_err(|e| { + PyErr::new::(format!("IO error: {:?}", e)) + })?; + } + limbo_core::RowResult::Done => { + return Ok(results); + } + } + } + } else { + Err(PyErr::new::("No statement prepared for execution").into()) } - Ok(results) } pub fn close(&self) -> Result<()> { diff --git a/bindings/python/tests/test_database.py b/bindings/python/tests/test_database.py index 806bc0aaa..06450add9 100644 --- a/bindings/python/tests/test_database.py +++ b/bindings/python/tests/test_database.py @@ -5,53 +5,54 @@ import pytest import limbo -@pytest.mark.parametrize("provider", ["sqlite3", "limbo"]) +@pytest.mark.parametrize('provider', ['sqlite3', 'limbo']) def test_fetchall_select_all_users(provider): - conn = connect(provider, "tests/database.db") + conn = connect(provider, 'tests/database.db') cursor = conn.cursor() - cursor.execute("SELECT * FROM users") + cursor.execute('SELECT * FROM users') users = cursor.fetchall() assert users - assert users == [(1, "alice"), (2, "bob")] + assert users == [(1, 'alice'), (2, 'bob')] @pytest.mark.parametrize( - "provider", + 'provider', [ - "sqlite3", + 'sqlite3', + 'limbo' ], ) def test_fetchall_select_user_ids(provider): - conn = connect(provider, "tests/database.db") + conn = connect(provider, 'tests/database.db') cursor = conn.cursor() - cursor.execute("SELECT id FROM users") + cursor.execute('SELECT id FROM users') user_ids = cursor.fetchall() assert user_ids assert user_ids == [(1,), (2,)] -@pytest.mark.parametrize("provider", ["sqlite3", "limbo"]) +@pytest.mark.parametrize('provider', ['sqlite3', 'limbo']) def test_fetchone_select_all_users(provider): - conn = connect(provider, "tests/database.db") + conn = connect(provider, 'tests/database.db') cursor = conn.cursor() - cursor.execute("SELECT * FROM users") + cursor.execute('SELECT * FROM users') alice = cursor.fetchone() assert alice - assert alice == (1, "alice") + assert alice == (1, 'alice') bob = cursor.fetchone() assert bob - assert bob == (2, "bob") + assert bob == (2, 'bob') -@pytest.mark.parametrize("provider", ["sqlite3", "limbo"]) +@pytest.mark.parametrize('provider', ['sqlite3', 'limbo']) def test_fetchone_select_max_user_id(provider): - conn = connect(provider, "tests/database.db") + conn = connect(provider, 'tests/database.db') cursor = conn.cursor() - cursor.execute("SELECT MAX(id) FROM users") + cursor.execute('SELECT MAX(id) FROM users') max_id = cursor.fetchone() assert max_id @@ -59,8 +60,8 @@ def test_fetchone_select_max_user_id(provider): def connect(provider, database): - if provider == "limbo": + if provider == 'limbo': return limbo.connect(database) - if provider == "sqlite3": + if provider == 'sqlite3': return sqlite3.connect(database) - raise Exception(f"Provider `{provider}` is not supported") + raise Exception(f'Provider `{provider}` is not supported')