From 90369c1e5b4a37ff263a3845608ea8ff19af5da4 Mon Sep 17 00:00:00 2001 From: Rouven Bauer Date: Thu, 1 Sep 2022 11:03:49 +0200 Subject: [PATCH 01/10] Warnings overhaul * Fix falsely emitted `DeprecationWarning` regarding `update_routing_table_timeout`. * Treat warnings as errors during testing * Changed tests to expect and catch warning or to avoid them. --- neo4j/io/__init__.py | 22 ++++--- neo4j/io/_socket.py | 2 +- neo4j/time/__init__.py | 10 +-- testkit/backend.py | 2 +- testkitbackend/__main__.py | 4 ++ testkitbackend/requests.py | 5 +- tests/integration/conftest.py | 8 +-- .../test_database_selection_example.py | 6 +- tests/integration/test_temporal_types.py | 32 ++++----- tests/stub/test_directdriver.py | 19 +++++- tests/stub/test_routingdriver.py | 13 +++- tests/unit/io/test_direct.py | 1 + tests/unit/spatial/test_cartesian_point.py | 6 +- tests/unit/spatial/test_wgs84_point.py | 6 +- tests/unit/time/test_clock.py | 4 +- tests/unit/time/test_datetime.py | 23 ++++++- tests/unit/time/test_duration.py | 66 ++++++++++++++++++- tests/unit/time/test_time.py | 38 ++++++++++- tests/unit/work/test_result.py | 16 ++++- tox-unit.ini | 2 +- tox.ini | 2 +- 21 files changed, 224 insertions(+), 63 deletions(-) diff --git a/neo4j/io/__init__.py b/neo4j/io/__init__.py index f796c4dce..fb71d5ef6 100644 --- a/neo4j/io/__init__.py +++ b/neo4j/io/__init__.py @@ -277,18 +277,19 @@ def get_handshake(cls): return b"".join(version.to_bytes() for version in offered_versions).ljust(16, b"\x00") @classmethod - def ping(cls, address, *, timeout=None, **config): + def ping(cls, address, *, timeout=None, pool_config=None): """ Attempt to establish a Bolt connection, returning the agreed Bolt protocol version if successful. """ - config = PoolConfig.consume(config) + if pool_config is None: + pool_config = PoolConfig() try: s, protocol_version, handshake, data = BoltSocket.connect( address, timeout=timeout, - custom_resolver=config.resolver, - ssl_context=config.get_ssl_context(), - keep_alive=config.keep_alive, + custom_resolver=pool_config.resolver, + ssl_context=pool_config.get_ssl_context(), + keep_alive=pool_config.keep_alive, ) except (ServiceUnavailable, SessionExpired, BoltHandshakeError): return None @@ -297,7 +298,8 @@ def ping(cls, address, *, timeout=None, **config): return protocol_version @classmethod - def open(cls, address, *, auth=None, timeout=None, routing_context=None, **pool_config): + def open(cls, address, *, auth=None, timeout=None, routing_context=None, + pool_config=None): """ Open a new Bolt connection to a given server address. :param address: @@ -316,7 +318,8 @@ def time_remaining(): return t if t > 0 else 0 t0 = perf_counter() - pool_config = PoolConfig.consume(pool_config) + if pool_config is None: + pool_config = PoolConfig() socket_connection_timeout = pool_config.connection_timeout if socket_connection_timeout is None: @@ -906,7 +909,7 @@ def open(cls, address, *, auth, pool_config, workspace_config): def opener(addr, timeout): return Bolt.open( addr, auth=auth, timeout=timeout, routing_context=None, - **pool_config + pool_config=pool_config ) pool = cls(opener, pool_config, workspace_config, address) @@ -951,7 +954,8 @@ def open(cls, *addresses, auth, pool_config, workspace_config, routing_context=N def opener(addr, timeout): return Bolt.open(addr, auth=auth, timeout=timeout, - routing_context=routing_context, **pool_config) + routing_context=routing_context, + pool_config=pool_config) pool = cls(opener, pool_config, workspace_config, address) return pool diff --git a/neo4j/io/_socket.py b/neo4j/io/_socket.py index f3cc3f313..a6293a278 100644 --- a/neo4j/io/_socket.py +++ b/neo4j/io/_socket.py @@ -259,7 +259,7 @@ def _handshake(cls, s, resolved_address): def close_socket(cls, socket_): try: if isinstance(socket_, BoltSocket): - socket.close() + socket_.close() else: socket_.shutdown(SHUT_RDWR) socket_.close() diff --git a/neo4j/time/__init__.py b/neo4j/time/__init__.py index 6016e3e83..4c9d745fe 100644 --- a/neo4j/time/__init__.py +++ b/neo4j/time/__init__.py @@ -482,7 +482,7 @@ def __mul__(self, other): :rtype: Duration """ if isinstance(other, float): - deprecation_warn("Multiplication with float will be deprecated in " + deprecation_warn("Multiplication with float will be removed in " "5.0.") if isinstance(other, (int, float)): return Duration( @@ -1627,7 +1627,7 @@ def from_clock_time(cls, clock_time, epoch): ts = clock_time.seconds % 86400 nanoseconds = int(NANO_SECONDS * ts + clock_time.nanoseconds) ticks = (epoch.time().ticks_ns + nanoseconds) % (86400 * NANO_SECONDS) - return Time.from_ticks_ns(ticks) + return cls.from_ticks_ns(ticks) @classmethod def __normalize_hour(cls, hour): @@ -1657,8 +1657,8 @@ def __normalize_nanosecond(cls, hour, minute, second, nanosecond): # TODO 5.0: remove ----------------------------------------------------- seconds, extra_ns = divmod(second, 1) if extra_ns: - deprecation_warn("Float support second will be removed in 5.0. " - "Use `nanosecond` instead.") + deprecation_warn("Float support for `second` will be removed in " + "5.0. Use `nanosecond` instead.") # ---------------------------------------------------------------------- hour, minute, second = cls.__normalize_second(hour, minute, second) nanosecond = int(nanosecond @@ -1753,7 +1753,7 @@ def nanosecond(self): return self.__nanosecond @property - @deprecated("hour_minute_second will be removed in 5.0. " + @deprecated("`hour_minute_second` will be removed in 5.0. " "Use `hour_minute_second_nanosecond` instead.") def hour_minute_second(self): """The time as a tuple of (hour, minute, second). diff --git a/testkit/backend.py b/testkit/backend.py index ca5a4779a..8fd811e22 100644 --- a/testkit/backend.py +++ b/testkit/backend.py @@ -3,6 +3,6 @@ if __name__ == "__main__": subprocess.check_call( - ["python", "-m", "testkitbackend"], + ["python", "-W", "error", "-m", "testkitbackend"], stdout=sys.stdout, stderr=sys.stderr ) diff --git a/testkitbackend/__main__.py b/testkitbackend/__main__.py index 3e8a4b87b..402ab3462 100644 --- a/testkitbackend/__main__.py +++ b/testkitbackend/__main__.py @@ -1,6 +1,10 @@ +import warnings + from .server import Server + if __name__ == "__main__": + warnings.simplefilter("error") server = Server(("0.0.0.0", 9876)) while True: server.handle_request() diff --git a/testkitbackend/requests.py b/testkitbackend/requests.py index a6a5d9e33..25aeada1a 100644 --- a/testkitbackend/requests.py +++ b/testkitbackend/requests.py @@ -16,6 +16,7 @@ # limitations under the License. import json from os import path +import warnings import neo4j import testkitbackend.fromtestkit as fromtestkit @@ -108,7 +109,9 @@ def NewDriver(backend, data): def VerifyConnectivity(backend, data): driver_id = data["driverId"] driver = backend.drivers[driver_id] - driver.verify_connectivity() + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=neo4j.ExperimentalWarning) + driver.verify_connectivity() backend.send_response("Driver", {"id": driver_id}) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 015ba64d8..d65c16f5f 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -43,14 +43,14 @@ NEO4J_RELEASES = getenv("NEO4J_RELEASES", "snapshot-enterprise 3.5-enterprise").split() NEO4J_HOST = "localhost" NEO4J_PORTS = { - "bolt": 17601, + "bolt": 7687, "http": 17401, "https": 17301, } -NEO4J_CORES = 3 -NEO4J_REPLICAS = 2 +NEO4J_CORES = 1 +NEO4J_REPLICAS = 0 NEO4J_USER = "neo4j" -NEO4J_PASSWORD = "password" +NEO4J_PASSWORD = "pass" NEO4J_AUTH = (NEO4J_USER, NEO4J_PASSWORD) NEO4J_LOCK = RLock() NEO4J_SERVICE = None diff --git a/tests/integration/examples/test_database_selection_example.py b/tests/integration/examples/test_database_selection_example.py index 99293ea8c..e92bd94cb 100644 --- a/tests/integration/examples/test_database_selection_example.py +++ b/tests/integration/examples/test_database_selection_example.py @@ -42,12 +42,12 @@ class DatabaseSelectionExample: def __init__(self, uri, user, password): self.driver = GraphDatabase.driver(uri, auth=(user, password)) with self.driver.session(database="system") as session: - session.run("DROP DATABASE example IF EXISTS").consume() - session.run("CREATE DATABASE example").consume() + session.run("DROP DATABASE example IF EXISTS WAIT").consume() + session.run("CREATE DATABASE example WAIT").consume() def close(self): with self.driver.session(database="system") as session: - session.run("DROP DATABASE example").consume() + session.run("DROP DATABASE example WAIT").consume() self.driver.close() def run_example_code(self): diff --git a/tests/integration/test_temporal_types.py b/tests/integration/test_temporal_types.py index 0e996db23..edf29c326 100644 --- a/tests/integration/test_temporal_types.py +++ b/tests/integration/test_temporal_types.py @@ -89,7 +89,7 @@ def test_whole_second_time_input(cypher_eval): def test_nanosecond_resolution_time_input(cypher_eval): result = cypher_eval("CYPHER runtime=interpreted WITH $x AS x " "RETURN [x.hour, x.minute, x.second, x.nanosecond]", - x=Time(12, 34, 56.789012345)) + x=Time(12, 34, 56, 789012345)) hour, minute, second, nanosecond = result assert hour == 12 assert minute == 34 @@ -101,7 +101,7 @@ def test_time_with_numeric_time_offset_input(cypher_eval): result = cypher_eval("CYPHER runtime=interpreted WITH $x AS x " "RETURN [x.hour, x.minute, x.second, " " x.nanosecond, x.offset]", - x=Time(12, 34, 56.789012345, tzinfo=FixedOffset(90))) + x=Time(12, 34, 56, 789012345, tzinfo=FixedOffset(90))) hour, minute, second, nanosecond, offset = result assert hour == 12 assert minute == 34 @@ -150,7 +150,7 @@ def test_nanosecond_resolution_datetime_input(cypher_eval): result = cypher_eval("CYPHER runtime=interpreted WITH $x AS x " "RETURN [x.year, x.month, x.day, " " x.hour, x.minute, x.second, x.nanosecond]", - x=DateTime(1976, 6, 13, 12, 34, 56.789012345)) + x=DateTime(1976, 6, 13, 12, 34, 56, 789012345)) year, month, day, hour, minute, second, nanosecond = result assert year == 1976 assert month == 6 @@ -166,7 +166,7 @@ def test_datetime_with_numeric_time_offset_input(cypher_eval): "RETURN [x.year, x.month, x.day, " " x.hour, x.minute, x.second, " " x.nanosecond, x.offset]", - x=DateTime(1976, 6, 13, 12, 34, 56.789012345, + x=DateTime(1976, 6, 13, 12, 34, 56, 789012345, tzinfo=FixedOffset(90))) year, month, day, hour, minute, second, nanosecond, offset = result assert year == 1976 @@ -180,7 +180,7 @@ def test_datetime_with_numeric_time_offset_input(cypher_eval): def test_datetime_with_named_time_zone_input(cypher_eval): - dt = DateTime(1976, 6, 13, 12, 34, 56.789012345) + dt = DateTime(1976, 6, 13, 12, 34, 56, 789012345) input_value = timezone("US/Pacific").localize(dt) result = cypher_eval("CYPHER runtime=interpreted WITH $x AS x " "RETURN [x.year, x.month, x.day, " @@ -199,7 +199,7 @@ def test_datetime_with_named_time_zone_input(cypher_eval): def test_datetime_array_input(cypher_eval): - data = [DateTime(2018, 4, 6, 13, 4, 42.516120), DateTime(1976, 6, 13)] + data = [DateTime(2018, 4, 6, 13, 4, 42, 516120), DateTime(1976, 6, 13)] value = cypher_eval("CREATE (a {x:$x}) RETURN a.x", x=data) assert value == data @@ -209,7 +209,7 @@ def test_duration_input(cypher_eval): "RETURN [x.months, x.days, x.seconds, " " x.microsecondsOfSecond]", x=Duration(years=1, months=2, days=3, hours=4, - minutes=5, seconds=6.789012)) + minutes=5, seconds=6, microseconds=789012)) months, days, seconds, microseconds = result assert months == 14 assert days == 3 @@ -258,13 +258,13 @@ def test_whole_second_time_output(cypher_eval): def test_nanosecond_resolution_time_output(cypher_eval): value = cypher_eval("RETURN time('12:34:56.789012345')") assert isinstance(value, Time) - assert value == Time(12, 34, 56.789012345, tzinfo=FixedOffset(0)) + assert value == Time(12, 34, 56, 789012345, tzinfo=FixedOffset(0)) def test_time_with_numeric_time_offset_output(cypher_eval): value = cypher_eval("RETURN time('12:34:56.789012345+0130')") assert isinstance(value, Time) - assert value == Time(12, 34, 56.789012345, tzinfo=FixedOffset(90)) + assert value == Time(12, 34, 56, 789012345, tzinfo=FixedOffset(90)) def test_whole_second_localtime_output(cypher_eval): @@ -276,7 +276,7 @@ def test_whole_second_localtime_output(cypher_eval): def test_nanosecond_resolution_localtime_output(cypher_eval): value = cypher_eval("RETURN localtime('12:34:56.789012345')") assert isinstance(value, Time) - assert value == Time(12, 34, 56.789012345) + assert value == Time(12, 34, 56, 789012345) def test_whole_second_datetime_output(cypher_eval): @@ -288,14 +288,14 @@ def test_whole_second_datetime_output(cypher_eval): def test_nanosecond_resolution_datetime_output(cypher_eval): value = cypher_eval("RETURN datetime('1976-06-13T12:34:56.789012345')") assert isinstance(value, DateTime) - assert value == DateTime(1976, 6, 13, 12, 34, 56.789012345, tzinfo=utc) + assert value == DateTime(1976, 6, 13, 12, 34, 56, 789012345, tzinfo=utc) def test_datetime_with_numeric_time_offset_output(cypher_eval): value = cypher_eval("RETURN " "datetime('1976-06-13T12:34:56.789012345+01:30')") assert isinstance(value, DateTime) - assert value == DateTime(1976, 6, 13, 12, 34, 56.789012345, + assert value == DateTime(1976, 6, 13, 12, 34, 56, 789012345, tzinfo=FixedOffset(90)) @@ -303,7 +303,7 @@ def test_datetime_with_named_time_zone_output(cypher_eval): value = cypher_eval("RETURN datetime('1976-06-13T12:34:56.789012345" "[Europe/London]')") assert isinstance(value, DateTime) - dt = DateTime(1976, 6, 13, 12, 34, 56.789012345) + dt = DateTime(1976, 6, 13, 12, 34, 56, 789012345) assert value == timezone("Europe/London").localize(dt) @@ -317,21 +317,21 @@ def test_nanosecond_resolution_localdatetime_output(cypher_eval): value = cypher_eval("RETURN " "localdatetime('1976-06-13T12:34:56.789012345')") assert isinstance(value, DateTime) - assert value == DateTime(1976, 6, 13, 12, 34, 56.789012345) + assert value == DateTime(1976, 6, 13, 12, 34, 56, 789012345) def test_duration_output(cypher_eval): value = cypher_eval("RETURN duration('P1Y2M3DT4H5M6.789S')") assert isinstance(value, Duration) assert value == Duration(years=1, months=2, days=3, hours=4, - minutes=5, seconds=6.789) + minutes=5, seconds=6, milliseconds=789) def test_nanosecond_resolution_duration_output(cypher_eval): value = cypher_eval("RETURN duration('P1Y2M3DT4H5M6.789123456S')") assert isinstance(value, Duration) assert value == Duration(years=1, months=2, days=3, hours=4, - minutes=5, seconds=6.789123456) + minutes=5, seconds=6, nanoseconds=789123456) def test_datetime_parameter_case1(session): diff --git a/tests/stub/test_directdriver.py b/tests/stub/test_directdriver.py index 2e7ae7f64..a7d3ac1d9 100644 --- a/tests/stub/test_directdriver.py +++ b/tests/stub/test_directdriver.py @@ -21,6 +21,7 @@ import pytest +import neo4j from neo4j.exceptions import ( ServiceUnavailable, ConfigurationError, @@ -80,7 +81,9 @@ def test_direct_driver_with_wrong_port(driver_info): uri = "bolt://127.0.0.1:9002" with pytest.raises(ServiceUnavailable): driver = GraphDatabase.driver(uri, auth=driver_info["auth_token"], **driver_config) - driver.verify_connectivity() + with pytest.warns(neo4j.ExperimentalWarning, + match="The configuration may change in the future."): + driver.verify_connectivity() @pytest.mark.parametrize( @@ -96,7 +99,13 @@ def test_direct_verify_connectivity(driver_info, test_script, test_expected): uri = "bolt://localhost:9001" with GraphDatabase.driver(uri, auth=driver_info["auth_token"], **driver_config) as driver: assert isinstance(driver, BoltDriver) - assert driver.verify_connectivity(default_access_mode=READ_ACCESS) == test_expected + with pytest.warns( + neo4j.ExperimentalWarning, + match="The configuration may change in the future." + ): + assert driver.verify_connectivity( + default_access_mode=READ_ACCESS + ) == test_expected @pytest.mark.parametrize( @@ -112,4 +121,8 @@ def test_direct_verify_connectivity_disconnect_on_run(driver_info, test_script): uri = "bolt://127.0.0.1:9001" with GraphDatabase.driver(uri, auth=driver_info["auth_token"], **driver_config) as driver: with pytest.raises(ServiceUnavailable): - driver.verify_connectivity(default_access_mode=READ_ACCESS) + with pytest.warns( + neo4j.ExperimentalWarning, + match="The configuration may change in the future." + ): + driver.verify_connectivity(default_access_mode=READ_ACCESS) diff --git a/tests/stub/test_routingdriver.py b/tests/stub/test_routingdriver.py index 139c225df..1444bd861 100644 --- a/tests/stub/test_routingdriver.py +++ b/tests/stub/test_routingdriver.py @@ -22,6 +22,7 @@ import pytest from neo4j import ( + ExperimentalWarning, GraphDatabase, Neo4jDriver, TRUST_ALL_CERTIFICATES, @@ -58,7 +59,11 @@ def test_neo4j_driver_verify_connectivity(driver_info, test_script): with StubCluster(test_script): with GraphDatabase.driver(driver_info["uri_neo4j"], auth=driver_info["auth_token"], user_agent="test") as driver: assert isinstance(driver, Neo4jDriver) - assert driver.verify_connectivity() is not None + with pytest.warns( + ExperimentalWarning, + match="The configuration may change in the future." + ): + assert driver.verify_connectivity() is not None # @pytest.mark.skip(reason="Flaky") @@ -75,4 +80,8 @@ def test_neo4j_driver_verify_connectivity_server_down(driver_info, test_script): assert isinstance(driver, Neo4jDriver) with pytest.raises(ServiceUnavailable): - driver.verify_connectivity() + with pytest.warns( + ExperimentalWarning, + match="The configuration may change in the future." + ): + driver.verify_connectivity() diff --git a/tests/unit/io/test_direct.py b/tests/unit/io/test_direct.py index aab2dc948..1801d6b47 100644 --- a/tests/unit/io/test_direct.py +++ b/tests/unit/io/test_direct.py @@ -116,6 +116,7 @@ def test_open(self): connection = Bolt.open(("localhost", 9999), auth=("test", "test")) def test_open_timeout(self): + conf = PoolConfig() with pytest.raises(ServiceUnavailable): connection = Bolt.open(("localhost", 9999), auth=("test", "test"), timeout=1) diff --git a/tests/unit/spatial/test_cartesian_point.py b/tests/unit/spatial/test_cartesian_point.py index ee86e5b99..5c6554b24 100644 --- a/tests/unit/spatial/test_cartesian_point.py +++ b/tests/unit/spatial/test_cartesian_point.py @@ -32,11 +32,11 @@ class CartesianPointTestCase(TestCase): def test_alias(self): x, y, z = 3.2, 4.0, -1.2 p = CartesianPoint((x, y, z)) - self.assert_(hasattr(p, "x")) + self.assertTrue(hasattr(p, "x")) self.assertEqual(p.x, x) - self.assert_(hasattr(p, "y")) + self.assertTrue(hasattr(p, "y")) self.assertEqual(p.y, y) - self.assert_(hasattr(p, "z")) + self.assertTrue(hasattr(p, "z")) self.assertEqual(p.z, z) def test_dehydration_3d(self): diff --git a/tests/unit/spatial/test_wgs84_point.py b/tests/unit/spatial/test_wgs84_point.py index 8f725a582..3f582172f 100644 --- a/tests/unit/spatial/test_wgs84_point.py +++ b/tests/unit/spatial/test_wgs84_point.py @@ -32,11 +32,11 @@ class WGS84PointTestCase(TestCase): def test_alias(self): x, y, z = 3.2, 4.0, -1.2 p = WGS84Point((x, y, z)) - self.assert_(hasattr(p, "longitude")) + self.assertTrue(hasattr(p, "longitude")) self.assertEqual(p.longitude, x) - self.assert_(hasattr(p, "latitude")) + self.assertTrue(hasattr(p, "latitude")) self.assertEqual(p.latitude, y) - self.assert_(hasattr(p, "height")) + self.assertTrue(hasattr(p, "height")) self.assertEqual(p.height, z) def test_dehydration_3d(self): diff --git a/tests/unit/time/test_clock.py b/tests/unit/time/test_clock.py index 4b4c18a72..25b51eeb5 100644 --- a/tests/unit/time/test_clock.py +++ b/tests/unit/time/test_clock.py @@ -57,7 +57,7 @@ def test_local_offset(self): def test_local_time(self): _ = Clock() for impl in Clock._Clock__implementations: - self.assert_(issubclass(impl, Clock)) + self.assertTrue(issubclass(impl, Clock)) clock = object.__new__(impl) time = clock.local_time() self.assertIsInstance(time, ClockTime) @@ -65,7 +65,7 @@ def test_local_time(self): def test_utc_time(self): _ = Clock() for impl in Clock._Clock__implementations: - self.assert_(issubclass(impl, Clock)) + self.assertTrue(issubclass(impl, Clock)) clock = object.__new__(impl) time = clock.utc_time() self.assertIsInstance(time, ClockTime) diff --git a/tests/unit/time/test_datetime.py b/tests/unit/time/test_datetime.py index 4f98b8c6b..4e33f90c8 100644 --- a/tests/unit/time/test_datetime.py +++ b/tests/unit/time/test_datetime.py @@ -36,7 +36,7 @@ ) from neo4j.time import ( - DateTime, + DateTime as _DateTime, MIN_YEAR, MAX_YEAR, Duration, @@ -53,6 +53,19 @@ timezone_utc = timezone("UTC") +class DateTime(_DateTime): + def __new__(cls, *args, **kwargs): + second = kwargs.get("seconds", args[5] if len(args) > 5 else None) + if isinstance(second, float) and not second.is_integer(): + with pytest.warns( + DeprecationWarning, + match="Float support for `second` will be removed in 5.0. " + "Use `nanosecond` instead." + ): + return super().__new__(cls, *args, **kwargs) + return super().__new__(cls, *args, **kwargs) + + def seconds_options(seconds, nanoseconds): yield seconds, nanoseconds yield seconds + nanoseconds / 1000000000, @@ -71,7 +84,10 @@ def test_zero(self): @pytest.mark.parametrize("seconds_args", [*seconds_options(17, 914390409)]) def test_non_zero_naive(self, seconds_args): - t = DateTime(2018, 4, 26, 23, 0, *seconds_args) + if isinstance(seconds_args[0], float): + t = DateTime(2018, 4, 26, 23, 0, *seconds_args) + else: + t = DateTime(2018, 4, 26, 23, 0, *seconds_args) assert t.year == 2018 assert t.month == 4 assert t.day == 26 @@ -433,7 +449,8 @@ def test_datetime_deep_copy(self): def test_iso_format_with_time_zone_case_1(): # python -m pytest tests/unit/time/test_datetime.py -s -v -k test_iso_format_with_time_zone_case_1 - expected = DateTime(2019, 10, 30, 7, 54, 2.129790999, tzinfo=timezone_utc) + expected = DateTime(2019, 10, 30, 7, 54, 2.129790999, + tzinfo=timezone_utc) assert expected.iso_format() == "2019-10-30T07:54:02.129790999+00:00" assert expected.tzinfo == FixedOffset(0) actual = DateTime.from_iso_format("2019-10-30T07:54:02.129790999+00:00") diff --git a/tests/unit/time/test_duration.py b/tests/unit/time/test_duration.py index aa29f7508..7417748e5 100644 --- a/tests/unit/time/test_duration.py +++ b/tests/unit/time/test_duration.py @@ -26,7 +26,71 @@ import pytest from neo4j import time -from neo4j.time import Duration +from neo4j.time import Duration as _Duration + + +class Duration(_Duration): + def __new__(cls, *args, **kwargs): + # seconds = kwargs.get("seconds", args[6] if len(args) > 6 else None) + subseconds = kwargs.get("subseconds", + args[7] if len(args) > 7 else None) + # if ( + # isinstance(seconds, float) and not seconds.is_integer() + # or isinstance(seconds, Decimal) and not seconds % 1 != 0 + # ): + # with pytest.warns(DeprecationWarning, + # match="Float support second will be removed in " + # "5.0. Use `nanosecond` instead."): + # return super().__new__(cls, *args, **kwargs) + if subseconds is not None: + with pytest.warns( + DeprecationWarning, + match="`subseconds` will be removed in 5.0. " + "Use `nanoseconds` instead." + ): + return super().__new__(cls, *args, **kwargs) + return super().__new__(cls, *args, **kwargs) + + @property + def hours_minutes_seconds(self): + with pytest.warns( + DeprecationWarning, + match="Will be removed in 5.0. " + "Use `hours_minutes_seconds_nanoseconds` instead." + ): + return super().hours_minutes_seconds + + @property + def subseconds(self): + with pytest.warns( + DeprecationWarning, + match="Will be removed in 5.0. Use `nanoseconds` instead." + ): + return super().subseconds + + def __mul__(self, other): + if isinstance(other, float): + with pytest.warns(DeprecationWarning, + match="Multiplication with float will be " + "removed in 5.0."): + return super().__mul__(other) + return super().__mul__(other) + + def __floordiv__(self, other): + with pytest.warns(DeprecationWarning, match="Will be removed in 5.0."): + return super().__floordiv__(other) + + def __mod__(self, other): + with pytest.warns(DeprecationWarning, match="Will be removed in 5.0."): + return super().__mod__(other) + + def __divmod__(self, other): + with pytest.warns(DeprecationWarning, match="Will be removed in 5.0."): + return super().__divmod__(other) + + def __truediv__(self, other): + with pytest.warns(DeprecationWarning, match="Will be removed in 5.0."): + return super().__truediv__(other) def seconds_options(seconds, nanoseconds): diff --git a/tests/unit/time/test_time.py b/tests/unit/time/test_time.py index 62d79edd2..f17dc5ade 100644 --- a/tests/unit/time/test_time.py +++ b/tests/unit/time/test_time.py @@ -35,13 +35,49 @@ utc, ) -from neo4j.time import Time +from neo4j.time import Time as _Time from neo4j.time.arithmetic import ( nano_add, nano_div, ) +class _TimeMeta(type(_Time)): + def __instancecheck__(self, instance): + return (isinstance(instance, _Time) + or super().__instancecheck__(instance)) + + def __subclasscheck__(self, subclass): + return (issubclass(subclass, _Time) + or super().__subclasscheck__(subclass)) + + +class Time(_Time, metaclass=_TimeMeta): + def __new__(cls, *args, **kwargs): + second = kwargs.get("second", args[2] if len(args) > 2 else None) + if isinstance(second, float) and not second.is_integer(): + with pytest.warns( + DeprecationWarning, + match="Float support for `second` will be removed in 5.0. " + "Use `nanosecond` instead." + ): + return super().__new__(cls, *args, **kwargs) + return super().__new__(cls, *args, **kwargs) + + @property + def hour_minute_second(self): + with pytest.warns( + DeprecationWarning, + match="`hour_minute_second` will be removed in 5.0. " + "Use `hour_minute_second_nanosecond` instead." + ): + return super().hour_minute_second + + @classmethod + def utc_now(cls): + return super().utc_now() + + timezone_us_eastern = timezone("US/Eastern") timezone_utc = timezone("UTC") diff --git a/tests/unit/work/test_result.py b/tests/unit/work/test_result.py index 2cf5ddeb9..7ce85ba4e 100644 --- a/tests/unit/work/test_result.py +++ b/tests/unit/work/test_result.py @@ -20,12 +20,14 @@ from unittest import mock +import warnings import pandas as pd import pytest from neo4j import ( Address, + ExperimentalWarning, Record, ResultSummary, ServerInfo, @@ -306,7 +308,8 @@ def test_result_single(records, fetch_size): connection = ConnectionStub(records=Records(["x"], records)) result = Result(connection, HydratorStub(), fetch_size, noop, noop) result._run("CYPHER", {}, None, None, "r", None) - with pytest.warns(None) as warning_record: + with warnings.catch_warnings(record=True) as warning_record: + warnings.simplefilter("always") record = result.single() if not records: assert not warning_record @@ -350,7 +353,13 @@ def test_consume(records, consume_one, summary_meta): assert summary.database is None server_info = summary.server assert isinstance(server_info, ServerInfo) - assert server_info.version_info() == Version(4, 3) + with pytest.warns( + DeprecationWarning, + match="The version_info method is deprecated, please use " + "ServerInfo.agent, ServerInfo.protocol_version, or call " + "the dbms.components procedure instead" + ): + assert server_info.version_info() == Version(4, 3) assert server_info.protocol_version == Version(4, 3) assert isinstance(summary.counters, SummaryCounters) @@ -463,7 +472,8 @@ def test_to_df(keys, values, types, instances): connection = ConnectionStub(records=Records(keys, values)) result = Result(connection, DataHydrator(), 1, noop, noop) result._run("CYPHER", {}, None, None, "r", None) - df = result.to_df() + with pytest.warns(ExperimentalWarning, match="pandas support"): + df = result.to_df() assert isinstance(df, pd.DataFrame) assert df.keys().to_list() == keys diff --git a/tox-unit.ini b/tox-unit.ini index 8ac52441f..ea9bdc839 100644 --- a/tox-unit.ini +++ b/tox-unit.ini @@ -7,6 +7,6 @@ deps = -r tests/requirements.txt commands = coverage erase - coverage run -m pytest -v {posargs} \ + coverage run -m pytest -W error -v {posargs} \ tests/unit coverage report diff --git a/tox.ini b/tox.ini index 36eb6506d..17afb1b77 100644 --- a/tox.ini +++ b/tox.ini @@ -31,7 +31,7 @@ deps = -r tests/requirements.txt commands = coverage erase - coverage run -m pytest -v {posargs} \ + coverage run -m pytest -W error -v {posargs} \ tests/unit \ tests/stub \ tests/integration From 070a3bef34a443543c76ce28595c9c1e319e0463 Mon Sep 17 00:00:00 2001 From: Rouven Bauer Date: Thu, 1 Sep 2022 12:01:15 +0200 Subject: [PATCH 02/10] TestKit backend: fix missing Python build dependency --- testkit/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testkit/Dockerfile b/testkit/Dockerfile index fe8130ae5..41e5ed28b 100644 --- a/testkit/Dockerfile +++ b/testkit/Dockerfile @@ -25,7 +25,7 @@ RUN apt-get update && \ apt-get install -y --no-install-recommends \ make build-essential libssl-dev zlib1g-dev libbz2-dev libreadline-dev \ libsqlite3-dev wget curl llvm libncurses5-dev xz-utils tk-dev \ - libxml2-dev libxmlsec1-dev libffi-dev \ + libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev \ ca-certificates && \ apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* From e2d79e502ea80aa437b01e33f0c262aa712c57f9 Mon Sep 17 00:00:00 2001 From: Rouven Bauer Date: Thu, 1 Sep 2022 12:56:41 +0200 Subject: [PATCH 03/10] TestKit backend: fix warning assertions --- testkitbackend/_warning_check.py | 64 ++++++++++++++++++++++++++++++++ testkitbackend/requests.py | 45 +++++++++++++++++----- 2 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 testkitbackend/_warning_check.py diff --git a/testkitbackend/_warning_check.py b/testkitbackend/_warning_check.py new file mode 100644 index 000000000..7fb25aa23 --- /dev/null +++ b/testkitbackend/_warning_check.py @@ -0,0 +1,64 @@ +# Copyright (c) "Neo4j" +# Neo4j Sweden AB [https://neo4j.com] +# +# This file is part of Neo4j. +# +# 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. + + +import re +import warnings +from contextlib import contextmanager + + +@contextmanager +def warning_check(category, message): + with warnings.catch_warnings(record=True) as warn_log: + warnings.filterwarnings("always", category=category, message=message) + yield + if len(warn_log) != 1: + raise AssertionError("Expected 1 warning, found %d: %s" + % (len(warn_log), warn_log)) + + +@contextmanager +def warnings_check(category_message_pairs): + with warnings.catch_warnings(record=True) as warn_log: + for category, message in category_message_pairs: + warnings.filterwarnings("always", category=category, + message=message) + yield + if len(warn_log) != len(category_message_pairs): + raise AssertionError( + "Expected %d warnings, found %d: %s" + % (len(category_message_pairs), len(warn_log), warn_log) + ) + category_message_pairs = [ + (category, re.compile(message, re.I)) + for category, message in category_message_pairs + ] + for category, matcher in category_message_pairs: + match = None + for i, warning in enumerate(warn_log): + if ( + warning.category == category + and matcher.match(warning.message.args[0]) + ): + match = i + break + if match is None: + raise AssertionError( + "Expected warning not found: %r %r" + % (category, matcher.pattern) + ) + warn_log.pop(match) diff --git a/testkitbackend/requests.py b/testkitbackend/requests.py index 25aeada1a..1e4ae40d2 100644 --- a/testkitbackend/requests.py +++ b/testkitbackend/requests.py @@ -14,15 +14,21 @@ # 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. + + import json from os import path -import warnings import neo4j import testkitbackend.fromtestkit as fromtestkit import testkitbackend.totestkit as totestkit from testkitbackend.fromtestkit import to_meta_and_timeout +from ._warning_check import ( + warning_check, + warnings_check, +) + class FrontendError(Exception): pass @@ -98,9 +104,21 @@ def NewDriver(backend, data): data.mark_item_as_read_if_equals("livenessCheckTimeoutMs", None) data.mark_item_as_read("domainNameResolverRegistered") - driver = neo4j.GraphDatabase.driver( - data["uri"], auth=auth, user_agent=data["userAgent"], **kwargs - ) + expected_warnings = [] + if "update_routing_table_timeout" in kwargs: + expected_warnings.append(( + DeprecationWarning, + "The 'update_routing_table_timeout' config key is deprecated" + )) + if "session_connection_timeout" in kwargs: + expected_warnings.append(( + DeprecationWarning, + "The 'session_connection_timeout' config key is deprecated" + )) + with warnings_check(expected_warnings): + driver = neo4j.GraphDatabase.driver( + data["uri"], auth=auth, user_agent=data["userAgent"], **kwargs + ) key = backend.next_key() backend.drivers[key] = driver backend.send_response("Driver", {"id": key}) @@ -109,8 +127,10 @@ def NewDriver(backend, data): def VerifyConnectivity(backend, data): driver_id = data["driverId"] driver = backend.drivers[driver_id] - with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=neo4j.ExperimentalWarning) + with warning_check( + neo4j.ExperimentalWarning, + "The configuration may change in the future." + ): driver.verify_connectivity() backend.send_response("Driver", {"id": driver_id}) @@ -118,10 +138,15 @@ def VerifyConnectivity(backend, data): def CheckMultiDBSupport(backend, data): driver_id = data["driverId"] driver = backend.drivers[driver_id] - backend.send_response( - "MultiDBSupport", - {"id": backend.next_key(), "available": driver.supports_multi_db()} - ) + with warning_check( + neo4j.ExperimentalWarning, + "Feature support query, based on Bolt protocol version and Neo4j " + "server version will change in the future." + ): + available = driver.supports_multi_db() + backend.send_response("MultiDBSupport", { + "id": backend.next_key(), "available": available + }) def resolution_func(backend, custom_resolver=False, custom_dns_resolver=False): From 556203aef96f78ffe54d3685d6925d3c69798ea0 Mon Sep 17 00:00:00 2001 From: Rouven Bauer Date: Thu, 1 Sep 2022 13:36:43 +0200 Subject: [PATCH 04/10] Undo accidentally changed IT configuration --- tests/integration/conftest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index d65c16f5f..015ba64d8 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -43,14 +43,14 @@ NEO4J_RELEASES = getenv("NEO4J_RELEASES", "snapshot-enterprise 3.5-enterprise").split() NEO4J_HOST = "localhost" NEO4J_PORTS = { - "bolt": 7687, + "bolt": 17601, "http": 17401, "https": 17301, } -NEO4J_CORES = 1 -NEO4J_REPLICAS = 0 +NEO4J_CORES = 3 +NEO4J_REPLICAS = 2 NEO4J_USER = "neo4j" -NEO4J_PASSWORD = "pass" +NEO4J_PASSWORD = "password" NEO4J_AUTH = (NEO4J_USER, NEO4J_PASSWORD) NEO4J_LOCK = RLock() NEO4J_SERVICE = None From 50ceb59bce2a010a45d1e6aa10f84808526542db Mon Sep 17 00:00:00 2001 From: Rouven Bauer Date: Thu, 1 Sep 2022 15:17:10 +0200 Subject: [PATCH 05/10] Improve TestKit glue output --- testkit/stress.py | 4 +--- testkit/unittests.py | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/testkit/stress.py b/testkit/stress.py index bc9718f2a..3af08d0c2 100644 --- a/testkit/stress.py +++ b/testkit/stress.py @@ -1,8 +1,6 @@ import subprocess -import os import sys - if __name__ == "__main__": # Until below works sys.exit(0) @@ -16,4 +14,4 @@ "NEO4J_PASSWORD": os.environ["TEST_NEO4J_PASS"], "NEO4J_URI": uri} subprocess.check_call(cmd, universal_newlines=True, - stderr=subprocess.STDOUT, env=env) + stdout=sys.stdout, stderr=sys.stderr, env=env) diff --git a/testkit/unittests.py b/testkit/unittests.py index 9561611d6..997ade533 100644 --- a/testkit/unittests.py +++ b/testkit/unittests.py @@ -1,9 +1,12 @@ import subprocess +import sys def run(args): subprocess.run( - args, universal_newlines=True, stderr=subprocess.STDOUT, check=True) + args, universal_newlines=True, stdout=sys.stdout, stderr=sys.stderr, + check=True + ) if __name__ == "__main__": From 3806cabd4dcd090b72bc69b6dc47e443cadfb002 Mon Sep 17 00:00:00 2001 From: Rouven Bauer Date: Thu, 1 Sep 2022 16:25:00 +0200 Subject: [PATCH 06/10] Close stub server pipes --- tests/stub/conftest.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/stub/conftest.py b/tests/stub/conftest.py index c8d91e698..6a9c4aa59 100644 --- a/tests/stub/conftest.py +++ b/tests/stub/conftest.py @@ -21,7 +21,6 @@ import subprocess import os -import time from platform import system from threading import Thread @@ -65,12 +64,14 @@ def wait(self): pass log.debug(line) + self._process.__exit__(None, None, None) return True def kill(self): # Kill process if not already dead if self._process.poll() is None: self._process.kill() + self._process.__exit__(None, None, None) class StubCluster: From b457a658892ea658aab7e1b073268a7f1d48f04c Mon Sep 17 00:00:00 2001 From: Rouven Bauer Date: Thu, 1 Sep 2022 16:59:47 +0200 Subject: [PATCH 07/10] Test: only use `WAIT` cypher keyword when supported --- .../examples/test_database_selection_example.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/integration/examples/test_database_selection_example.py b/tests/integration/examples/test_database_selection_example.py index e92bd94cb..0eb451fbd 100644 --- a/tests/integration/examples/test_database_selection_example.py +++ b/tests/integration/examples/test_database_selection_example.py @@ -28,6 +28,7 @@ from neo4j import GraphDatabase # tag::database-selection-import[] from neo4j import READ_ACCESS +import neo4j.exceptions # end::database-selection-import[] from neo4j.exceptions import ServiceUnavailable @@ -41,13 +42,21 @@ class DatabaseSelectionExample: def __init__(self, uri, user, password): self.driver = GraphDatabase.driver(uri, auth=(user, password)) + self._wait = " WAIT" with self.driver.session(database="system") as session: - session.run("DROP DATABASE example IF EXISTS WAIT").consume() - session.run("CREATE DATABASE example WAIT").consume() + while True: + try: + session.run(f"DROP DATABASE example IF EXISTS{self._wait}")\ + .consume() + except neo4j.exceptions.CypherSyntaxError: + if not self._wait: + raise + self._wait = "" + session.run(f"CREATE DATABASE example{self._wait}").consume() def close(self): with self.driver.session(database="system") as session: - session.run("DROP DATABASE example WAIT").consume() + session.run(f"DROP DATABASE example{self._wait}").consume() self.driver.close() def run_example_code(self): From 8d10c73191eb998b9d8eb90a766bd31f6bc524f6 Mon Sep 17 00:00:00 2001 From: Rouven Bauer Date: Thu, 1 Sep 2022 17:27:55 +0200 Subject: [PATCH 08/10] Fix session config consumption --- neo4j/__init__.py | 4 ---- neo4j/conf.py | 12 +++++++++++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/neo4j/__init__.py b/neo4j/__init__.py index 6266a4462..20fb0d1ca 100644 --- a/neo4j/__init__.py +++ b/neo4j/__init__.py @@ -383,13 +383,11 @@ def session(self, **config): """ from neo4j.work.simple import Session session_config = SessionConfig(self._default_workspace_config, config) - SessionConfig.consume(config) # Consume the config return Session(self._pool, session_config) def pipeline(self, **config): from neo4j.work.pipelining import Pipeline, PipelineConfig pipeline_config = PipelineConfig(self._default_workspace_config, config) - PipelineConfig.consume(config) # Consume the config return Pipeline(self._pool, pipeline_config) @experimental("The configuration may change in the future.") @@ -427,13 +425,11 @@ def __init__(self, pool, default_workspace_config): def session(self, **config): session_config = SessionConfig(self._default_workspace_config, config) - SessionConfig.consume(config) # Consume the config return Session(self._pool, session_config) def pipeline(self, **config): from neo4j.work.pipelining import Pipeline, PipelineConfig pipeline_config = PipelineConfig(self._default_workspace_config, config) - PipelineConfig.consume(config) # Consume the config return Pipeline(self._pool, pipeline_config) @experimental("The configuration may change in the future.") diff --git a/neo4j/conf.py b/neo4j/conf.py index 90aedce5e..cb18ee26f 100644 --- a/neo4j/conf.py +++ b/neo4j/conf.py @@ -170,9 +170,19 @@ def set_attr(k, v): else: raise AttributeError(k) + rejected_keys = [] for key, value in data_dict.items(): if value is not None: - set_attr(key, value) + try: + set_attr(key, value) + except AttributeError as exc: + if not exc.args == (key,): + raise + rejected_keys.append(key) + + if rejected_keys: + raise ConfigurationError("Unexpected config keys: " + + ", ".join(rejected_keys)) def __init__(self, *args, **kwargs): for arg in args: From 6ee8ae8992469e5a51a3b2748a00469242b115a5 Mon Sep 17 00:00:00 2001 From: Rouven Bauer Date: Fri, 2 Sep 2022 08:40:58 +0200 Subject: [PATCH 09/10] TestKit backend: close resources when done --- testkitbackend/backend.py | 17 +++++++++++++++++ testkitbackend/server.py | 7 +++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/testkitbackend/backend.py b/testkitbackend/backend.py index 497735517..78eb25796 100644 --- a/testkitbackend/backend.py +++ b/testkitbackend/backend.py @@ -110,6 +110,23 @@ def __init__(self, rd, wr): self._requestHandlers = dict( [m for m in getmembers(requests, isfunction)]) + def close(self): + for dict_of_closables in ( + self.transactions, + {key: tracker.session for key, tracker in self.sessions.items()}, + self.drivers, + ): + for key, closable in dict_of_closables.items(): + try: + closable.close() + except (Neo4jError, DriverError, OSError): + log.error( + "Error during TestKit backend garbage collection. " + "While collecting: (key: %s) %s\n%s", + key, closable, traceback.format_exc() + ) + dict_of_closables.clear() + def next_key(self): self.key = self.key + 1 return self.key diff --git a/testkitbackend/server.py b/testkitbackend/server.py index 457281641..27908767c 100644 --- a/testkitbackend/server.py +++ b/testkitbackend/server.py @@ -25,7 +25,10 @@ def __init__(self, address): class Handler(StreamRequestHandler): def handle(self): backend = Backend(self.rfile, self.wfile) - while backend.process_request(): - pass + try: + while backend.process_request(): + pass + finally: + backend.close() print("Disconnected") super(Server, self).__init__(address, Handler) From 120c676bc8f113459e7d6edf6c3b790f8036699d Mon Sep 17 00:00:00 2001 From: Rouven Bauer Date: Fri, 2 Sep 2022 08:49:25 +0200 Subject: [PATCH 10/10] Fix loop mistake in test --- tests/integration/examples/test_database_selection_example.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/examples/test_database_selection_example.py b/tests/integration/examples/test_database_selection_example.py index 0eb451fbd..8c69d8506 100644 --- a/tests/integration/examples/test_database_selection_example.py +++ b/tests/integration/examples/test_database_selection_example.py @@ -46,8 +46,10 @@ def __init__(self, uri, user, password): with self.driver.session(database="system") as session: while True: try: - session.run(f"DROP DATABASE example IF EXISTS{self._wait}")\ + session\ + .run(f"DROP DATABASE example IF EXISTS{self._wait}")\ .consume() + break except neo4j.exceptions.CypherSyntaxError: if not self._wait: raise