From fd6d425ad168c3a4f45827542d0e72cd22c6d8ef Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Thu, 5 Dec 2024 14:44:55 +0100 Subject: [PATCH 1/5] Fix coroutine not awaited warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a certain deadline passed, the driver will not even attempt certain IO operations. By not calling the coroutine function to then ignore it under these circumstances (causing the warning), but instead deferring creation of the coroutine until it is clear we want to await it, we avoid the warning. Functionally, this changes nothing, but makes the driver less noise (and probably a better async citizen 🏅). --- .../_async_compat/network/_bolt_socket.py | 12 +- tests/unit/mixed/async_compat/test_network.py | 177 ++++++++++++++++++ 2 files changed, 182 insertions(+), 7 deletions(-) create mode 100644 tests/unit/mixed/async_compat/test_network.py diff --git a/src/neo4j/_async_compat/network/_bolt_socket.py b/src/neo4j/_async_compat/network/_bolt_socket.py index 83a663e57..357e7410f 100644 --- a/src/neo4j/_async_compat/network/_bolt_socket.py +++ b/src/neo4j/_async_compat/network/_bolt_socket.py @@ -98,7 +98,7 @@ def __init__(self, reader, protocol, writer): self._timeout = None self._deadline = None - async def _wait_for_io(self, io_fut): + async def _wait_for_io(self, io_async_fn, *args, **kwargs): timeout = self._timeout to_raise = SocketTimeout if self._deadline is not None: @@ -109,6 +109,7 @@ async def _wait_for_io(self, io_fut): timeout = deadline_timeout to_raise = SocketDeadlineExceededError + io_fut = io_async_fn(*args, **kwargs) if timeout is not None and timeout <= 0: # give the io-operation time for one loop cycle to do its thing io_fut = asyncio.create_task(io_fut) @@ -157,20 +158,17 @@ def settimeout(self, timeout): self._timeout = timeout async def recv(self, n): - io_fut = self._reader.read(n) - return await self._wait_for_io(io_fut) + return await self._wait_for_io(self._reader.read, n) async def recv_into(self, buffer, nbytes): # FIXME: not particularly memory or time efficient - io_fut = self._reader.read(nbytes) - res = await self._wait_for_io(io_fut) + res = await self._wait_for_io(self._reader.read, nbytes) buffer[: len(res)] = res return len(res) async def sendall(self, data): self._writer.write(data) - io_fut = self._writer.drain() - return await self._wait_for_io(io_fut) + return await self._wait_for_io(self._writer.drain) async def close(self): self._writer.close() diff --git a/tests/unit/mixed/async_compat/test_network.py b/tests/unit/mixed/async_compat/test_network.py new file mode 100644 index 000000000..08b19a069 --- /dev/null +++ b/tests/unit/mixed/async_compat/test_network.py @@ -0,0 +1,177 @@ +# Copyright (c) "Neo4j" +# Neo4j Sweden AB [https://neo4j.com] +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from __future__ import annotations + +import asyncio +import typing as t + +import freezegun +import pytest + +from neo4j._async_compat.network import AsyncBoltSocket +from neo4j._exceptions import SocketDeadlineExceededError + +from ...._async_compat.mark_decorator import mark_async_test + + +if t.TYPE_CHECKING: + import typing_extensions as te + from freezegun.api import ( + FrozenDateTimeFactory, + StepTickTimeFactory, + TickingDateTimeFactory, + ) + + TFreezeTime: te.TypeAlias = ( + StepTickTimeFactory | TickingDateTimeFactory | FrozenDateTimeFactory + ) + + +@pytest.fixture +def reader_factory(mocker): + # class MockReader(mocker.Mock): + # def __init__(self, data=None): + # super().__init__(spec=asyncio.StreamReader) + # self._data = data + # self._i = 0 + # + # if data is not None: + # def read_side_effect(n): + # from_ = self._i + # self._i += n + # return self._data[from_ : self._i] + # + # self.read.side_effect = read_side_effect + def factory(): + return mocker.create_autospec(asyncio.StreamReader) + + return factory + + +@pytest.fixture +def writer_factory(mocker): + def factory(): + return mocker.create_autospec(asyncio.StreamWriter) + + return factory + # return mocker.Mock(spec=asyncio.StreamWriter) + + +@pytest.fixture +def socket_factory(reader_factory, writer_factory): + def factory(): + protocol = None + return AsyncBoltSocket(reader_factory(), protocol, writer_factory()) + + return factory + + +def reader(s: AsyncBoltSocket): + return s._reader + + +def writer(s: AsyncBoltSocket): + return s._writer + + +@pytest.mark.parametrize( + ("timeout", "deadline", "pre_tick", "tick", "exception"), + ( + (None, None, 60 * 60 * 10, 60 * 60 * 10, None), + # test timeout + (5, None, 0, 4, None), + # timeout is not affected by time passed before the call + (5, None, 7, 4, None), + (5, None, 0, 6, TimeoutError), + # test deadline + (None, 5, 0, 4, None), + (None, 5, 2, 2, None), + # deadline is affected by time passed before the call + (None, 5, 2, 4, SocketDeadlineExceededError), + (None, 5, 6, 0, SocketDeadlineExceededError), + (None, 5, 0, 6, SocketDeadlineExceededError), + # test combination + (5, 5, 0, 4, None), + (5, 5, 2, 2, None), + # deadline triggered by time passed before + (5, 5, 2, 4, SocketDeadlineExceededError), + # the shorter one determines the error + (4, 5, 0, 6, TimeoutError), + (5, 4, 0, 6, SocketDeadlineExceededError), + ), +) +@pytest.mark.parametrize("method", ("recv", "recv_into", "sendall")) +@mark_async_test +async def test_async_bolt_socket_read_timeout( + socket_factory, timeout, deadline, pre_tick, tick, exception, method +): + def make_read_side_effect(freeze_time: TFreezeTime): + async def read_side_effect(n): + assert n == 1 + freeze_time.tick(tick) + for _ in range(10): + await asyncio.sleep(0) + return b"y" + + return read_side_effect + + def make_drain_side_effect(freeze_time: TFreezeTime): + async def drain_side_effect(): + freeze_time.tick(tick) + for _ in range(10): + await asyncio.sleep(0) + + return drain_side_effect + + async def call_method(s: AsyncBoltSocket): + if method == "recv": + res = await s.recv(1) + assert res == b"y" + elif method == "recv_into": + b = bytearray(1) + await s.recv_into(b, 1) + assert b == b"y" + elif method == "sendall": + await s.sendall(b"y") + else: + raise NotImplementedError(f"method: {method}") + + with freezegun.freeze_time("1970-01-01T00:00:00") as frozen_time: + socket = socket_factory() + if timeout is not None: + socket.settimeout(timeout) + if deadline is not None: + socket.set_deadline(deadline) + if pre_tick: + frozen_time.tick(pre_tick) + + if method in {"recv", "recv_into"}: + reader(socket).read.side_effect = make_read_side_effect( + frozen_time + ) + elif method == "sendall": + writer(socket).drain.side_effect = make_drain_side_effect( + frozen_time + ) + else: + raise NotImplementedError(f"method: {method}") + + if exception: + with pytest.raises(exception): + await call_method(socket) + else: + await call_method(socket) From 8400afed6be94e2c833c18645a1703375e6f693b Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Fri, 6 Dec 2024 09:44:26 +0100 Subject: [PATCH 2/5] Fix tests compatibility with Python 3.7-3.9 --- tests/unit/mixed/async_compat/test_network.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/mixed/async_compat/test_network.py b/tests/unit/mixed/async_compat/test_network.py index 08b19a069..9782d9718 100644 --- a/tests/unit/mixed/async_compat/test_network.py +++ b/tests/unit/mixed/async_compat/test_network.py @@ -17,6 +17,7 @@ from __future__ import annotations import asyncio +import socket import typing as t import freezegun @@ -96,7 +97,7 @@ def writer(s: AsyncBoltSocket): (5, None, 0, 4, None), # timeout is not affected by time passed before the call (5, None, 7, 4, None), - (5, None, 0, 6, TimeoutError), + (5, None, 0, 6, socket.timeout), # test deadline (None, 5, 0, 4, None), (None, 5, 2, 2, None), @@ -110,7 +111,7 @@ def writer(s: AsyncBoltSocket): # deadline triggered by time passed before (5, 5, 2, 4, SocketDeadlineExceededError), # the shorter one determines the error - (4, 5, 0, 6, TimeoutError), + (4, 5, 0, 6, socket.timeout), (5, 4, 0, 6, SocketDeadlineExceededError), ), ) From 6e88c40b4042c27c449ffba44c8029cd442f3775 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Fri, 6 Dec 2024 13:53:58 +0100 Subject: [PATCH 3/5] Bump dependencies & enable Python 3.7 dev env --- .pre-commit-config.yaml | 9 ++- benchkit/app.py | 6 +- benchkit/workloads.py | 4 +- pyproject.toml | 1 + requirements-dev.txt | 37 +++++---- src/neo4j/_async/work/result.py | 4 +- src/neo4j/_spatial/__init__.py | 3 +- src/neo4j/_sync/work/result.py | 4 +- src/neo4j/api.py | 3 +- src/neo4j/time/__init__.py | 3 +- testkit/Dockerfile | 15 ---- tests/unit/async_/test_driver.py | 9 +-- tests/unit/common/test_record.py | 75 +++++++++++-------- tests/unit/mixed/async_compat/test_network.py | 6 +- tests/unit/sync/test_driver.py | 9 +-- 15 files changed, 96 insertions(+), 92 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c9805d972..4161af7dd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,10 +19,13 @@ repos: - batch - id: trailing-whitespace args: [ --markdown-linebreak-ext=md ] - - repo: https://github.com/pycqa/isort - rev: 5.12.0 + - repo: local hooks: - id: isort + name: isort + entry: isort + types_or: [ python, pyi ] + language: system - repo: https://github.com/sphinx-contrib/sphinx-lint rev: e83a1a42a73284d301c05baaffc176042ffbcf82 hooks: @@ -45,7 +48,7 @@ repos: name: mypy static type check entry: mypy args: [ --show-error-codes, src, tests, testkitbackend, benchkit ] - 'types_or': [ python, pyi ] + types_or: [ python, pyi ] language: system pass_filenames: false require_serial: true diff --git a/benchkit/app.py b/benchkit/app.py index a2e9e756e..b4e56853c 100644 --- a/benchkit/app.py +++ b/benchkit/app.py @@ -16,6 +16,7 @@ from __future__ import annotations +import sys import typing as t from contextlib import contextmanager from multiprocessing import Semaphore @@ -43,7 +44,10 @@ from .workloads import Workload -T_App: te.TypeAlias = "Sanic[Config, BenchKitContext]" +if sys.version_info < (3, 8): + T_App: te.TypeAlias = "Sanic" +else: + T_App: te.TypeAlias = "Sanic[Config, BenchKitContext]" def create_app() -> T_App: diff --git a/benchkit/workloads.py b/benchkit/workloads.py index 8d86901f6..21c2c6506 100644 --- a/benchkit/workloads.py +++ b/benchkit/workloads.py @@ -543,7 +543,7 @@ def parse(cls, query: t.Any) -> te.Self: @dataclass class _WorkloadConfig: database: str | None - routing: t.Literal["r", "w"] + routing: te.Literal["r", "w"] @classmethod def parse(cls, data: t.Any) -> te.Self: @@ -553,7 +553,7 @@ def parse(cls, data: t.Any) -> te.Self: if not isinstance(database, str): raise TypeError("Workload database must be a string") - routing: t.Literal["r", "w"] = "w" + routing: te.Literal["r", "w"] = "w" if "routing" in data: raw_routing = data["routing"] if not isinstance(routing, str): diff --git a/pyproject.toml b/pyproject.toml index 06affa14f..d37802ce0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,6 +123,7 @@ use_parentheses = true [tool.pytest.ini_options] mock_use_standalone_module = true asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" [tool.mypy] diff --git a/requirements-dev.txt b/requirements-dev.txt index 5a22e864c..5a6da94fa 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,32 +2,31 @@ -e .[pandas,numpy,pyarrow] # needed for packaging -build +build>=1.1.1 # TODO: 6.0 - bump when support for Python 3.7 is dropped # auto-generate sync driver from async code -unasync>=0.5.0 +unasync==0.5.0 # pre-commit hooks and tools -pre-commit>=2.15.0 -isort>=5.10.0 -mypy>=0.971 -typing-extensions>=4.3.0 -types-pytz>=2022.1.2 -ruff>=0.6.4 +pre-commit>=2.21.0 # TODO: 6.0 - bump when support for Python 3.7 is dropped +isort>=5.11.5 # TODO: 6.0 - bump when support for Python 3.7 is dropped +mypy>=1.4.1 # TODO: 6.0 - bump when support for Python 3.7 is dropped +typing-extensions>=4.7.1 +types-pytz>=2023.3.1.1 # TODO: 6.0 - bump when support for Python 3.7 is dropped +ruff>=0.8.2 # needed for running tests -coverage[toml]>=5.5 +coverage[toml]>=7.2.7 # TODO: 6.0 - bump when support for Python 3.7 is dropped freezegun>=1.5.1 -mock>=4.0.3 -pytest>=6.2.5 -pytest-asyncio>=0.16.0 -pytest-benchmark>=3.4.1 -pytest-cov>=3.0.0 -pytest-mock>=3.6.1 -tox>=4.0.0 -teamcity-messages>=1.32 +mock>=5.1.0 +pytest>=7.4.4 # TODO: 6.0 - bump when support for Python 3.7 is dropped +pytest-asyncio~=0.21.2 +pytest-benchmark>=4.0.0 +pytest-cov>=4.1.0 # TODO: 6.0 - bump when support for Python 3.7 is dropped +pytest-mock>=3.11.1 # TODO: 6.0 - bump when support for Python 3.7 is dropped +tox>=4.8.0 # TODO: 6.0 - bump when support for Python 3.7 is dropped # needed for building docs -sphinx +Sphinx>=5.3.0 # TODO: 6.0 - bump when support for Python 3.7 is dropped # needed for BenchKit -sanic>=23.12.1 ; python_version >= '3.8.0' +sanic>=23.3.0 # TODO: 6.0 - bump when support for Python 3.7 is dropped diff --git a/src/neo4j/_async/work/result.py b/src/neo4j/_async/work/result.py index 696358de5..ac62fa1f9 100644 --- a/src/neo4j/_async/work/result.py +++ b/src/neo4j/_async/work/result.py @@ -105,7 +105,7 @@ class AsyncResult(AsyncNonConcurrentMethodChecker): """ _creation_stack: list[inspect.FrameInfo] | None - _creation_frame_cache: None | t.Literal[False] | inspect.FrameInfo + _creation_frame_cache: None | te.Literal[False] | inspect.FrameInfo def __init__( self, @@ -356,7 +356,7 @@ def _handle_warnings(self) -> None: ) @property - def _creation_frame(self) -> t.Literal[False] | inspect.FrameInfo: + def _creation_frame(self) -> te.Literal[False] | inspect.FrameInfo: if self._creation_frame_cache is not None: return self._creation_frame_cache diff --git a/src/neo4j/_spatial/__init__.py b/src/neo4j/_spatial/__init__.py index 20da1abdb..bf6f19c47 100644 --- a/src/neo4j/_spatial/__init__.py +++ b/src/neo4j/_spatial/__init__.py @@ -58,7 +58,8 @@ def y(self) -> float: ... def z(self) -> float: ... def __new__(cls, iterable: t.Iterable[float]) -> Point: - return tuple.__new__(cls, map(float, iterable)) + # TODO: 6.0 - remove type ignore when support for Python 3.7 is dropped + return tuple.__new__(cls, map(float, iterable)) # type: ignore[type-var] def __repr__(self) -> str: return f"POINT({' '.join(map(str, self))})" diff --git a/src/neo4j/_sync/work/result.py b/src/neo4j/_sync/work/result.py index f343efa2c..27164cf84 100644 --- a/src/neo4j/_sync/work/result.py +++ b/src/neo4j/_sync/work/result.py @@ -105,7 +105,7 @@ class Result(NonConcurrentMethodChecker): """ _creation_stack: list[inspect.FrameInfo] | None - _creation_frame_cache: None | t.Literal[False] | inspect.FrameInfo + _creation_frame_cache: None | te.Literal[False] | inspect.FrameInfo def __init__( self, @@ -356,7 +356,7 @@ def _handle_warnings(self) -> None: ) @property - def _creation_frame(self) -> t.Literal[False] | inspect.FrameInfo: + def _creation_frame(self) -> te.Literal[False] | inspect.FrameInfo: if self._creation_frame_cache is not None: return self._creation_frame_cache diff --git a/src/neo4j/api.py b/src/neo4j/api.py index 160f7c973..3d541f9f7 100644 --- a/src/neo4j/api.py +++ b/src/neo4j/api.py @@ -385,7 +385,8 @@ def protocol_version(self) -> tuple[int, int]: This is returned as a 2-tuple:class:`tuple` (subclass) of ``(major, minor)`` integers. """ - return self._protocol_version + # TODO: 6.0 - remove cast when support for Python 3.7 is dropped + return t.cast(tuple[int, int], self._protocol_version) @property def agent(self) -> str: diff --git a/src/neo4j/time/__init__.py b/src/neo4j/time/__init__.py index 4f8ad101b..cfee552d7 100644 --- a/src/neo4j/time/__init__.py +++ b/src/neo4j/time/__init__.py @@ -470,7 +470,8 @@ def __new__( ) if not MIN_INT64 <= avg_total_seconds <= MAX_INT64: raise ValueError(f"Duration value out of range: {tuple_!r}") - return tuple.__new__(cls, tuple_) + # TODO: 6.0 - remove type ignore when support for Python 3.7 is dropped + return tuple.__new__(cls, tuple_) # type: ignore[type-var] def __bool__(self) -> bool: """Falsy if all primary instance attributes are.""" diff --git a/testkit/Dockerfile b/testkit/Dockerfile index 8b6b9d63f..e61135da1 100644 --- a/testkit/Dockerfile +++ b/testkit/Dockerfile @@ -57,18 +57,3 @@ RUN for version in $PYTHON_VERSIONS; do \ python$version -m pip install -U pip && \ python$version -m pip install -U coverage tox; \ done - -# Installing pyarrow lib until pre-built wheel for Python 3.13 exists -# https://github.com/apache/arrow/issues/43519 -RUN apt update && \ - apt install -y -V lsb-release cmake gcc && \ - distro_name=$(lsb_release --id --short | tr 'A-Z' 'a-z') && \ - code_name=$(lsb_release --codename --short) && \ - wget https://apache.jfrog.io/artifactory/arrow/${distro_name}/apache-arrow-apt-source-latest-${code_name}.deb && \ - apt install -y -V ./apache-arrow-apt-source-latest-${code_name}.deb && \ - apt update && \ - apt install -y -V libarrow-dev libarrow-dataset-dev libarrow-flight-dev libparquet-dev && \ - apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -ENV PYARROW_WITH_CUDA=off -ENV PYARROW_WITH_GANDIVA=off -ENV PYARROW_PARALLEL=8 diff --git a/tests/unit/async_/test_driver.py b/tests/unit/async_/test_driver.py index 53d287102..71ba0e90d 100644 --- a/tests/unit/async_/test_driver.py +++ b/tests/unit/async_/test_driver.py @@ -780,11 +780,10 @@ async def test_warn_notification_severity_driver_config( if min_sev_session is ...: session = driver.session() else: - session = driver.session( - # Works at runtime (will be ignored), but should be rejected by - # type checkers. - # type: ignore[call-arg] - warn_notification_severity=min_sev_session + # Works at runtime (will be ignored), but should be rejected by + # type checkers. + session = driver.session( # type: ignore[call-arg] + warn_notification_severity=min_sev_session, ) async with session: session_cls_mock.assert_called_once() diff --git a/tests/unit/common/test_record.py b/tests/unit/common/test_record.py index b3c38301a..0f06fbe2c 100644 --- a/tests/unit/common/test_record.py +++ b/tests/unit/common/test_record.py @@ -17,6 +17,7 @@ from __future__ import annotations import traceback +import typing as t import pytest import pytz @@ -166,39 +167,49 @@ def test_record_data_keys(keys, expected) -> None: ( *( (value, value) - for value in ( - None, - True, - False, - 0, - 1, - -1, - 2147483647, - -2147483648, - 3.141592653589, - "", - "Hello, world!", - "👋, 🌍!", - [], - [1, 2.0, "3", True, None], - {"foo": ["bar", 1]}, - b"", - b"foobar", - Date(2021, 1, 1), - Time(12, 34, 56, 123456789), - Time(1, 2, 3, 4, pytz.FixedOffset(60)), - DateTime(2021, 1, 1, 12, 34, 56, 123456789), - DateTime( - 2018, 10, 12, 11, 37, 41, 474716862, pytz.FixedOffset(60) - ), - pytz.timezone("Europe/Stockholm").localize( - DateTime(2018, 10, 12, 11, 37, 41, 474716862) + for value in t.cast( + t.Tuple[t.Any], + ( + None, + True, + False, + 0, + 1, + -1, + 2147483647, + -2147483648, + 3.141592653589, + "", + "Hello, world!", + "👋, 🌍!", + [], + [1, 2.0, "3", True, None], + {"foo": ["bar", 1]}, + b"", + b"foobar", + Date(2021, 1, 1), + Time(12, 34, 56, 123456789), + Time(1, 2, 3, 4, pytz.FixedOffset(60)), + DateTime(2021, 1, 1, 12, 34, 56, 123456789), + DateTime( + 2018, + 10, + 12, + 11, + 37, + 41, + 474716862, + pytz.FixedOffset(60), + ), + pytz.timezone("Europe/Stockholm").localize( + DateTime(2018, 10, 12, 11, 37, 41, 474716862) + ), + Duration(1, 2, 3, 4, 5, 6, 7), + CartesianPoint((1, 2.0)), + CartesianPoint((1, 2.0, 3)), + WGS84Point((1, 2.0)), + WGS84Point((1, 2.0, 3)), ), - Duration(1, 2, 3, 4, 5, 6, 7), - CartesianPoint((1, 2.0)), - CartesianPoint((1, 2.0, 3)), - WGS84Point((1, 2.0)), - WGS84Point((1, 2.0, 3)), ) ), *( diff --git a/tests/unit/mixed/async_compat/test_network.py b/tests/unit/mixed/async_compat/test_network.py index 9782d9718..2827c837e 100644 --- a/tests/unit/mixed/async_compat/test_network.py +++ b/tests/unit/mixed/async_compat/test_network.py @@ -37,9 +37,9 @@ TickingDateTimeFactory, ) - TFreezeTime: te.TypeAlias = ( - StepTickTimeFactory | TickingDateTimeFactory | FrozenDateTimeFactory - ) + TFreezeTime: te.TypeAlias = t.Union[ + StepTickTimeFactory, TickingDateTimeFactory, FrozenDateTimeFactory + ] @pytest.fixture diff --git a/tests/unit/sync/test_driver.py b/tests/unit/sync/test_driver.py index a508c108e..b90ebd52d 100644 --- a/tests/unit/sync/test_driver.py +++ b/tests/unit/sync/test_driver.py @@ -779,11 +779,10 @@ def test_warn_notification_severity_driver_config( if min_sev_session is ...: session = driver.session() else: - session = driver.session( - # Works at runtime (will be ignored), but should be rejected by - # type checkers. - # type: ignore[call-arg] - warn_notification_severity=min_sev_session + # Works at runtime (will be ignored), but should be rejected by + # type checkers. + session = driver.session( # type: ignore[call-arg] + warn_notification_severity=min_sev_session, ) with session: session_cls_mock.assert_called_once() From 5dbf63f802cc9b5eb5b166b51b6110bc5db62d05 Mon Sep 17 00:00:00 2001 From: MaxAake <61233757+MaxAake@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:16:01 +0100 Subject: [PATCH 4/5] Clean-up Signed-off-by: Rouven Bauer --- tests/unit/mixed/async_compat/test_network.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/unit/mixed/async_compat/test_network.py b/tests/unit/mixed/async_compat/test_network.py index 2827c837e..876cd8b32 100644 --- a/tests/unit/mixed/async_compat/test_network.py +++ b/tests/unit/mixed/async_compat/test_network.py @@ -44,19 +44,6 @@ @pytest.fixture def reader_factory(mocker): - # class MockReader(mocker.Mock): - # def __init__(self, data=None): - # super().__init__(spec=asyncio.StreamReader) - # self._data = data - # self._i = 0 - # - # if data is not None: - # def read_side_effect(n): - # from_ = self._i - # self._i += n - # return self._data[from_ : self._i] - # - # self.read.side_effect = read_side_effect def factory(): return mocker.create_autospec(asyncio.StreamReader) @@ -69,7 +56,6 @@ def factory(): return mocker.create_autospec(asyncio.StreamWriter) return factory - # return mocker.Mock(spec=asyncio.StreamWriter) @pytest.fixture From 0c083f5ac0ebdef6a11c63a10ce0534744124dc8 Mon Sep 17 00:00:00 2001 From: Robsdedude Date: Mon, 9 Dec 2024 12:17:29 +0100 Subject: [PATCH 5/5] Revert "Bump dependencies & enable Python 3.7 dev env" This reverts commit 6e88c40b4042c27c449ffba44c8029cd442f3775. --- .pre-commit-config.yaml | 9 +-- benchkit/app.py | 6 +- benchkit/workloads.py | 4 +- pyproject.toml | 1 - requirements-dev.txt | 37 ++++----- src/neo4j/_async/work/result.py | 4 +- src/neo4j/_spatial/__init__.py | 3 +- src/neo4j/_sync/work/result.py | 4 +- src/neo4j/api.py | 3 +- src/neo4j/time/__init__.py | 3 +- testkit/Dockerfile | 15 ++++ tests/unit/async_/test_driver.py | 9 ++- tests/unit/common/test_record.py | 75 ++++++++----------- tests/unit/mixed/async_compat/test_network.py | 6 +- tests/unit/sync/test_driver.py | 9 ++- 15 files changed, 92 insertions(+), 96 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4161af7dd..c9805d972 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,13 +19,10 @@ repos: - batch - id: trailing-whitespace args: [ --markdown-linebreak-ext=md ] - - repo: local + - repo: https://github.com/pycqa/isort + rev: 5.12.0 hooks: - id: isort - name: isort - entry: isort - types_or: [ python, pyi ] - language: system - repo: https://github.com/sphinx-contrib/sphinx-lint rev: e83a1a42a73284d301c05baaffc176042ffbcf82 hooks: @@ -48,7 +45,7 @@ repos: name: mypy static type check entry: mypy args: [ --show-error-codes, src, tests, testkitbackend, benchkit ] - types_or: [ python, pyi ] + 'types_or': [ python, pyi ] language: system pass_filenames: false require_serial: true diff --git a/benchkit/app.py b/benchkit/app.py index b4e56853c..a2e9e756e 100644 --- a/benchkit/app.py +++ b/benchkit/app.py @@ -16,7 +16,6 @@ from __future__ import annotations -import sys import typing as t from contextlib import contextmanager from multiprocessing import Semaphore @@ -44,10 +43,7 @@ from .workloads import Workload -if sys.version_info < (3, 8): - T_App: te.TypeAlias = "Sanic" -else: - T_App: te.TypeAlias = "Sanic[Config, BenchKitContext]" +T_App: te.TypeAlias = "Sanic[Config, BenchKitContext]" def create_app() -> T_App: diff --git a/benchkit/workloads.py b/benchkit/workloads.py index 21c2c6506..8d86901f6 100644 --- a/benchkit/workloads.py +++ b/benchkit/workloads.py @@ -543,7 +543,7 @@ def parse(cls, query: t.Any) -> te.Self: @dataclass class _WorkloadConfig: database: str | None - routing: te.Literal["r", "w"] + routing: t.Literal["r", "w"] @classmethod def parse(cls, data: t.Any) -> te.Self: @@ -553,7 +553,7 @@ def parse(cls, data: t.Any) -> te.Self: if not isinstance(database, str): raise TypeError("Workload database must be a string") - routing: te.Literal["r", "w"] = "w" + routing: t.Literal["r", "w"] = "w" if "routing" in data: raw_routing = data["routing"] if not isinstance(routing, str): diff --git a/pyproject.toml b/pyproject.toml index d37802ce0..06affa14f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,6 @@ use_parentheses = true [tool.pytest.ini_options] mock_use_standalone_module = true asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" [tool.mypy] diff --git a/requirements-dev.txt b/requirements-dev.txt index 5a6da94fa..5a22e864c 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,31 +2,32 @@ -e .[pandas,numpy,pyarrow] # needed for packaging -build>=1.1.1 # TODO: 6.0 - bump when support for Python 3.7 is dropped +build # auto-generate sync driver from async code -unasync==0.5.0 +unasync>=0.5.0 # pre-commit hooks and tools -pre-commit>=2.21.0 # TODO: 6.0 - bump when support for Python 3.7 is dropped -isort>=5.11.5 # TODO: 6.0 - bump when support for Python 3.7 is dropped -mypy>=1.4.1 # TODO: 6.0 - bump when support for Python 3.7 is dropped -typing-extensions>=4.7.1 -types-pytz>=2023.3.1.1 # TODO: 6.0 - bump when support for Python 3.7 is dropped -ruff>=0.8.2 +pre-commit>=2.15.0 +isort>=5.10.0 +mypy>=0.971 +typing-extensions>=4.3.0 +types-pytz>=2022.1.2 +ruff>=0.6.4 # needed for running tests -coverage[toml]>=7.2.7 # TODO: 6.0 - bump when support for Python 3.7 is dropped +coverage[toml]>=5.5 freezegun>=1.5.1 -mock>=5.1.0 -pytest>=7.4.4 # TODO: 6.0 - bump when support for Python 3.7 is dropped -pytest-asyncio~=0.21.2 -pytest-benchmark>=4.0.0 -pytest-cov>=4.1.0 # TODO: 6.0 - bump when support for Python 3.7 is dropped -pytest-mock>=3.11.1 # TODO: 6.0 - bump when support for Python 3.7 is dropped -tox>=4.8.0 # TODO: 6.0 - bump when support for Python 3.7 is dropped +mock>=4.0.3 +pytest>=6.2.5 +pytest-asyncio>=0.16.0 +pytest-benchmark>=3.4.1 +pytest-cov>=3.0.0 +pytest-mock>=3.6.1 +tox>=4.0.0 +teamcity-messages>=1.32 # needed for building docs -Sphinx>=5.3.0 # TODO: 6.0 - bump when support for Python 3.7 is dropped +sphinx # needed for BenchKit -sanic>=23.3.0 # TODO: 6.0 - bump when support for Python 3.7 is dropped +sanic>=23.12.1 ; python_version >= '3.8.0' diff --git a/src/neo4j/_async/work/result.py b/src/neo4j/_async/work/result.py index ac62fa1f9..696358de5 100644 --- a/src/neo4j/_async/work/result.py +++ b/src/neo4j/_async/work/result.py @@ -105,7 +105,7 @@ class AsyncResult(AsyncNonConcurrentMethodChecker): """ _creation_stack: list[inspect.FrameInfo] | None - _creation_frame_cache: None | te.Literal[False] | inspect.FrameInfo + _creation_frame_cache: None | t.Literal[False] | inspect.FrameInfo def __init__( self, @@ -356,7 +356,7 @@ def _handle_warnings(self) -> None: ) @property - def _creation_frame(self) -> te.Literal[False] | inspect.FrameInfo: + def _creation_frame(self) -> t.Literal[False] | inspect.FrameInfo: if self._creation_frame_cache is not None: return self._creation_frame_cache diff --git a/src/neo4j/_spatial/__init__.py b/src/neo4j/_spatial/__init__.py index bf6f19c47..20da1abdb 100644 --- a/src/neo4j/_spatial/__init__.py +++ b/src/neo4j/_spatial/__init__.py @@ -58,8 +58,7 @@ def y(self) -> float: ... def z(self) -> float: ... def __new__(cls, iterable: t.Iterable[float]) -> Point: - # TODO: 6.0 - remove type ignore when support for Python 3.7 is dropped - return tuple.__new__(cls, map(float, iterable)) # type: ignore[type-var] + return tuple.__new__(cls, map(float, iterable)) def __repr__(self) -> str: return f"POINT({' '.join(map(str, self))})" diff --git a/src/neo4j/_sync/work/result.py b/src/neo4j/_sync/work/result.py index 27164cf84..f343efa2c 100644 --- a/src/neo4j/_sync/work/result.py +++ b/src/neo4j/_sync/work/result.py @@ -105,7 +105,7 @@ class Result(NonConcurrentMethodChecker): """ _creation_stack: list[inspect.FrameInfo] | None - _creation_frame_cache: None | te.Literal[False] | inspect.FrameInfo + _creation_frame_cache: None | t.Literal[False] | inspect.FrameInfo def __init__( self, @@ -356,7 +356,7 @@ def _handle_warnings(self) -> None: ) @property - def _creation_frame(self) -> te.Literal[False] | inspect.FrameInfo: + def _creation_frame(self) -> t.Literal[False] | inspect.FrameInfo: if self._creation_frame_cache is not None: return self._creation_frame_cache diff --git a/src/neo4j/api.py b/src/neo4j/api.py index 3d541f9f7..160f7c973 100644 --- a/src/neo4j/api.py +++ b/src/neo4j/api.py @@ -385,8 +385,7 @@ def protocol_version(self) -> tuple[int, int]: This is returned as a 2-tuple:class:`tuple` (subclass) of ``(major, minor)`` integers. """ - # TODO: 6.0 - remove cast when support for Python 3.7 is dropped - return t.cast(tuple[int, int], self._protocol_version) + return self._protocol_version @property def agent(self) -> str: diff --git a/src/neo4j/time/__init__.py b/src/neo4j/time/__init__.py index cfee552d7..4f8ad101b 100644 --- a/src/neo4j/time/__init__.py +++ b/src/neo4j/time/__init__.py @@ -470,8 +470,7 @@ def __new__( ) if not MIN_INT64 <= avg_total_seconds <= MAX_INT64: raise ValueError(f"Duration value out of range: {tuple_!r}") - # TODO: 6.0 - remove type ignore when support for Python 3.7 is dropped - return tuple.__new__(cls, tuple_) # type: ignore[type-var] + return tuple.__new__(cls, tuple_) def __bool__(self) -> bool: """Falsy if all primary instance attributes are.""" diff --git a/testkit/Dockerfile b/testkit/Dockerfile index e61135da1..8b6b9d63f 100644 --- a/testkit/Dockerfile +++ b/testkit/Dockerfile @@ -57,3 +57,18 @@ RUN for version in $PYTHON_VERSIONS; do \ python$version -m pip install -U pip && \ python$version -m pip install -U coverage tox; \ done + +# Installing pyarrow lib until pre-built wheel for Python 3.13 exists +# https://github.com/apache/arrow/issues/43519 +RUN apt update && \ + apt install -y -V lsb-release cmake gcc && \ + distro_name=$(lsb_release --id --short | tr 'A-Z' 'a-z') && \ + code_name=$(lsb_release --codename --short) && \ + wget https://apache.jfrog.io/artifactory/arrow/${distro_name}/apache-arrow-apt-source-latest-${code_name}.deb && \ + apt install -y -V ./apache-arrow-apt-source-latest-${code_name}.deb && \ + apt update && \ + apt install -y -V libarrow-dev libarrow-dataset-dev libarrow-flight-dev libparquet-dev && \ + apt clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +ENV PYARROW_WITH_CUDA=off +ENV PYARROW_WITH_GANDIVA=off +ENV PYARROW_PARALLEL=8 diff --git a/tests/unit/async_/test_driver.py b/tests/unit/async_/test_driver.py index 71ba0e90d..53d287102 100644 --- a/tests/unit/async_/test_driver.py +++ b/tests/unit/async_/test_driver.py @@ -780,10 +780,11 @@ async def test_warn_notification_severity_driver_config( if min_sev_session is ...: session = driver.session() else: - # Works at runtime (will be ignored), but should be rejected by - # type checkers. - session = driver.session( # type: ignore[call-arg] - warn_notification_severity=min_sev_session, + session = driver.session( + # Works at runtime (will be ignored), but should be rejected by + # type checkers. + # type: ignore[call-arg] + warn_notification_severity=min_sev_session ) async with session: session_cls_mock.assert_called_once() diff --git a/tests/unit/common/test_record.py b/tests/unit/common/test_record.py index 0f06fbe2c..b3c38301a 100644 --- a/tests/unit/common/test_record.py +++ b/tests/unit/common/test_record.py @@ -17,7 +17,6 @@ from __future__ import annotations import traceback -import typing as t import pytest import pytz @@ -167,49 +166,39 @@ def test_record_data_keys(keys, expected) -> None: ( *( (value, value) - for value in t.cast( - t.Tuple[t.Any], - ( - None, - True, - False, - 0, - 1, - -1, - 2147483647, - -2147483648, - 3.141592653589, - "", - "Hello, world!", - "👋, 🌍!", - [], - [1, 2.0, "3", True, None], - {"foo": ["bar", 1]}, - b"", - b"foobar", - Date(2021, 1, 1), - Time(12, 34, 56, 123456789), - Time(1, 2, 3, 4, pytz.FixedOffset(60)), - DateTime(2021, 1, 1, 12, 34, 56, 123456789), - DateTime( - 2018, - 10, - 12, - 11, - 37, - 41, - 474716862, - pytz.FixedOffset(60), - ), - pytz.timezone("Europe/Stockholm").localize( - DateTime(2018, 10, 12, 11, 37, 41, 474716862) - ), - Duration(1, 2, 3, 4, 5, 6, 7), - CartesianPoint((1, 2.0)), - CartesianPoint((1, 2.0, 3)), - WGS84Point((1, 2.0)), - WGS84Point((1, 2.0, 3)), + for value in ( + None, + True, + False, + 0, + 1, + -1, + 2147483647, + -2147483648, + 3.141592653589, + "", + "Hello, world!", + "👋, 🌍!", + [], + [1, 2.0, "3", True, None], + {"foo": ["bar", 1]}, + b"", + b"foobar", + Date(2021, 1, 1), + Time(12, 34, 56, 123456789), + Time(1, 2, 3, 4, pytz.FixedOffset(60)), + DateTime(2021, 1, 1, 12, 34, 56, 123456789), + DateTime( + 2018, 10, 12, 11, 37, 41, 474716862, pytz.FixedOffset(60) + ), + pytz.timezone("Europe/Stockholm").localize( + DateTime(2018, 10, 12, 11, 37, 41, 474716862) ), + Duration(1, 2, 3, 4, 5, 6, 7), + CartesianPoint((1, 2.0)), + CartesianPoint((1, 2.0, 3)), + WGS84Point((1, 2.0)), + WGS84Point((1, 2.0, 3)), ) ), *( diff --git a/tests/unit/mixed/async_compat/test_network.py b/tests/unit/mixed/async_compat/test_network.py index 876cd8b32..ee4c290d9 100644 --- a/tests/unit/mixed/async_compat/test_network.py +++ b/tests/unit/mixed/async_compat/test_network.py @@ -37,9 +37,9 @@ TickingDateTimeFactory, ) - TFreezeTime: te.TypeAlias = t.Union[ - StepTickTimeFactory, TickingDateTimeFactory, FrozenDateTimeFactory - ] + TFreezeTime: te.TypeAlias = ( + StepTickTimeFactory | TickingDateTimeFactory | FrozenDateTimeFactory + ) @pytest.fixture diff --git a/tests/unit/sync/test_driver.py b/tests/unit/sync/test_driver.py index b90ebd52d..a508c108e 100644 --- a/tests/unit/sync/test_driver.py +++ b/tests/unit/sync/test_driver.py @@ -779,10 +779,11 @@ def test_warn_notification_severity_driver_config( if min_sev_session is ...: session = driver.session() else: - # Works at runtime (will be ignored), but should be rejected by - # type checkers. - session = driver.session( # type: ignore[call-arg] - warn_notification_severity=min_sev_session, + session = driver.session( + # Works at runtime (will be ignored), but should be rejected by + # type checkers. + # type: ignore[call-arg] + warn_notification_severity=min_sev_session ) with session: session_cls_mock.assert_called_once()