Skip to content

Commit 075cc28

Browse files
committed
Migrate temporal and spatial ITs to TestKit
1 parent 3389c4a commit 075cc28

28 files changed

+693
-675
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@
8787
does not offer the `commit`, `rollback`, `close`, and `closed` methods.
8888
Those methods would have caused a hard to interpreted error previously. Hence,
8989
they have been removed.
90+
- Deprecated Nodes' and Relationships' `id` property (`int`) in favor of
91+
`element_id` (`str`).
92+
This also affects `Graph` objects as `graph.nodes[...]` and
93+
`graph.relationships[...]` now prefers strings over integers.
94+
- `ServerInfo.connection_id` has been deprecated and will be removed in a
95+
future release. There is no replacement as this is considered internal
96+
information.
9097

9198

9299
## Version 4.4

neo4j/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,8 @@ def agent(self):
308308
return self._metadata.get("server")
309309

310310
@property
311+
@deprecated("The connection id is considered internal information "
312+
"and will no longer be exposed in future versions.")
311313
def connection_id(self):
312314
""" Unique identifier for the remote server connection.
313315
"""

neo4j/graph/__init__.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@
3131

3232
from collections.abc import Mapping
3333

34-
from ..meta import deprecated
34+
from ..meta import (
35+
deprecated,
36+
deprecation_warn,
37+
)
3538

3639

3740
class Graph:
@@ -253,6 +256,20 @@ def __init__(self, entity_dict):
253256
self._entity_dict = entity_dict
254257

255258
def __getitem__(self, e_id):
259+
# TODO: 6.0 - remove this compatibility shim
260+
if isinstance(e_id, (int, float, complex)):
261+
deprecation_warn(
262+
"Accessing entities by an integer id is deprecated, "
263+
"use the new style element_id (str) instead"
264+
)
265+
if isinstance(e_id, float) and int(e_id) == e_id:
266+
# Non-int floats would always fail for legacy IDs
267+
e_id = int(e_id)
268+
elif isinstance(e_id, complex) and int(e_id.real) == e_id:
269+
# complex numbers with imaginary parts or non-integer real
270+
# parts would always fail for legacy IDs
271+
e_id = int(e_id.real)
272+
e_id = str(e_id)
256273
return self._entity_dict[e_id]
257274

258275
def __len__(self):

neo4j/time/__init__.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1657,6 +1657,19 @@ def tzinfo(self):
16571657

16581658
# OPERATIONS #
16591659

1660+
@staticmethod
1661+
def _native_time_to_ticks(native_time):
1662+
return int(3600000000000 * native_time.hour
1663+
+ 60000000000 * native_time.minute
1664+
+ NANO_SECONDS * native_time.second
1665+
+ 1000 * native_time.microsecond)
1666+
1667+
def _check_both_naive_or_tz_aware(self, other):
1668+
if (isinstance(other, (time, Time))
1669+
and ((self.tzinfo is None) ^ (other.tzinfo is None))):
1670+
raise TypeError("can't compare offset-naive and offset-aware "
1671+
"times")
1672+
16601673
def __hash__(self):
16611674
""""""
16621675
return hash(self.__ticks) ^ hash(self.tzinfo)
@@ -1666,10 +1679,7 @@ def __eq__(self, other):
16661679
if isinstance(other, Time):
16671680
return self.__ticks == other.__ticks and self.tzinfo == other.tzinfo
16681681
if isinstance(other, time):
1669-
other_ticks = (3600000000000 * other.hour
1670-
+ 60000000000 * other.minute
1671-
+ NANO_SECONDS * other.second
1672-
+ 1000 * other.microsecond)
1682+
other_ticks = self._native_time_to_ticks(other)
16731683
return self.ticks == other_ticks and self.tzinfo == other.tzinfo
16741684
return False
16751685

@@ -1679,50 +1689,50 @@ def __ne__(self, other):
16791689

16801690
def __lt__(self, other):
16811691
"""`<` comparison with :class:`.Time` or :class:`datetime.time`."""
1692+
self._check_both_naive_or_tz_aware(other)
16821693
if isinstance(other, Time):
16831694
return (self.tzinfo == other.tzinfo
16841695
and self.ticks < other.ticks)
16851696
if isinstance(other, time):
16861697
if self.tzinfo != other.tzinfo:
16871698
return False
1688-
other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000)
1689-
return self.ticks < other_ticks
1699+
return self.ticks < self._native_time_to_ticks(other)
16901700
return NotImplemented
16911701

16921702
def __le__(self, other):
16931703
"""`<=` comparison with :class:`.Time` or :class:`datetime.time`."""
1704+
self._check_both_naive_or_tz_aware(other)
16941705
if isinstance(other, Time):
16951706
return (self.tzinfo == other.tzinfo
16961707
and self.ticks <= other.ticks)
16971708
if isinstance(other, time):
16981709
if self.tzinfo != other.tzinfo:
16991710
return False
1700-
other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000)
1701-
return self.ticks <= other_ticks
1711+
return self.ticks <= self._native_time_to_ticks(other)
17021712
return NotImplemented
17031713

17041714
def __ge__(self, other):
17051715
"""`>=` comparison with :class:`.Time` or :class:`datetime.time`."""
1716+
self._check_both_naive_or_tz_aware(other)
17061717
if isinstance(other, Time):
17071718
return (self.tzinfo == other.tzinfo
17081719
and self.ticks >= other.ticks)
17091720
if isinstance(other, time):
17101721
if self.tzinfo != other.tzinfo:
17111722
return False
1712-
other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000)
1713-
return self.ticks >= other_ticks
1723+
return self.ticks >= self._native_time_to_ticks(other)
17141724
return NotImplemented
17151725

17161726
def __gt__(self, other):
17171727
"""`>` comparison with :class:`.Time` or :class:`datetime.time`."""
1728+
self._check_both_naive_or_tz_aware(other)
17181729
if isinstance(other, Time):
17191730
return (self.tzinfo == other.tzinfo
17201731
and self.ticks >= other.ticks)
17211732
if isinstance(other, time):
17221733
if self.tzinfo != other.tzinfo:
17231734
return False
1724-
other_ticks = 3600 * other.hour + 60 * other.minute + other.second + (other.microsecond / 1000000)
1725-
return self.ticks >= other_ticks
1735+
return self.ticks >= self._native_time_to_ticks(other)
17261736
return NotImplemented
17271737

17281738
def __copy__(self):
@@ -2203,7 +2213,8 @@ def __eq__(self, other):
22032213
`==` comparison with :class:`.DateTime` or :class:`datetime.datetime`.
22042214
"""
22052215
if isinstance(other, (DateTime, datetime)):
2206-
return self.date() == other.date() and self.time() == other.time()
2216+
return (self.date() == other.date()
2217+
and self.timetz() == other.timetz())
22072218
return False
22082219

22092220
def __ne__(self, other):
@@ -2218,7 +2229,7 @@ def __lt__(self, other):
22182229
"""
22192230
if isinstance(other, (DateTime, datetime)):
22202231
if self.date() == other.date():
2221-
return self.time() < other.time()
2232+
return self.timetz() < other.timetz()
22222233
else:
22232234
return self.date() < other.date()
22242235
return NotImplemented

neo4j/time/hydration.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def dehydrate_datetime(value):
136136
""" Dehydrator for `datetime` values.
137137
138138
:param value:
139-
:type value: datetime
139+
:type value: datetime or DateTime
140140
:return:
141141
"""
142142

@@ -167,7 +167,8 @@ def seconds_and_nanoseconds(dt):
167167
else:
168168
# with time offset
169169
seconds, nanoseconds = seconds_and_nanoseconds(value)
170-
return Structure(b"F", seconds, nanoseconds, tz.utcoffset(value).seconds)
170+
return Structure(b"F", seconds, nanoseconds,
171+
int(tz.utcoffset(value).total_seconds()))
171172

172173

173174
def hydrate_duration(months, days, seconds, nanoseconds):

testkit/build.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,22 @@
1919

2020

2121
"""
22-
Executed in Go driver container.
22+
Executed in driver container.
2323
Responsible for building driver and test backend.
2424
"""
2525

2626

2727
import subprocess
28+
import sys
2829

2930

3031
def run(args, env=None):
31-
subprocess.run(args, universal_newlines=True, stderr=subprocess.STDOUT,
32-
check=True, env=env)
32+
subprocess.run(args, universal_newlines=True, stdout=sys.stdout,
33+
stderr=sys.stderr, check=True, env=env)
3334

3435

3536
if __name__ == "__main__":
3637
run(["python", "setup.py", "build"])
38+
run(["python", "-m", "pip", "install", "-U", "pip"])
39+
run(["python", "-m", "pip", "install", "-Ur",
40+
"testkitbackend/requirements.txt"])

testkitbackend/_async/requests.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import json
2020
from os import path
21+
import warnings
2122

2223
import neo4j
2324
from neo4j._async_compat.util import AsyncUtil
@@ -411,6 +412,17 @@ async def ResultSingle(backend, data):
411412
))
412413

413414

415+
async def ResultSingleOptional(backend, data):
416+
result = backend.results[data["resultId"]]
417+
with warnings.catch_warnings(record=True) as warning_list:
418+
record = await result.single(strict=False)
419+
if record:
420+
record = totestkit.record(record)
421+
await backend.send_response("RecordOptional", {
422+
"record": record, "warnings": list(map(str, warning_list))
423+
})
424+
425+
414426
async def ResultPeek(backend, data):
415427
result = backend.results[data["resultId"]]
416428
record = await result.peek()

testkitbackend/_sync/requests.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import json
2020
from os import path
21+
import warnings
2122

2223
import neo4j
2324
from neo4j._async_compat.util import Util
@@ -411,6 +412,17 @@ def ResultSingle(backend, data):
411412
))
412413

413414

