diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..ff2c165e2 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +/.github/CODEOWNERS @neo4j/drivers +/.github/workflows/ @neo4j/drivers diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 000000000..f85fb9212 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,57 @@ +name: Tests + +on: + push: + branches: + - '5.0' + pull_request: + branches: + - '5.0' + +jobs: + win-unit-tests: + name: Windows Unit Tests + runs-on: windows-latest + strategy: + matrix: + python-version: + - semver: '3.7' + tox-factor: 'py37' + - semver: '3.8' + tox-factor: 'py38' + - semver: '3.9' + tox-factor: 'py39' + - semver: '3.10' + tox-factor: 'py310' + - semver: '3.11' + tox-factor: 'py311' + - semver: '3.12' + tox-factor: 'py312' + - semver: '3.13' + tox-factor: 'py313' + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + + - name: Set up Python ${{ matrix.python-version.semver }} + uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 + with: + python-version: ${{ matrix.python-version.semver }} + cache: 'pip' + - name: Install tox + run: python -m pip install -U tox>=4.8.0 + - name: Run unit tests + run: python -m tox -vv -f unit ${{ matrix.python-version.tox-factor }} + + gha-conclusion: + name: gha-conclusion + needs: win-unit-tests + runs-on: ubuntu-latest + steps: + - name: Signal failure + if: ${{ cancelled() || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'failure') }} + run: | + echo "Some workflows have failed!" + exit 1 + - name: Signal success + if: ${{ !cancelled() && !contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'failure') }} + run: echo "All done!" diff --git a/src/neo4j/_codec/hydration/v1/temporal.py b/src/neo4j/_codec/hydration/v1/temporal.py index 9f6e943b1..937b2435c 100644 --- a/src/neo4j/_codec/hydration/v1/temporal.py +++ b/src/neo4j/_codec/hydration/v1/temporal.py @@ -216,7 +216,7 @@ def dehydrate_np_datetime(value): """ if np.isnat(value): return None - year = value.astype("datetime64[Y]").astype(int) + 1970 + year = value.astype("datetime64[Y]").astype(np.int64) + 1970 if not 0 < year <= 9999: # while we could encode years outside the range, they would fail # when retrieved from the database. @@ -224,9 +224,10 @@ def dehydrate_np_datetime(value): f"Year out of range ({MIN_YEAR:d}..{MAX_YEAR:d}) " f"found {year}" ) - seconds = value.astype(np.dtype("datetime64[s]")).astype(int) + seconds = value.astype(np.dtype("datetime64[s]")).astype(np.int64) nanoseconds = ( - value.astype(np.dtype("datetime64[ns]")).astype(int) % NANO_SECONDS + value.astype(np.dtype("datetime64[ns]")).astype(np.int64) + % NANO_SECONDS ) return Structure(b"d", seconds, nanoseconds) @@ -299,6 +300,7 @@ def dehydrate_timedelta(value): if np is not None: + _NUMPY_DURATION_NS_FALLBACK = object() _NUMPY_DURATION_UNITS = { "Y": "years", "M": "months", @@ -310,6 +312,9 @@ def dehydrate_timedelta(value): "ms": "milliseconds", "us": "microseconds", "ns": "nanoseconds", + "ps": _NUMPY_DURATION_NS_FALLBACK, + "fs": _NUMPY_DURATION_NS_FALLBACK, + "as": _NUMPY_DURATION_NS_FALLBACK, } def dehydrate_np_timedelta(value): @@ -323,14 +328,17 @@ def dehydrate_np_timedelta(value): if np.isnat(value): return None unit, step_size = np.datetime_data(value) - numer = int(value.astype(int)) - # raise RuntimeError((type(numer), type(step_size))) - kwarg = _NUMPY_DURATION_UNITS.get(unit) - if kwarg is not None: - return dehydrate_duration(Duration(**{kwarg: numer * step_size})) - return dehydrate_duration( - Duration(nanoseconds=value.astype("timedelta64[ns]").astype(int)) - ) + numer = int(value.astype(np.int64)) + try: + kwarg = _NUMPY_DURATION_UNITS[unit] + except KeyError: + raise TypeError( + f"Unsupported numpy.timedelta64 unit: {unit!r}" + ) from None + if kwarg is _NUMPY_DURATION_NS_FALLBACK: + nanoseconds = value.astype("timedelta64[ns]").astype(np.int64) + return dehydrate_duration(Duration(nanoseconds=nanoseconds)) + return dehydrate_duration(Duration(**{kwarg: numer * step_size})) if pd is not None: diff --git a/tests/unit/async_/io/test_neo4j_pool.py b/tests/unit/async_/io/test_neo4j_pool.py index e1549fd06..3924689e1 100644 --- a/tests/unit/async_/io/test_neo4j_pool.py +++ b/tests/unit/async_/io/test_neo4j_pool.py @@ -15,6 +15,7 @@ import inspect +import time import pytest @@ -28,6 +29,7 @@ AsyncBolt, AsyncNeo4jPool, ) +from neo4j._async_compat import async_sleep from neo4j._async_compat.util import AsyncUtil from neo4j._conf import ( RoutingConfig, @@ -45,6 +47,8 @@ from ...._async_compat import mark_async_test +MONOTONIC_TIME_RESOLUTION = time.get_clock_info("monotonic").resolution + ROUTER1_ADDRESS = ResolvedAddress(("1.2.3.1", 9000), host_name="host") ROUTER2_ADDRESS = ResolvedAddress(("1.2.3.1", 9001), host_name="host") ROUTER3_ADDRESS = ResolvedAddress(("1.2.3.1", 9002), host_name="host") @@ -193,6 +197,8 @@ async def test_acquires_new_routing_table_if_stale( old_value = pool.routing_tables[db.name].last_updated_time pool.routing_tables[db.name].ttl = 0 + await async_sleep(MONOTONIC_TIME_RESOLUTION * 2) + cx = await pool.acquire(READ_ACCESS, 30, db, None, None, None) await pool.release(cx) assert pool.routing_tables[db.name].last_updated_time > old_value @@ -214,6 +220,8 @@ async def test_removes_old_routing_table(opener): db2_rt = pool.routing_tables[TEST_DB2.name] db2_rt.ttl = -RoutingConfig.routing_table_purge_delay + await async_sleep(MONOTONIC_TIME_RESOLUTION * 2) + cx = await pool.acquire(READ_ACCESS, 30, TEST_DB1, None, None, None) await pool.release(cx) assert pool.routing_tables[TEST_DB1.name].last_updated_time > old_value diff --git a/tests/unit/async_/test_addressing.py b/tests/unit/async_/test_addressing.py index 5a0a940b5..7c76724be 100644 --- a/tests/unit/async_/test_addressing.py +++ b/tests/unit/async_/test_addressing.py @@ -53,8 +53,12 @@ async def test_address_resolve_with_custom_resolver_none() -> None: @pytest.mark.parametrize( ("test_input", "expected"), [ + (Address(("example.invalid", "7687")), ValueError), + (Address(("example.invalid", 7687)), ValueError), + (Address(("example.invalid", None)), ValueError), (Address(("127.0.0.1", "abcd")), ValueError), (Address((None, None)), ValueError), + (Address((1234, "7687")), TypeError), ], ) @mark_async_test @@ -67,6 +71,21 @@ async def test_address_resolve_with_unresolvable_address( ) +@pytest.mark.parametrize( + "test_input", + [ + Address((None, 7687)), + Address(("example.com", None)), + ], +) +@mark_async_test +async def test_address_resolves_with_none(test_input) -> None: + resolved = await AsyncUtil.list( + AsyncNetworkUtil.resolve_address(test_input, resolver=None) + ) + assert resolved + + @mark_async_test @pytest.mark.parametrize("resolver_type", ("sync", "async")) async def test_address_resolve_with_custom_resolver(resolver_type) -> None: diff --git a/tests/unit/common/codec/packstream/v1/test_packstream.py b/tests/unit/common/codec/packstream/v1/test_packstream.py index 324b2c0d3..3b70d3bcc 100644 --- a/tests/unit/common/codec/packstream/v1/test_packstream.py +++ b/tests/unit/common/codec/packstream/v1/test_packstream.py @@ -15,6 +15,7 @@ import struct +import sys from io import BytesIO from math import ( isnan, @@ -37,6 +38,11 @@ standard_ascii = [chr(i) for i in range(128)] not_ascii = "♥O◘♦♥O◘♦" +SKIP_PANDAS_INT_AS_INT64 = ( + pd.Series([], dtype=int).dtype.itemsize < 8 + and sys.version_info < (3, 9) + and sys.platform == "win32" +) @pytest.fixture @@ -289,7 +295,13 @@ def test_positive_int64(self, int_type, assert_packable): @pytest.mark.parametrize( "dtype", ( - int, + pytest.param( + int, + marks=pytest.mark.skipif( + SKIP_PANDAS_INT_AS_INT64, + reason="Legacy pandas treating int as int32", + ), + ), pd.Int64Dtype(), pd.UInt64Dtype(), np.int64, @@ -317,7 +329,13 @@ def test_negative_int64(self, int_type, assert_packable): @pytest.mark.parametrize( "dtype", ( - int, + pytest.param( + int, + marks=pytest.mark.skipif( + SKIP_PANDAS_INT_AS_INT64, + reason="Legacy pandas treating int as int32", + ), + ), pd.Int64Dtype(), np.int64, np.longlong, diff --git a/tests/unit/sync/io/test_neo4j_pool.py b/tests/unit/sync/io/test_neo4j_pool.py index 13b9be4e2..30fc908f6 100644 --- a/tests/unit/sync/io/test_neo4j_pool.py +++ b/tests/unit/sync/io/test_neo4j_pool.py @@ -15,6 +15,7 @@ import inspect +import time import pytest @@ -22,6 +23,7 @@ READ_ACCESS, WRITE_ACCESS, ) +from neo4j._async_compat import sleep from neo4j._async_compat.util import Util from neo4j._conf import ( RoutingConfig, @@ -45,6 +47,8 @@ from ...._async_compat import mark_sync_test +MONOTONIC_TIME_RESOLUTION = time.get_clock_info("monotonic").resolution + ROUTER1_ADDRESS = ResolvedAddress(("1.2.3.1", 9000), host_name="host") ROUTER2_ADDRESS = ResolvedAddress(("1.2.3.1", 9001), host_name="host") ROUTER3_ADDRESS = ResolvedAddress(("1.2.3.1", 9002), host_name="host") @@ -193,6 +197,8 @@ def test_acquires_new_routing_table_if_stale( old_value = pool.routing_tables[db.name].last_updated_time pool.routing_tables[db.name].ttl = 0 + sleep(MONOTONIC_TIME_RESOLUTION * 2) + cx = pool.acquire(READ_ACCESS, 30, db, None, None, None) pool.release(cx) assert pool.routing_tables[db.name].last_updated_time > old_value @@ -214,6 +220,8 @@ def test_removes_old_routing_table(opener): db2_rt = pool.routing_tables[TEST_DB2.name] db2_rt.ttl = -RoutingConfig.routing_table_purge_delay + sleep(MONOTONIC_TIME_RESOLUTION * 2) + cx = pool.acquire(READ_ACCESS, 30, TEST_DB1, None, None, None) pool.release(cx) assert pool.routing_tables[TEST_DB1.name].last_updated_time > old_value diff --git a/tests/unit/sync/test_addressing.py b/tests/unit/sync/test_addressing.py index fc741716e..3f48247be 100644 --- a/tests/unit/sync/test_addressing.py +++ b/tests/unit/sync/test_addressing.py @@ -53,8 +53,12 @@ def test_address_resolve_with_custom_resolver_none() -> None: @pytest.mark.parametrize( ("test_input", "expected"), [ + (Address(("example.invalid", "7687")), ValueError), + (Address(("example.invalid", 7687)), ValueError), + (Address(("example.invalid", None)), ValueError), (Address(("127.0.0.1", "abcd")), ValueError), (Address((None, None)), ValueError), + (Address((1234, "7687")), TypeError), ], ) @mark_sync_test @@ -67,6 +71,21 @@ def test_address_resolve_with_unresolvable_address( ) +@pytest.mark.parametrize( + "test_input", + [ + Address((None, 7687)), + Address(("example.com", None)), + ], +) +@mark_sync_test +def test_address_resolves_with_none(test_input) -> None: + resolved = Util.list( + NetworkUtil.resolve_address(test_input, resolver=None) + ) + assert resolved + + @mark_sync_test @pytest.mark.parametrize("resolver_type", ("sync", "async")) def test_address_resolve_with_custom_resolver(resolver_type) -> None: