diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9487a0b..0df97ab 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -105,7 +105,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Bumping version run: | python ./scripts/version_bumper.py --target Cargo.toml "${{ github.ref_name }}" @@ -134,7 +134,7 @@ jobs: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: - python-version: '3.10' + python-version: '3.11' - name: Bumping version run: | python ./scripts/version_bumper.py --target Cargo.toml "${{ github.ref_name }}" diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index b5cb474..b1f6062 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -8,9 +8,8 @@ jobs: strategy: matrix: cmd: - - black - - isort - - ruff + - ruff-check + - ruff-format - mypy runs-on: ubuntu-latest steps: @@ -18,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.13" - name: Run lint check uses: pre-commit/action@v3.0.0 with: @@ -49,7 +48,6 @@ jobs: - uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - args: -p scyllapy --all-features -- -W clippy::all -W clippy::pedantic -D warnings pytest: name: ${{matrix.job.os}}-${{matrix.py_version}} services: @@ -64,7 +62,7 @@ jobs: - 9042:9042 strategy: matrix: - py_version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + py_version: ["3.11", "3.12", "3.13", "3.14"] job: - os: ubuntu-latest ssl_cmd: sudo apt-get update && sudo apt-get install libssl-dev openssl diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5081196..94bf677 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,24 +1,8 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.1.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - - repo: https://github.com/pycqa/isort - rev: 5.12.0 - hooks: - - id: isort - name: python isort - pass_filenames: false - always_run: true - args: ["python"] - - repo: https://github.com/psf/black - rev: 23.9.1 - hooks: - - id: black - name: python black - pass_filenames: false - always_run: true - args: ["python"] - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.9.0 hooks: @@ -30,13 +14,18 @@ repos: pass_filenames: false args: ["python"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.0.291 + rev: v0.17.0 hooks: - - id: ruff - name: ruff + - id: ruff-check + name: ruff-check pass_filenames: false always_run: true args: ["python", "--fix"] + - id: ruff-format + name: ruff-format + pass_filenames: false + always_run: true + args: ["python"] - repo: local hooks: - id: fmt diff --git a/Cargo.toml b/Cargo.toml index 69af7ff..22af0d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,26 +9,26 @@ crate-type = ["cdylib"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -chrono = "0.4.31" +chrono = "0.4.45" eq-float = "0.1.0" -futures = "0.3.28" -log = "0.4.20" -openssl = { version = "0.10.57", features = ["vendored"] } -pyo3 = { version = "0.20.0", features = [ +futures = "0.3.34" +log = "0.4.34" +openssl = { version = "0.10.81", features = ["vendored"] } +pyo3 = { version = "0.29.3", features = [ "auto-initialize", - "abi3-py38", + "abi3-py311", "extension-module", "chrono", ] } -pyo3-asyncio = { version = "0.20.0", features = ["tokio-runtime"] } -pyo3-log = "0.9.0" +pyo3-async-runtimes = { version = "0.29.0", features = ["tokio-runtime"] } +pyo3-log = "0.13.4" rustc-hash = "1.1.0" scylla = { version = "0.12.0", features = ["ssl", "full-serialization"] } -bigdecimal-04 = { package = "bigdecimal", version = "0.4" } -thiserror = "1.0.48" -tokio = { version = "1.32.0", features = ["bytes"] } -uuid = { version = "1.4.1", features = ["v4"] } -time = { version = "*", features = ["formatting", "macros"] } +bigdecimal-04 = { package = "bigdecimal", version = "0.4.11" } +thiserror = "1.0.69" +tokio = { version = "1.53.3", features = ["bytes"] } +uuid = { version = "1.28.0", features = ["v4"] } +time = { version = "0.3.55", features = ["formatting", "macros"] } [profile.release] lto = "fat" diff --git a/pyproject.toml b/pyproject.toml index 283e523..43a127d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,108 +1,122 @@ [project] name = "scyllapy" description = "Async scylla driver for python" -requires-python = ">=3.8,<4" readme = "README.md" -license = { file = "LICENSE" } -keywords = ["scylla", "cassandra", "async-driver", "scylla-driver"] +requires-python = ">=3.11,<4" +license-files = ["LICENSE"] +keywords = ["async-driver", "cassandra", "scylla", "scylla-driver"] classifiers = [ - "Typing :: Typed", - "Topic :: Database", - "Development Status :: 4 - Beta", - "Programming Language :: Rust", - "Operating System :: MacOS", - "Operating System :: Microsoft", - "Operating System :: POSIX :: Linux", - "Intended Audience :: Developers", - "Topic :: Database :: Front-Ends", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Operating System :: MacOS", + "Operating System :: Microsoft", + "Operating System :: POSIX :: Linux", + "Programming Language :: Rust", + "Topic :: Database", + "Topic :: Database :: Front-Ends", + "Typing :: Typed", ] dependencies = ["python-dateutil"] +dynamic = ["version"] -[tool.maturin] -python-source = "python" -module-name = "scyllapy._internal" -features = ["pyo3/extension-module"] +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" [tool.isort] profile = "black" multi_line_output = 3 +[tool.maturin] +features = ["pyo3/extension-module"] +module-name = "scyllapy._internal" +python-source = "python" + [tool.mypy] strict = true mypy_path = "python" ignore_missing_imports = true -allow_subclassing_any = true -allow_untyped_calls = true +disallow_subclassing_any = false +disallow_untyped_calls = false pretty = true -show_error_codes = true implicit_reexport = true allow_untyped_decorators = true warn_return_any = false warn_unused_ignores = false -[build-system] -requires = ["maturin>=1.0,<2.0"] -build-backend = "maturin" +[tool.pytest] +anyio_mode = "auto" [tool.ruff] +target-version = "py311" +exclude = [".venv/"] +line-length = 88 + +[tool.ruff.lint] +mccabe = { max-complexity = 10 } # List of enabled rulsets. # See https://docs.astral.sh/ruff/rules/ for more information. select = [ - "E", # Error - "F", # Pyflakes - "W", # Pycodestyle - "C90", # McCabe complexity - "N", # pep8-naming - "D", # Pydocstyle - "ANN", # Pytype annotations - "S", # Bandit - "B", # Bugbear - "COM", # Commas - "C4", # Comprehensions - "ISC", # Implicit string concat - "PIE", # Unnecessary code - "T20", # Catch prints - "PYI", # validate pyi files - "Q", # Checks for quotes - "RSE", # Checks raise statements - "RET", # Checks return statements - "SLF", # Self checks - "SIM", # Simplificator - "PTH", # Pathlib checks - "ERA", # Checks for commented out code - "PL", # PyLint checks - "RUF", # Specific to Ruff checks + "E", # Error + "F", # Pyflakes + "W", # Pycodestyle + "C90", # McCabe complexity + "I", # Isort + "N", # pep8-naming + "D", # Pydocstyle + "ANN", # Pytype annotations + "S", # Bandit + "B", # Bugbear + "COM", # Commas + "C4", # Comprehensions + "ISC", # Implicit string concat + "PIE", # Unnecessary code + "T20", # Catch prints + "PYI", # validate pyi files + "Q", # Checks for quotes + "RSE", # Checks raise statements + "RET", # Checks return statements + "SLF", # Self checks + "SIM", # Simplificator + "PTH", # Pathlib checks + "ERA", # Checks for commented out code + "PL", # PyLint checks + "RUF", # Specific to Ruff checks + "UP", # Pyupgrade ] ignore = [ - "D105", # Missing docstring in magic method - "D107", # Missing docstring in __init__ - "D211", # No blank lines allowed before class docstring - "D212", # Multi-line docstring summary should start at the first line - "D401", # First line should be in imperative mood - "D104", # Missing docstring in public package - "D100", # Missing docstring in public module - "ANN102", # Missing type annotation for self in method - "ANN101", # Missing type annotation for argument - "ANN401", # typing.Any are disallowed in `**kwargs - "PLR0913", # Too many arguments for function call - "D106", # Missing docstring in public nested class + "D105", # Missing docstring in magic method + "D107", # Missing docstring in __init__ + "D212", # Multi-line docstring summary should start at the first line + "D401", # First line should be in imperative mood + "D104", # Missing docstring in public package + "D100", # Missing docstring in public module + "ANN401", # typing.Any are disallowed in `**kwargs + "PLR0913", # Too many arguments for function call + "D106", # Missing docstring in public nested class + "PYI021", # Docstrings should not be included in stubs + "COM812", # Missing trailing comma in a list or tuple ] -exclude = [".venv/"] -mccabe = { max-complexity = 10 } -line-length = 88 -[tool.ruff.per-file-ignores] -"python/scyllapy/*" = ["PYI021"] +[tool.ruff.lint.per-file-ignores] "python/tests/*" = [ - "S101", # Use of assert detected - "S608", # Possible SQL injection vector through string-based query construction - "D103", # Missing docstring in public function - "S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes + "S101", # Use of assert detected + "S301", # Use of pickle detected + "D103", # Missing docstring in public function + "SLF001", # Private member accessed + "S311", # Standard pseudo-random generators are not suitable for security/cryptographic purposes + "D101", # Missing docstring in public class + "PLR2004", # Magic value used in comparison + "B017", # Do not assert blind exception + "S608", # Possible SQL injection ] -[tool.ruff.pydocstyle] +[tool.ruff.lint.pydocstyle] convention = "pep257" ignore-decorators = ["typing.overload"] -[tool.ruff.pylint] +[tool.ruff.lint.pylint] allow-magic-value-types = ["int", "str", "float"] + +[tool.uv] +package = false diff --git a/python/scyllapy/__init__.py b/python/scyllapy/__init__.py index f786e84..5d5d2c4 100644 --- a/python/scyllapy/__init__.py +++ b/python/scyllapy/__init__.py @@ -18,17 +18,17 @@ __version__ = version("scyllapy") __all__ = [ - "__version__", - "Scylla", - "Consistency", - "Query", - "SerialConsistency", - "PreparedQuery", "Batch", "BatchType", + "Consistency", + "ExecutionProfile", + "InlineBatch", + "PreparedQuery", + "Query", "QueryResult", "SSLVerifyMode", + "Scylla", + "SerialConsistency", + "__version__", "extra_types", - "InlineBatch", - "ExecutionProfile", ] diff --git a/python/scyllapy/_internal/__init__.pyi b/python/scyllapy/_internal/__init__.pyi index cf7775d..6f940e3 100644 --- a/python/scyllapy/_internal/__init__.pyi +++ b/python/scyllapy/_internal/__init__.pyi @@ -1,13 +1,5 @@ -from typing import ( - Any, - Callable, - Generic, - Iterable, - Literal, - Optional, - TypeVar, - overload, -) +from collections.abc import Callable, Iterable +from typing import Any, Generic, Literal, TypeVar, overload from scyllapy._internal.load_balancing import LoadBalancingPolicy @@ -134,7 +126,7 @@ class Scylla: async def batch( self, batch: Batch | InlineBatch, - params: Optional[Iterable[Iterable[Any] | dict[str, Any]]] = None, + params: Iterable[Iterable[Any] | dict[str, Any]] | None = None, ) -> QueryResult: """ Execute a batch statement. @@ -165,11 +157,11 @@ class QueryResult: trace_id: str | None @overload - def all(self, as_class: Literal[None] = None) -> list[dict[str, Any]]: ... + def all(self, as_class: None = None) -> list[dict[str, Any]]: ... @overload def all(self, as_class: Callable[..., _T] | None = None) -> list[_T]: ... @overload - def first(self, as_class: Literal[None] = None) -> dict[str, Any] | None: ... + def first(self, as_class: None = None) -> dict[str, Any] | None: ... @overload def first(self, as_class: Callable[..., _T] | None = None) -> _T | None: ... def scalars(self) -> list[Any]: ... diff --git a/python/scyllapy/exceptions.py b/python/scyllapy/exceptions.py index ae3de1d..8410a7e 100644 --- a/python/scyllapy/exceptions.py +++ b/python/scyllapy/exceptions.py @@ -9,9 +9,9 @@ __all__ = ( "ScyllaPyBaseError", + "ScyllaPyBindingError", "ScyllaPyDBError", - "ScyllaPySessionError", "ScyllaPyMappingError", "ScyllaPyQueryBuiderError", - "ScyllaPyBindingError", + "ScyllaPySessionError", ) diff --git a/python/scyllapy/extra_types.py b/python/scyllapy/extra_types.py index d38054d..171b889 100644 --- a/python/scyllapy/extra_types.py +++ b/python/scyllapy/extra_types.py @@ -1,5 +1,5 @@ import dataclasses -from typing import Any, List +from typing import Any from ._internal.extra_types import BigInt, Counter, Double, SmallInt, TinyInt, Unset @@ -19,7 +19,7 @@ class ScyllaPyUDT: It can be further extended to support other model types. """ - def __dump_udt__(self) -> List[Any]: + def __dump_udt__(self) -> list[Any]: """ Method to dump UDT models to a dict. @@ -45,4 +45,4 @@ def __dump_udt__(self) -> List[Any]: raise ValueError("Unsupported model type") -__all__ = ("BigInt", "Counter", "Double", "SmallInt", "TinyInt", "Unset", "ScyllaPyUDT") +__all__ = ("BigInt", "Counter", "Double", "ScyllaPyUDT", "SmallInt", "TinyInt", "Unset") diff --git a/python/scyllapy/query_builder.py b/python/scyllapy/query_builder.py index 24e7822..4f14f59 100644 --- a/python/scyllapy/query_builder.py +++ b/python/scyllapy/query_builder.py @@ -1,3 +1,3 @@ from ._internal.query_builder import Delete, Insert, Select, Update -__all__ = ["Select", "Delete", "Insert", "Update"] +__all__ = ["Delete", "Insert", "Select", "Update"] diff --git a/python/tests/conftest.py b/python/tests/conftest.py index fe865c7..bf2d96b 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -1,11 +1,11 @@ import os -from typing import AsyncGenerator +from collections.abc import AsyncGenerator import pytest -from tests.utils import random_string - from scyllapy import Scylla +from tests.utils import random_string + @pytest.fixture(scope="session") def anyio_backend() -> str: diff --git a/python/tests/query_builders/test_delete.py b/python/tests/query_builders/test_delete.py index a7b101f..2547e04 100644 --- a/python/tests/query_builders/test_delete.py +++ b/python/tests/query_builders/test_delete.py @@ -1,9 +1,9 @@ import pytest -from tests.utils import random_string - from scyllapy import Scylla from scyllapy.query_builder import Delete +from tests.utils import random_string + @pytest.mark.anyio async def test_success(scylla: Scylla) -> None: @@ -39,8 +39,13 @@ async def test_custom_if(scylla: Scylla) -> None: f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", [1, "meme"], ) - await Delete(table_name).where("id = ?", [1]).if_("name != ?", [None]).execute( - scylla, + await ( + Delete(table_name) + .where("id = ?", [1]) + .if_("name != ?", [None]) + .execute( + scylla, + ) ) res = await scylla.execute(f"SELECT * FROM {table_name}") assert not res.all() @@ -54,8 +59,13 @@ async def test_custom_custom_if(scylla: Scylla) -> None: f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", [1, "meme"], ) - await Delete(table_name).where("id = ?", [1]).if_("name != ?", [None]).execute( - scylla, + await ( + Delete(table_name) + .where("id = ?", [1]) + .if_("name != ?", [None]) + .execute( + scylla, + ) ) res = await scylla.execute(f"SELECT * FROM {table_name}") assert not res.all() diff --git a/python/tests/query_builders/test_inserts.py b/python/tests/query_builders/test_inserts.py index 7e4b8c6..341fe1d 100644 --- a/python/tests/query_builders/test_inserts.py +++ b/python/tests/query_builders/test_inserts.py @@ -1,9 +1,9 @@ import pytest -from tests.utils import random_string - from scyllapy import Scylla from scyllapy.query_builder import Insert +from tests.utils import random_string + @pytest.mark.anyio async def test_insert_success(scylla: Scylla) -> None: @@ -19,10 +19,16 @@ async def test_insert_if_not_exists(scylla: Scylla) -> None: table_name = random_string(4) await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") await Insert(table_name).set("id", 1).set("name", "random").execute(scylla) - await Insert(table_name).set("id", 1).set( - "name", - "random2", - ).if_not_exists().execute(scylla) + await ( + Insert(table_name) + .set("id", 1) + .set( + "name", + "random2", + ) + .if_not_exists() + .execute(scylla) + ) res = await scylla.execute(f"SELECT * FROM {table_name}") assert res.all() == [{"id": 1, "name": "random"}] diff --git a/python/tests/query_builders/test_select.py b/python/tests/query_builders/test_select.py index 8361727..fb4ac0c 100644 --- a/python/tests/query_builders/test_select.py +++ b/python/tests/query_builders/test_select.py @@ -1,11 +1,11 @@ import uuid import pytest -from tests.utils import random_string - from scyllapy import Scylla from scyllapy.query_builder import Select +from tests.utils import random_string + @pytest.mark.anyio async def test_select_success(scylla: Scylla) -> None: diff --git a/python/tests/query_builders/test_update.py b/python/tests/query_builders/test_update.py index 1dd6fdd..e4db1a6 100644 --- a/python/tests/query_builders/test_update.py +++ b/python/tests/query_builders/test_update.py @@ -1,9 +1,9 @@ import pytest -from tests.utils import random_string - from scyllapy import Scylla from scyllapy.query_builder import Update +from tests.utils import random_string + @pytest.mark.anyio async def test_success(scylla: Scylla) -> None: @@ -26,10 +26,16 @@ async def test_ifs(scylla: Scylla) -> None: f"INSERT INTO {table_name}(id, name) VALUES (?, ?)", [1, "meme"], ) - await Update(table_name).set("name", "meme2").if_("name = ?", ["meme"]).where( - "id = ?", - [1], - ).execute(scylla) + await ( + Update(table_name) + .set("name", "meme2") + .if_("name = ?", ["meme"]) + .where( + "id = ?", + [1], + ) + .execute(scylla) + ) res = await scylla.execute(f"SELECT * FROM {table_name}") assert res.all() == [{"id": 1, "name": "meme2"}] @@ -38,9 +44,15 @@ async def test_ifs(scylla: Scylla) -> None: async def test_if_exists(scylla: Scylla) -> None: table_name = random_string(4) await scylla.execute(f"CREATE TABLE {table_name} (id INT PRIMARY KEY, name TEXT)") - await Update(table_name).set("name", "meme2").if_exists().where( - "id = ?", - [1], - ).execute(scylla) + await ( + Update(table_name) + .set("name", "meme2") + .if_exists() + .where( + "id = ?", + [1], + ) + .execute(scylla) + ) res = await scylla.execute(f"SELECT * FROM {table_name}") assert res.all() == [] diff --git a/python/tests/test_batches.py b/python/tests/test_batches.py index d536a38..75b54b6 100644 --- a/python/tests/test_batches.py +++ b/python/tests/test_batches.py @@ -1,8 +1,8 @@ import pytest -from tests.utils import random_string - from scyllapy import Batch, Scylla +from tests.utils import random_string + @pytest.mark.anyio async def test_batches(scylla: Scylla) -> None: diff --git a/python/tests/test_bindings.py b/python/tests/test_bindings.py index a6e1a07..d548f5f 100644 --- a/python/tests/test_bindings.py +++ b/python/tests/test_bindings.py @@ -2,15 +2,16 @@ import ipaddress import random import uuid +from collections.abc import Callable from decimal import Decimal -from typing import Any, Callable +from typing import Any import pytest from dateutil.relativedelta import relativedelta -from tests.utils import random_string - from scyllapy import Scylla +from tests.utils import random_string + @pytest.mark.anyio @pytest.mark.parametrize( diff --git a/python/tests/test_extra_types.py b/python/tests/test_extra_types.py index befee62..a6487f1 100644 --- a/python/tests/test_extra_types.py +++ b/python/tests/test_extra_types.py @@ -1,12 +1,13 @@ +from collections.abc import Callable from dataclasses import asdict, dataclass -from typing import Any, Callable +from typing import Any import pytest -from tests.utils import random_string - from scyllapy import Scylla, extra_types from scyllapy.exceptions import ScyllaPyDBError +from tests.utils import random_string + @pytest.mark.anyio @pytest.mark.parametrize( diff --git a/python/tests/test_pagination.py b/python/tests/test_pagination.py index 9bc573d..848a8f7 100644 --- a/python/tests/test_pagination.py +++ b/python/tests/test_pagination.py @@ -1,11 +1,11 @@ from dataclasses import dataclass import pytest -from tests.utils import random_string - from scyllapy import Scylla from scyllapy.query_builder import Select +from tests.utils import random_string + @pytest.mark.anyio async def test_scalars(scylla: Scylla) -> None: diff --git a/python/tests/test_parsing.py b/python/tests/test_parsing.py index b4cfab5..82f96e3 100644 --- a/python/tests/test_parsing.py +++ b/python/tests/test_parsing.py @@ -1,8 +1,8 @@ import pytest -from tests.utils import random_string - from scyllapy import Scylla +from tests.utils import random_string + @pytest.mark.anyio async def test_udt_parsing(scylla: Scylla) -> None: diff --git a/python/tests/test_prepared.py b/python/tests/test_prepared.py index 7ea618f..833871c 100644 --- a/python/tests/test_prepared.py +++ b/python/tests/test_prepared.py @@ -1,8 +1,8 @@ import pytest -from tests.utils import random_string - from scyllapy import Scylla +from tests.utils import random_string + @pytest.mark.anyio async def test_prepared(scylla: Scylla) -> None: diff --git a/python/tests/test_profiles.py b/python/tests/test_profiles.py index f06fcb9..95662a4 100644 --- a/python/tests/test_profiles.py +++ b/python/tests/test_profiles.py @@ -1,9 +1,9 @@ import pytest -from tests.utils import random_string - from scyllapy import Consistency, ExecutionProfile, Query, Scylla from scyllapy.exceptions import ScyllaPyDBError +from tests.utils import random_string + @pytest.mark.anyio async def test_wrong_consistency(scylla: Scylla) -> None: @@ -11,7 +11,7 @@ async def test_wrong_consistency(scylla: Scylla) -> None: table_name = random_string(4) await scylla.execute(f"CREATE TABLE {table_name}(id INT PRIMARY KEY)") query = Query(f"SELECT * FROM {table_name} WHERE id = ?", profile=profile) - with pytest.raises(ScyllaPyDBError, match=".*only supported for writes.*"): + with pytest.raises(ScyllaPyDBError, match=r".*only supported for writes.*"): await scylla.execute(query, [1]) await scylla.execute(query.with_profile(None), [1]) diff --git a/python/tests/test_queries.py b/python/tests/test_queries.py index 5d3652a..2d02c41 100644 --- a/python/tests/test_queries.py +++ b/python/tests/test_queries.py @@ -1,10 +1,10 @@ from dataclasses import dataclass import pytest -from tests.utils import random_string - from scyllapy import Scylla +from tests.utils import random_string + @pytest.mark.anyio async def test_empty_scalars(scylla: Scylla) -> None: diff --git a/python/tests/test_query_res.py b/python/tests/test_query_res.py index 68a2bc7..e902632 100644 --- a/python/tests/test_query_res.py +++ b/python/tests/test_query_res.py @@ -1,8 +1,8 @@ import pytest -from tests.utils import random_string - from scyllapy import Scylla +from tests.utils import random_string + @pytest.mark.anyio async def test_results_len(scylla: Scylla) -> None: diff --git a/src/batches.rs b/src/batches.rs index c13e681..7dd0370 100644 --- a/src/batches.rs +++ b/src/batches.rs @@ -1,4 +1,4 @@ -use pyo3::{pyclass, pymethods, types::PyDict, PyAny}; +use pyo3::{pyclass, pymethods, types::PyDict, Bound, PyAny}; use scylla::{ batch::{Batch, BatchStatement, BatchType}, frame::value::LegacySerializedValues, @@ -9,7 +9,7 @@ use crate::{ utils::parse_python_query_params, }; -#[pyclass(name = "BatchType")] +#[pyclass(name = "BatchType", from_py_object)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ScyllaPyBatchType { COUNTER, @@ -17,14 +17,14 @@ pub enum ScyllaPyBatchType { UNLOGGED, } -#[pyclass(name = "Batch")] +#[pyclass(name = "Batch", from_py_object)] #[derive(Clone)] pub struct ScyllaPyBatch { inner: Batch, request_params: ScyllaPyRequestParams, } -#[pyclass(name = "InlineBatch")] +#[pyclass(name = "InlineBatch", from_py_object)] #[derive(Clone)] pub struct ScyllaPyInlineBatch { inner: Batch, @@ -60,7 +60,10 @@ impl ScyllaPyBatch { batch_type = ScyllaPyBatchType::UNLOGGED, **params ))] - pub fn py_new(batch_type: ScyllaPyBatchType, params: Option<&PyDict>) -> ScyllaPyResult { + pub fn py_new( + batch_type: ScyllaPyBatchType, + params: Option<&Bound<'_, PyDict>>, + ) -> ScyllaPyResult { Ok(Self { inner: Batch::new(batch_type.into()), request_params: ScyllaPyRequestParams::from_dict(params)?, @@ -96,7 +99,10 @@ impl ScyllaPyInlineBatch { batch_type = ScyllaPyBatchType::UNLOGGED, **params ))] - pub fn py_new(batch_type: ScyllaPyBatchType, params: Option<&PyDict>) -> ScyllaPyResult { + pub fn py_new( + batch_type: ScyllaPyBatchType, + params: Option<&Bound<'_, PyDict>>, + ) -> ScyllaPyResult { Ok(Self { inner: Batch::new(batch_type.into()), request_params: ScyllaPyRequestParams::from_dict(params)?, @@ -118,7 +124,7 @@ impl ScyllaPyInlineBatch { pub fn add_query( &mut self, query: BatchQueryInput, - values: Option<&PyAny>, + values: Option<&Bound<'_, PyAny>>, ) -> ScyllaPyResult<()> { self.inner.append_statement(query); if let Some(passed_params) = values { diff --git a/src/consistencies.rs b/src/consistencies.rs index 6a20947..7983009 100644 --- a/src/consistencies.rs +++ b/src/consistencies.rs @@ -5,7 +5,7 @@ use scylla::statement::{Consistency, SerialConsistency}; /// /// This class allows to run queries /// with specific consistency levels. -#[pyclass(name = "Consistency")] +#[pyclass(name = "Consistency", from_py_object)] #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[allow(non_camel_case_types)] pub enum ScyllaPyConsistency { @@ -22,7 +22,7 @@ pub enum ScyllaPyConsistency { LOCAL_SERIAL, } -#[pyclass(name = "SerialConsistency")] +#[pyclass(name = "SerialConsistency", from_py_object)] #[derive(Clone, Copy, PartialEq, Eq, Debug)] #[allow(non_camel_case_types)] pub enum ScyllaPySerialConsistency { diff --git a/src/exceptions/py_err.rs b/src/exceptions/py_err.rs index 6dc52d7..db81ca0 100644 --- a/src/exceptions/py_err.rs +++ b/src/exceptions/py_err.rs @@ -1,4 +1,8 @@ -use pyo3::{create_exception, types::PyModule, PyResult, Python}; +use pyo3::{ + create_exception, + types::{PyModule, PyModuleMethods}, + Bound, PyResult, Python, +}; create_exception!( scyllapy.exceptions, @@ -23,7 +27,7 @@ create_exception!( /// # Errors /// /// May throw an error, if module cannot be constructed. -pub fn setup_module(py: Python<'_>, module: &PyModule) -> PyResult<()> { +pub fn setup_module(py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { module.add("ScyllaPyBaseError", py.get_type::())?; module.add("ScyllaPyDBError", py.get_type::())?; module.add( diff --git a/src/execution_profiles.rs b/src/execution_profiles.rs index 449cd4c..f11a2b7 100644 --- a/src/execution_profiles.rs +++ b/src/execution_profiles.rs @@ -8,7 +8,7 @@ use crate::{ load_balancing::ScyllaPyLoadBalancingPolicy, }; -#[pyclass(name = "ExecutionProfile")] +#[pyclass(name = "ExecutionProfile", from_py_object)] #[derive(Clone, Debug)] pub struct ScyllaPyExecutionProfile { inner: scylla::ExecutionProfile, diff --git a/src/extra_types.rs b/src/extra_types.rs index 9d946ab..611b8b9 100644 --- a/src/extra_types.rs +++ b/src/extra_types.rs @@ -1,8 +1,12 @@ -use pyo3::{pyclass, pymethods, types::PyModule, PyResult, Python}; +use pyo3::{ + pyclass, pymethods, + types::{PyModule, PyModuleMethods}, + Bound, PyResult, Python, +}; macro_rules! simple_wrapper { ($name:ident, $ttype:ty) => { - #[pyclass] + #[pyclass(from_py_object)] #[derive(Clone)] pub struct $name { inner: $ttype, @@ -10,7 +14,7 @@ macro_rules! simple_wrapper { impl $name { #[must_use] - pub fn get_value(&self) -> $ttype { + pub const fn get_value(&self) -> $ttype { self.inner } } @@ -19,7 +23,7 @@ macro_rules! simple_wrapper { impl $name { #[new] #[must_use] - pub fn py_new(val: $ttype) -> Self { + pub const fn py_new(val: $ttype) -> Self { Self { inner: val } } @@ -37,7 +41,7 @@ simple_wrapper!(BigInt, i64); simple_wrapper!(Double, f64); simple_wrapper!(Counter, i64); -#[pyclass(name = "Unset")] +#[pyclass(name = "Unset", skip_from_py_object)] #[derive(Clone, Copy)] pub struct ScyllaPyUnset {} @@ -45,7 +49,7 @@ pub struct ScyllaPyUnset {} impl ScyllaPyUnset { #[new] #[must_use] - pub fn py_new() -> Self { + pub const fn py_new() -> Self { Self {} } } @@ -56,7 +60,7 @@ impl ScyllaPyUnset { /// /// May return error if module cannot be created, /// or any of classes cannot be added. -pub fn setup_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { +pub fn setup_module(_py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; module.add_class::()?; diff --git a/src/lib.rs b/src/lib.rs index 0a00989..f5e8658 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,26 @@ +#![deny( + // Base lints. + clippy::all, + // Some pedantic lints. + clippy::pedantic, + // New lints which are cool. + clippy::nursery, +)] +#![ + allow( + // Beacsue some types naming conventions are + // just different to the ones that rust have. + non_camel_case_types, + // I don't care about this. + clippy::module_name_repetitions, + // Yo, the hell you should put + // it in docs, if signature is clear as sky. + clippy::missing_errors_doc, + // Because pythonic way is + // to have many args with defaults. + clippy::too_many_arguments +)] + pub mod batches; pub mod consistencies; pub mod exceptions; @@ -12,13 +35,17 @@ pub mod query_results; pub mod scylla_cls; pub mod utils; -use pyo3::{pymodule, types::PyModule, PyResult, Python}; +use pyo3::{ + pymodule, + types::{PyModule, PyModuleMethods}, + Bound, PyResult, Python, +}; use crate::utils::add_submodule; #[pymodule] #[pyo3(name = "_internal")] -fn _internal(py: Python<'_>, pymod: &PyModule) -> PyResult<()> { +fn _internal(py: Python<'_>, pymod: &Bound<'_, PyModule>) -> PyResult<()> { pyo3_log::init(); pymod.add_class::()?; pymod.add_class::()?; diff --git a/src/load_balancing.rs b/src/load_balancing.rs index 8f288f1..e57108c 100644 --- a/src/load_balancing.rs +++ b/src/load_balancing.rs @@ -2,15 +2,15 @@ use std::sync::Arc; use pyo3::{ pyclass, pymethods, - types::{PyModule, PyType}, - PyAny, PyResult, Python, + types::{PyModule, PyModuleMethods, PyType}, + Bound, PyAny, PyResult, Python, }; use scylla::load_balancing::{DefaultPolicy, LatencyAwarenessBuilder, LoadBalancingPolicy}; use std::time::Duration; use crate::{exceptions::rust_err::ScyllaPyResult, utils::scyllapy_future}; -#[pyclass(name = "LoadBalancingPolicy")] +#[pyclass(name = "LoadBalancingPolicy", from_py_object)] #[derive(Clone, Debug)] pub struct ScyllaPyLoadBalancingPolicy { inner: Arc, @@ -29,15 +29,15 @@ impl ScyllaPyLoadBalancingPolicy { latency_awareness = None, ) )] - fn build( - cls: &PyType, + fn build<'py>( + cls: &Bound<'py, PyType>, token_aware: Option, prefer_rack: Option, prefer_datacenter: Option, permit_dc_failover: Option, shuffling_replicas: Option, latency_awareness: Option, - ) -> ScyllaPyResult<&PyAny> { + ) -> ScyllaPyResult> { scyllapy_future(cls.py(), async move { let mut policy_builer = DefaultPolicy::builder(); if let Some(permit) = permit_dc_failover { @@ -66,7 +66,7 @@ impl ScyllaPyLoadBalancingPolicy { } } -#[pyclass(name = "LatencyAwareness")] +#[pyclass(name = "LatencyAwareness", from_py_object)] #[derive(Clone, Debug)] pub struct ScyllaPyLatencyAwareness { inner: LatencyAwarenessBuilder, @@ -129,7 +129,7 @@ impl From for Arc { /// # Errors /// /// If cannot add class to the module. -pub fn setup_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { +pub fn setup_module(_py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::()?; module.add_class::()?; Ok(()) diff --git a/src/prepared_queries.rs b/src/prepared_queries.rs index 65ee4f9..62146e9 100644 --- a/src/prepared_queries.rs +++ b/src/prepared_queries.rs @@ -1,7 +1,7 @@ use pyo3::pyclass; use scylla::prepared_statement::PreparedStatement; -#[pyclass(name = "PreparedQuery")] +#[pyclass(name = "PreparedQuery", from_py_object)] #[derive(Clone, Debug)] pub struct ScyllaPyPreparedQuery { pub inner: PreparedStatement, diff --git a/src/queries.rs b/src/queries.rs index 01946c8..ed41f42 100644 --- a/src/queries.rs +++ b/src/queries.rs @@ -5,7 +5,11 @@ use crate::{ exceptions::rust_err::ScyllaPyResult, execution_profiles::ScyllaPyExecutionProfile, }; -use pyo3::{pyclass, pymethods, types::PyDict, FromPyObject, Python}; +use pyo3::{ + pyclass, pymethods, + types::{PyAnyMethods, PyDict, PyDictMethods}, + Bound, FromPyObject, PyErr, Python, +}; use scylla::{batch::Batch, execution_profile::ExecutionProfileHandle, statement::query::Query}; #[derive(Clone, Debug, Default, FromPyObject)] @@ -60,44 +64,44 @@ impl ScyllaPyRequestParams { /// /// May result in an error if /// incorrect type passed. - pub fn from_dict(params: Option<&PyDict>) -> ScyllaPyResult { + pub fn from_dict(params: Option<&Bound<'_, PyDict>>) -> ScyllaPyResult { let Some(params) = params else { return Ok(Self::default()); }; Ok(Self { consistency: params .get_item("consistency")? - .map(pyo3::FromPyObject::extract) + .map(|obj| obj.extract().map_err(PyErr::from)) .transpose()?, serial_consistency: params .get_item("serial_consistency")? - .map(pyo3::FromPyObject::extract) + .map(|obj| obj.extract().map_err(PyErr::from)) .transpose()?, request_timeout: params .get_item("request_timeout")? - .map(pyo3::FromPyObject::extract) + .map(|obj| obj.extract()) .transpose()?, timestamp: params .get_item("timestamp")? - .map(pyo3::FromPyObject::extract) + .map(|obj| obj.extract()) .transpose()?, is_idempotent: params .get_item("is_idempotent")? - .map(pyo3::FromPyObject::extract) + .map(|obj| obj.extract()) .transpose()?, tracing: params .get_item("tracing")? - .map(pyo3::FromPyObject::extract) + .map(|obj| obj.extract()) .transpose()?, profile: params .get_item("profile")? - .map(pyo3::FromPyObject::extract) + .map(|obj| obj.extract().map_err(PyErr::from)) .transpose()?, }) } } -#[pyclass(name = "Query")] +#[pyclass(name = "Query", from_py_object)] #[derive(Clone, Debug)] pub struct ScyllaPyQuery { #[pyo3(get)] @@ -105,9 +109,9 @@ pub struct ScyllaPyQuery { pub params: ScyllaPyRequestParams, } -impl From<&ScyllaPyQuery> for ScyllaPyQuery { - fn from(value: &ScyllaPyQuery) -> Self { - ScyllaPyQuery { +impl From<&Self> for ScyllaPyQuery { + fn from(value: &Self) -> Self { + Self { query: value.query.clone(), params: ScyllaPyRequestParams::default(), } @@ -123,7 +127,11 @@ impl ScyllaPyQuery { /// /// # Errors /// May raise an error if incorrect type passed in kwargs. - pub fn py_new(_py: Python<'_>, query: String, kwargs: Option<&PyDict>) -> ScyllaPyResult { + pub fn py_new( + _py: Python<'_>, + query: String, + kwargs: Option<&Bound<'_, PyDict>>, + ) -> ScyllaPyResult { Ok(Self { query, params: ScyllaPyRequestParams::from_dict(kwargs)?, diff --git a/src/query_builder/delete.rs b/src/query_builder/delete.rs index 8a784f5..718a4a0 100644 --- a/src/query_builder/delete.rs +++ b/src/query_builder/delete.rs @@ -1,4 +1,4 @@ -use pyo3::{pyclass, pymethods, types::PyDict, PyAny, PyRefMut, Python}; +use pyo3::{pyclass, pymethods, types::PyDict, Bound, PyAny, PyRefMut, Python}; use scylla::{frame::value::LegacySerializedValues, query::Query}; use super::utils::{pretty_build, IfCluase, Timeout}; @@ -10,7 +10,7 @@ use crate::{ utils::{py_to_value, ScyllaPyCQLDTO}, }; -#[pyclass] +#[pyclass(skip_from_py_object)] #[derive(Clone, Debug, Default)] pub struct Delete { table_: String, @@ -53,15 +53,15 @@ impl Delete { format!("USING {}", prepared_params.join(" AND ")) }; let where_clause = format!("WHERE {}", self.where_clauses_.join(" AND ")); - let if_conditions = self - .if_clause_ - .as_ref() - .map_or(String::default(), |cond| match cond { - IfCluase::Exists => String::from("IF EXISTS"), - IfCluase::Condition { clauses, values: _ } => { - format!("IF {}", clauses.join(" AND ")) - } - }); + let if_conditions = + self.if_clause_ + .as_ref() + .map_or_else(String::default, |cond| match cond { + IfCluase::Exists => String::from("IF EXISTS"), + IfCluase::Condition { clauses, values: _ } => { + format!("IF {}", clauses.join(" AND ")) + } + }); Ok(pretty_build([ "DELETE", columns.as_str(), @@ -104,12 +104,12 @@ impl Delete { pub fn r#where<'a>( mut slf: PyRefMut<'a, Self>, clause: String, - values: Option>, + values: Option>>, ) -> ScyllaPyResult> { slf.where_clauses_.push(clause); if let Some(vals) = values { for value in vals { - slf.values_.push(py_to_value(value, None)?); + slf.values_.push(py_to_value(&value, None)?); } } Ok(slf) @@ -143,7 +143,7 @@ impl Delete { pub fn if_<'a>( mut slf: PyRefMut<'a, Self>, clause: String, - values: Option>, + values: Option>>, ) -> ScyllaPyResult> { let parsed_values = if let Some(vals) = values { vals.iter() @@ -178,7 +178,7 @@ impl Delete { #[pyo3(signature = (**params))] pub fn request_params<'a>( mut slf: PyRefMut<'a, Self>, - params: Option<&'a PyDict>, + params: Option<&'a Bound<'a, PyDict>>, ) -> ScyllaPyResult> { slf.request_params_ = ScyllaPyRequestParams::from_dict(params)?; Ok(slf) @@ -191,16 +191,21 @@ impl Delete { /// May return an error, if something goes wrong /// during query building /// or during query execution. - pub fn execute<'a>(&'a self, py: Python<'a>, scylla: &'a Scylla) -> ScyllaPyResult<&'a PyAny> { + pub fn execute<'a>( + &'a self, + py: Python<'a>, + scylla: &'a Bound<'a, Scylla>, + ) -> ScyllaPyResult> { let mut query = Query::new(self.build_query()?); self.request_params_.apply_to_query(&mut query); - let values = if let Some(if_clause) = &self.if_clause_ { - if_clause.extend_values(self.values_.clone()) - } else { - self.values_.clone() - }; - scylla.native_execute(py, Some(query), None, values, false) + let values = self.if_clause_.as_ref().map_or_else( + || self.values_.clone(), + |if_clause| if_clause.extend_values(self.values_.clone()), + ); + scylla + .get() + .native_execute(py, Some(query), None, values, false) } /// Add to batch @@ -215,11 +220,10 @@ impl Delete { let mut query = Query::new(self.build_query()?); self.request_params_.apply_to_query(&mut query); - let values = if let Some(if_clause) = &self.if_clause_ { - if_clause.extend_values(self.values_.clone()) - } else { - self.values_.clone() - }; + let values = self.if_clause_.as_ref().map_or_else( + || self.values_.clone(), + |if_clause| if_clause.extend_values(self.values_.clone()), + ); let mut serialized = LegacySerializedValues::new(); for val in values { serialized.add_value(&val)?; @@ -249,7 +253,7 @@ impl Delete { } #[must_use] - pub fn __deepcopy__(&self, _memo: &PyDict) -> Self { + pub fn __deepcopy__(&self, _memo: &Bound<'_, PyDict>) -> Self { self.clone() } } diff --git a/src/query_builder/insert.rs b/src/query_builder/insert.rs index 67ed097..a65f606 100644 --- a/src/query_builder/insert.rs +++ b/src/query_builder/insert.rs @@ -1,4 +1,8 @@ -use pyo3::{pyclass, pymethods, types::PyDict, PyAny, PyRefMut, Python}; +use pyo3::{ + pyclass, pymethods, + types::{PyAnyMethods, PyDict}, + Bound, PyAny, PyRefMut, Python, +}; use scylla::{frame::value::LegacySerializedValues, query::Query}; use crate::{ @@ -11,8 +15,9 @@ use crate::{ use super::utils::{pretty_build, Timeout}; -#[pyclass] +#[pyclass(skip_from_py_object)] #[derive(Clone, Debug, Default)] +#[allow(clippy::struct_field_names)] pub struct Insert { table_: String, if_not_exists_: bool, @@ -106,7 +111,7 @@ impl Insert { pub fn set<'a>( mut slf: PyRefMut<'a, Self>, name: String, - value: &'a PyAny, + value: &'a Bound<'a, PyAny>, ) -> ScyllaPyResult> { slf.names_.push(name); // Small optimization to speedup inserts. @@ -147,7 +152,7 @@ impl Insert { #[pyo3(signature = (**params))] pub fn request_params<'a>( mut slf: PyRefMut<'a, Self>, - params: Option<&'a PyDict>, + params: Option<&'a Bound<'a, PyDict>>, ) -> ScyllaPyResult> { slf.request_params_ = ScyllaPyRequestParams::from_dict(params)?; Ok(slf) @@ -161,10 +166,16 @@ impl Insert { /// /// If query cannot be built. /// Also proxies errors from `native_execute`. - pub fn execute<'a>(&'a self, py: Python<'a>, scylla: &'a Scylla) -> ScyllaPyResult<&'a PyAny> { + pub fn execute<'a>( + &'a self, + py: Python<'a>, + scylla: &'a Bound<'a, Scylla>, + ) -> ScyllaPyResult> { let mut query = Query::new(self.build_query()?); self.request_params_.apply_to_query(&mut query); - scylla.native_execute(py, Some(query), None, self.values_.clone(), false) + scylla + .get() + .native_execute(py, Some(query), None, self.values_.clone(), false) } /// Add to batch @@ -206,7 +217,7 @@ impl Insert { } #[must_use] - pub fn __deepcopy__(&self, _memo: &PyDict) -> Self { + pub fn __deepcopy__(&self, _memo: &Bound<'_, PyDict>) -> Self { self.clone() } } diff --git a/src/query_builder/mod.rs b/src/query_builder/mod.rs index ba1b979..c908432 100644 --- a/src/query_builder/mod.rs +++ b/src/query_builder/mod.rs @@ -1,4 +1,7 @@ -use pyo3::{types::PyModule, PyResult, Python}; +use pyo3::{ + types::{PyModule, PyModuleMethods}, + Bound, PyResult, Python, +}; use self::{delete::Delete, insert::Insert, select::Select, update::Update}; @@ -17,7 +20,7 @@ mod utils; /// /// * Cannot create module by any reason. /// * Cannot add class by some reason. -pub fn setup_module(_py: Python<'_>, module: &PyModule) -> PyResult<()> { +pub fn setup_module(_py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_class::