415+
def ResultSingleOptional(backend, data):
416+
result = backend.results[data["resultId"]]
417+
with warnings.catch_warnings(record=True) as warning_list:
418+
record = result.single(strict=False)
419+
if record:
420+
record = totestkit.record(record)
421+
backend.send_response("RecordOptional", {
422+
"record": record, "warnings": list(map(str, warning_list))
423+
})
424+
425+
414426
def ResultPeek(backend, data):
415427
result = backend.results[data["resultId"]]
416428
record = result.peek()

testkitbackend/fromtestkit.py

Lines changed: 81 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,21 @@
1616
# limitations under the License.
1717

1818

19+
from datetime import timedelta
20+
21+
import pytz
22+
1923
from neo4j import Query
24+
from neo4j.spatial import (
25+
CartesianPoint,
26+
WGS84Point,
27+
)
28+
from neo4j.time import (
29+
Date,
30+
DateTime,
31+
Duration,
32+
Time,
33+
)
2034

2135

2236
def to_cypher_and_params(data):
@@ -54,24 +68,81 @@ def to_query_and_params(data):
5468
def to_param(m):
5569
""" Converts testkit parameter format to driver (python) parameter
5670
"""
57-
value = m["data"]["value"]
71+
data = m["data"]
5872
name = m["name"]
5973
if name == "CypherNull":
74+
if data["value"] is not None:
75+
raise ValueError("CypherNull should be None")
6076
return None
6177
if name == "CypherString":
62-
return str(value)
78+
return str(data["value"])
6379
if name == "CypherBool":
64-
return bool(value)
80+
return bool(data["value"])
6581
if name == "CypherInt":
66-
return int(value)
82+
return int(data["value"])
6783
if name == "CypherFloat":
68-
return float(value)
84+
return float(data["value"])
6985
if name == "CypherString":
70-
return str(value)
86+
return str(data["value"])
7187
if name == "CypherBytes":
72-
return bytearray([int(byte, 16) for byte in value.split()])
88+
return bytearray([int(byte, 16) for byte in data["value"].split()])
7389
if name == "CypherList":
74-
return [to_param(v) for v in value]
90+
return [to_param(v) for v in data["value"]]
7591
if name == "CypherMap":
76-
return {k: to_param(value[k]) for k in value}
77-
raise Exception("Unknown param type " + name)
92+
return {k: to_param(data["value"][k]) for k in data["value"]}
93+
if name == "CypherPoint":
94+
coords = [data["x"], data["y"]]
95+
if data.get("z") is not None:
96+
coords.append(data["z"])
97+
if data["system"] == "cartesian":
98+
return CartesianPoint(coords)
99+
if data["system"] == "wgs84":
100+
return WGS84Point(coords)
101+
raise ValueError("Unknown point system: {}".format(data["system"]))
102+
if name == "CypherDate":
103+
return Date(data["year"], data["month"], data["day"])
104+
if name == "CypherTime":
105+
tz = None
106+
utc_offset_s = data.get("utc_offset_s")
107+
if utc_offset_s is not None:
108+
utc_offset_m = utc_offset_s // 60
109+
if utc_offset_m * 60 != utc_offset_s:
110+
raise ValueError("the used timezone library only supports "
111+
"UTC offsets by minutes")
112+
tz = pytz.FixedOffset(utc_offset_m)
113+
return Time(data["hour"], data["minute"], data["second"],
114+
data["nanosecond"], tzinfo=tz)
115+
if name == "CypherDateTime":
116+
datetime = DateTime(
117+
data["year"], data["month"], data["day"],
118+
data["hour"], data["minute"], data["second"], data["nanosecond"]
119+
)
120+
utc_offset_s = data["utc_offset_s"]
121+
timezone_id = data["timezone_id"]
122+
if timezone_id is not None:
123+
utc_offset = timedelta(seconds=utc_offset_s)
124+
tz = pytz.timezone(timezone_id)
125+
localized_datetime = tz.localize(datetime, is_dst=False)
126+
if localized_datetime.utcoffset() == utc_offset:
127+
return localized_datetime
128+
localized_datetime = tz.localize(datetime, is_dst=True)
129+
if localized_datetime.utcoffset() == utc_offset:
130+
return localized_datetime
131+
raise ValueError(
132+
"cannot localize datetime %s to timezone %s with UTC "
133+
"offset %s" % (datetime, timezone_id, utc_offset)
134+
)
135+
elif utc_offset_s is not None:
136+
utc_offset_m = utc_offset_s // 60
137+
if utc_offset_m * 60 != utc_offset_s:
138+
raise ValueError("the used timezone library only supports "
139+
"UTC offsets by minutes")
140+
tz = pytz.FixedOffset(utc_offset_m)
141+
return tz.localize(datetime)
142+
return datetime
143+
if name == "CypherDuration":
144+
return Duration(
145+
months=data["months"], days=data["days"],
146+
seconds=data["seconds"], nanoseconds=data["nanoseconds"]
147+
)
148+
raise ValueError("Unknown param type " + name)

testkitbackend/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-r ../requirements.txt

0 commit comments

Comments
 (0)