Skip to content

Add support for Python 3.12 #972

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Neo4j versions supported:

Python versions supported:

* Python 3.12 (added in driver version 5.14.0)
* Python 3.11 (added in driver version 5.3.0)
* Python 3.10
* Python 3.9
Expand Down
13 changes: 8 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ classifiers = [
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dynamic = ["version", "readme"]

Expand All @@ -50,17 +51,19 @@ pandas = [
"pandas >= 1.1.0, < 3.0.0",
"numpy >= 1.7.0, < 2.0.0",
]
pyarrow = ["pyarrow >= 1.0.0"]


[build-system]
requires = [
"setuptools~=65.6",
"tomlkit~=0.11.6",
"setuptools >= 66.1.0",
# TODO: 6.0 - can be removed once `setup.py` is simplified
"tomlkit ~= 0.11.6",
]
build-backend = "setuptools.build_meta"

# still in beta
#[tool.setuptools.dynamic]
#version = {attr = "neo4j._meta.version"}
[tool.setuptools.dynamic]
version = {attr = "neo4j._meta.version"}


[tool.coverage]
Expand Down
8 changes: 1 addition & 7 deletions requirements-dev.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# the driver itself
-e .[pandas,numpy]
-e .[pandas,numpy,pyarrow]

# auto-generate sync driver from async code
unasync>=0.5.0
Expand All @@ -10,16 +10,10 @@ mypy>=0.971
typing-extensions>=4.3.0
types-pytz>=2022.1.2

# for packaging
setuptools~=65.6
# TODO: 6.0 - can be removed once `setup.py` is simplified
tomlkit~=0.11.6

# needed for running tests
coverage[toml]>=5.5
freezegun >= 1.2.2
mock>=4.0.3
pyarrow>=1.0.0
pytest>=6.2.5
pytest-asyncio>=0.16.0
pytest-benchmark>=3.4.1
Expand Down
7 changes: 1 addition & 6 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@
from neo4j._meta import (
deprecated_package as deprecated,
package,
version,
)


Expand Down Expand Up @@ -84,8 +83,4 @@ def changed_package_name(new_name):


with changed_package_name(package):
setup(
# until `[tool.setuptools.dynamic]` in pyproject.toml is out of beta
version=version,
long_description=readme,
)
setup(long_description=readme)
2 changes: 2 additions & 0 deletions src/neo4j/_async_compat/concurrency.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ async def acquire(self, blocking=True, timeout=-1):
try:
await wait_for(fut, timeout)
except asyncio.CancelledError:
if fut.cancelled():
raise
already_finished = not fut.cancel()
if already_finished:
# Too late to cancel the acquisition.
Expand Down
7 changes: 3 additions & 4 deletions src/neo4j/_async_compat/shims/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,11 @@
# The shipped wait_for can swallow cancellation errors (starting with 3.8).
# See: https://github.com/python/cpython/pull/26097
# and https://github.com/python/cpython/pull/28149
# Since 3.8 and 3.9 already received their final maintenance release, there
# will be now fix for this. So this patch needs to stick around at least until
# we remove support for Python 3.9.
# Ultimately, this got fixed in https://github.com/python/cpython/pull/98518
# (released with Python 3.12) by re-doing how wait_for works.


if sys.version_info >= (3, 8):
if (3, 12) > sys.version_info >= (3, 8):
# copied from Python 3.10's asyncio package with applied patch

# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
Expand Down
16 changes: 13 additions & 3 deletions testkit/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,29 @@ ENV PYENV_ROOT /.pyenv
ENV PATH $PYENV_ROOT/shims:$PYENV_ROOT/bin:$PATH

# Setup python version
ENV PYTHON_VERSIONS 3.7 3.8 3.9 3.10 3.11
ENV PYTHON_VERSIONS 3.12 3.11 3.10 3.9 3.8 3.7

RUN for version in $PYTHON_VERSIONS; do \
pyenv install $version; \
done
RUN pyenv rehash
RUN pyenv global $(pyenv versions --bare --skip-aliases)
RUN pyenv global $(pyenv versions --bare --skip-aliases | sort --version-sort --reverse)

# Install Latest pip and setuptools for each environment
# + tox and tools for starting the tests
# https://pip.pypa.io/en/stable/news/
RUN for version in $PYTHON_VERSIONS; do \
python$version -m pip install -U pip && \
python$version -m pip install -U setuptools && \
python$version -m pip install -U coverage tox; \
done

# Installing pyarrow lib until pre-built wheel for Python 3.12 exists
# https://github.com/apache/arrow/issues/37880
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 # For C++ \
15 changes: 15 additions & 0 deletions testkit/_common.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
import re
import subprocess
import sys

Expand All @@ -7,14 +8,28 @@


def run(args, env=None):
print(args)
return subprocess.run(
args, universal_newlines=True, stdout=sys.stdout, stderr=sys.stderr,
check=True, env=env
)


def get_python_version():
cmd = [TEST_BACKEND_VERSION, "-V"]
res = subprocess.check_output(cmd, universal_newlines=True,
stderr=sys.stderr)
raw_version = re.match(r"(?:.*?)((?:\d+\.)+(?:\d+))", res).group(1)
return tuple(int(e) for e in raw_version.split("."))


def run_python(args, env=None, warning_as_error=True):
cmd = [TEST_BACKEND_VERSION, "-u"]
if get_python_version() >= (3, 12):
# Ignore warnings for Python 3.12 for now
# https://github.com/dateutil/dateutil/issues/1284 needs to be released
# and propagate through our dependency graph
warning_as_error = False
if warning_as_error:
cmd += ["-W", "error"]
cmd += list(args)
Expand Down
6 changes: 5 additions & 1 deletion testkitbackend/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ async def main():


if __name__ == "__main__":
warnings.simplefilter("error")
if sys.version_info < (3, 12):
# Ignore warnings for Python 3.12 for now
# https://github.com/dateutil/dateutil/issues/1284 needs to be released
# and propagate through our dependency graph
warnings.simplefilter("error")
if len(sys.argv) == 2 and sys.argv[1].lower().strip() == "async":
async_main()
else:
Expand Down
1 change: 1 addition & 0 deletions tests/unit/async_/test_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,7 @@ async def test_execute_query_keyword_parameters(
@pytest.mark.parametrize("parameters", (
{"_": "a"}, {"foo_": None}, {"foo_": 1, "bar_": 2}
))
@mark_async_test
async def test_reserved_query_keyword_parameters(
mocker, parameters: t.Dict[str, t.Any],
) -> None:
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/async_/work/test_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ async def test_data(num_records):
record.data.return_value = expected_data[-1]
assert await result.data("hello", "world") == expected_data
for record in records:
assert record.data.called_once_with("hello", "world")
record.data.assert_called_once_with("hello", "world")


@pytest.mark.parametrize("records", (
Expand Down
1 change: 0 additions & 1 deletion tests/unit/mixed/async_compat/test_concurrency.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,6 @@ async def waiter_non_blocking():
assert fut.exception() is exc
awaits += 1


assert not lock.locked()
await asyncio.gather(blocker(), waiter_non_blocking())
assert not lock.locked()
Expand Down
8 changes: 4 additions & 4 deletions tests/unit/mixed/async_compat/test_shims.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ async def _check_wait_for(wait_for_, should_propagate_cancellation):


@pytest.mark.skipif(
sys.version_info < (3, 8),
reason="wait_for is only broken in Python 3.8+"
not (3, 12) > sys.version_info >= (3, 8),
reason="wait_for is only broken in Python 3.8-3.11 (inclusive)"
)
@mark_async_test
async def test_wait_for_shim_is_necessary_starting_from_3x8():
Expand All @@ -56,8 +56,8 @@ async def test_wait_for_shim_is_necessary_starting_from_3x8():


@pytest.mark.skipif(
sys.version_info >= (3, 8),
reason="wait_for is only broken in Python 3.8+"
(3, 12) > sys.version_info >= (3, 8),
reason="wait_for is only broken in Python 3.8-3.11 (inclusive)"
)
@mark_async_test
async def test_wait_for_shim_is_not_necessary_prior_to_3x8():
Expand Down
1 change: 1 addition & 0 deletions tests/unit/sync/test_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,7 @@ def test_execute_query_keyword_parameters(
@pytest.mark.parametrize("parameters", (
{"_": "a"}, {"foo_": None}, {"foo_": 1, "bar_": 2}
))
@mark_sync_test
def test_reserved_query_keyword_parameters(
mocker, parameters: t.Dict[str, t.Any],
) -> None:
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/sync/work/test_result.py
Original file line number Diff line number Diff line change
Expand Up @@ -607,7 +607,7 @@ def test_data(num_records):
record.data.return_value = expected_data[-1]
assert result.data("hello", "world") == expected_data
for record in records:
assert record.data.called_once_with("hello", "world")
record.data.assert_called_once_with("hello", "world")


@pytest.mark.parametrize("records", (
Expand Down
13 changes: 9 additions & 4 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
[tox]
envlist = py{37,38,39,310,311}-{unit,integration,performance}
envlist = py{37,38,39,310,311,312}-{unit,integration,performance}

[testenv]
passenv = TEST_NEO4J_*
deps = -r requirements-dev.txt
setenv = COVERAGE_FILE={envdir}/.coverage
usedevelop = true
# Ignore warnings for Python 3.12 for now
# https://github.com/dateutil/dateutil/issues/1284 needs to be released
# and propagate through our dependency graph
warnargs =
py{37,38,39,310,311}: -W error
py312:
commands =
coverage erase
unit: coverage run -m pytest -W error -v {posargs} tests/unit
unit: coverage run -m pytest -v --doctest-modules {posargs} src
integration: coverage run -m pytest -W error -v {posargs} tests/integration
unit: coverage run -m pytest {[testenv]warnargs} -v {posargs} tests/unit
integration: coverage run -m pytest {[testenv]warnargs} -v {posargs} tests/integration
performance: python -m pytest --benchmark-autosave -v {posargs} tests/performance
unit,integration: coverage report