Skip to content

Commit b856166

Browse files
committed
Async driver draft incl. unit tests & TestKit backend
1 parent d6a0d36 commit b856166

File tree

212 files changed

+13682
-3769
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

212 files changed

+13682
-3769
lines changed

.pre-commit-config.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,10 @@ repos:
3333
rev: 5.10.0
3434
hooks:
3535
- id: isort
36+
- repo: local
37+
hooks:
38+
- id: unasync
39+
name: unasync
40+
entry: bin/make-unasync
41+
language: system
42+
files: "^(neo4j/_async|tests/unit/async_|testkitbackend/_async)/.*"

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
- Python 3.10 support added
66
- Python 3.6 support has been dropped.
7-
7+
- `Result`, `Session`, and `Transaction`, can no longer be imported from
8+
`neo4j.work`. They should've been imported from `neo4j` all along.
9+
- Experimental pipelines feature has been removed.
10+
- Experimental async driver has been added.
811

912
## Version 4.4
1013

CONTRIBUTING.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ Remember that many community members have become regular contributors and some a
5353

5454
## Specifically for this project:
5555

56+
All code in `_sync` or `sync` folders is auto-generated. Don't change it, but
57+
install the pre-commit hooks as described below insted. They will take care of
58+
updating the code if necessary.
59+
5660
Setting up the development environment:
5761
* Install Python 3.6+
5862
* Install the requirements

bin/make-unasync

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
#!/usr/bin/env python
2+
3+
# Copyright (c) "Neo4j"
4+
# Neo4j Sweden AB [http://neo4j.com]
5+
#
6+
# This file is part of Neo4j.
7+
#
8+
# Licensed under the Apache License, Version 2.0 (the "License");
9+
# you may not use this file except in compliance with the License.
10+
# You may obtain a copy of the License at
11+
#
12+
# http://www.apache.org/licenses/LICENSE-2.0
13+
#
14+
# Unless required by applicable law or agreed to in writing, software
15+
# distributed under the License is distributed on an "AS IS" BASIS,
16+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17+
# See the License for the specific language governing permissions and
18+
# limitations under the License.
19+
20+
21+
import collections
22+
import errno
23+
from functools import reduce
24+
import os
25+
from pathlib import Path
26+
import sys
27+
import tokenize as std_tokenize
28+
29+
import isort
30+
import isort.files
31+
import unasync
32+
33+
34+
ROOT_DIR = Path(__file__).parents[1].absolute()
35+
ASYNC_DIR = ROOT_DIR / "neo4j" / "_async"
36+
SYNC_DIR = ROOT_DIR / "neo4j" / "_sync"
37+
ASYNC_TEST_DIR = ROOT_DIR / "tests" / "unit" / "async_"
38+
SYNC_TEST_DIR = ROOT_DIR / "tests" / "unit" / "sync"
39+
ASYNC_TESTKIT_BACKEND_DIR = ROOT_DIR / "testkitbackend" / "_async"
40+
SYNC_TESTKIT_BACKEND_DIR = ROOT_DIR / "testkitbackend" / "_sync"
41+
UNASYNC_SUFFIX = ".unasync"
42+
43+
PY_FILE_EXTENSIONS = {".py", ".pyi"}
44+
45+
46+
# copy from unasync for customization -----------------------------------------
47+
# https://github.com/python-trio/unasync
48+
# License: MIT and Apache2
49+
50+
51+
Token = collections.namedtuple(
52+
"Token", ["type", "string", "start", "end", "line"]
53+
)
54+
55+
56+
def _makedirs_existok(dir):
57+
try:
58+
os.makedirs(dir)
59+
except OSError as e:
60+
if e.errno != errno.EEXIST:
61+
raise
62+
63+
64+
def _get_tokens(f):
65+
if sys.version_info[0] == 2:
66+
for tok in std_tokenize.generate_tokens(f.readline):
67+
type_, string, start, end, line = tok
68+
yield Token(type_, string, start, end, line)
69+
else:
70+
for tok in std_tokenize.tokenize(f.readline):
71+
if tok.type == std_tokenize.ENCODING:
72+
continue
73+
yield tok
74+
75+
76+
def _tokenize(f):
77+
last_end = (1, 0)
78+
for tok in _get_tokens(f):
79+
if last_end[0] < tok.start[0]:
80+
yield "", std_tokenize.STRING, " \\\n"
81+
last_end = (tok.start[0], 0)
82+
83+
space = ""
84+
if tok.start > last_end:
85+
assert tok.start[0] == last_end[0]
86+
space = " " * (tok.start[1] - last_end[1])
87+
yield space, tok.type, tok.string
88+
89+
last_end = tok.end
90+
if tok.type in [std_tokenize.NEWLINE, std_tokenize.NL]:
91+
last_end = (tok.end[0] + 1, 0)
92+
93+
94+
def _untokenize(tokens):
95+
return "".join(space + tokval for space, tokval in tokens)
96+
97+
98+
# end of copy -----------------------------------------------------------------
99+
100+
101+
class CustomRule(unasync.Rule):
102+
def __init__(self, *args, **kwargs):
103+
super(CustomRule, self).__init__(*args, **kwargs)
104+
self.out_files = []
105+
106+
def _unasync_name(self, name):
107+
# copy from unasync to customize renaming rules
108+
# https://github.com/python-trio/unasync
109+
# License: MIT and Apache2
110+
if name in self.token_replacements:
111+
return self.token_replacements[name]
112+
# Convert class names from 'AsyncXyz' to 'Xyz'
113+
elif len(name) > 5 and name.startswith("Async") and name[5].isupper():
114+
return name[5:]
115+
# Convert variable/method/function names from 'async_xyz' to 'xyz'
116+
elif len(name) > 6 and name.startswith("async_"):
117+
return name[6:]
118+
return name
119+
120+
def _unasync_file(self, filepath):
121+
# copy from unasync to append file suffix to out path
122+
# https://github.com/python-trio/unasync
123+
# License: MIT and Apache2
124+
with open(filepath, "rb") as f:
125+
write_kwargs = {}
126+
if sys.version_info[0] >= 3:
127+
encoding, _ = std_tokenize.detect_encoding(f.readline)
128+
write_kwargs["encoding"] = encoding
129+
f.seek(0)
130+
tokens = _tokenize(f)
131+
tokens = self._unasync_tokens(tokens)
132+
result = _untokenize(tokens)
133+
outfile_path = filepath.replace(self.fromdir, self.todir)
134+
outfile_path += UNASYNC_SUFFIX
135+
self.out_files.append(outfile_path)
136+
_makedirs_existok(os.path.dirname(outfile_path))
137+
with open(outfile_path, "w", **write_kwargs) as f:
138+
print(result, file=f, end="")
139+
140+
141+
def apply_unasync(files):
142+
"""Generate sync code from async code."""
143+
144+
additional_main_replacements = {}
145+
additional_test_replacements = {
146+
"_async": "_sync",
147+
"mark_async_test": "mark_sync_test",
148+
}
149+
additional_testkit_backend_replacements = {}
150+
rules = [
151+
CustomRule(
152+
fromdir=str(ASYNC_DIR),
153+
todir=str(SYNC_DIR),
154+
additional_replacements=additional_main_replacements,
155+
),
156+
CustomRule(
157+
fromdir=str(ASYNC_TEST_DIR),
158+
todir=str(SYNC_TEST_DIR),
159+
additional_replacements=additional_test_replacements,
160+
),
161+
CustomRule(
162+
fromdir=str(ASYNC_TESTKIT_BACKEND_DIR),
163+
todir=str(SYNC_TESTKIT_BACKEND_DIR),
164+
additional_replacements=additional_testkit_backend_replacements,
165+
),
166+
]
167+
168+
if not files:
169+
paths = list(ASYNC_DIR.rglob("*"))
170+
paths += list(ASYNC_TEST_DIR.rglob("*"))
171+
paths += list(ASYNC_TESTKIT_BACKEND_DIR.rglob("*"))
172+
else:
173+
paths = [ROOT_DIR / Path(f) for f in files]
174+
filtered_paths = []
175+
for path in paths:
176+
if path.suffix in PY_FILE_EXTENSIONS:
177+
filtered_paths.append(path)
178+
179+
unasync.unasync_files(map(str, filtered_paths), rules)
180+
181+
return [Path(path) for rule in rules for path in rule.out_files]
182+
183+
184+
def apply_isort(paths):
185+
"""Sort imports in generated sync code.
186+
187+
Since classes in imports are renamed from AsyncXyz to Xyz, the alphabetical
188+
order of the import can change.
189+
"""
190+
isort_config = isort.Config(settings_path=str(ROOT_DIR), quiet=True)
191+
192+
for path in paths:
193+
isort.file(str(path), config=isort_config)
194+
195+
return paths
196+
197+
198+
def apply_changes(paths):
199+
def files_equal(path1, path2):
200+
with open(path1, "rb") as f1:
201+
with open(path2, "rb") as f2:
202+
data1 = f1.read(1024)
203+
data2 = f2.read(1024)
204+
while data1 or data2:
205+
if data1 != data2:
206+
changed_paths[path1] = path2
207+
return False
208+
data1 = f1.read(1024)
209+
data2 = f2.read(1024)
210+
return True
211+
212+
changed_paths = {}
213+
214+
for in_path in paths:
215+
out_path = Path(str(in_path)[:-len(UNASYNC_SUFFIX)])
216+
if not out_path.is_file():
217+
changed_paths[in_path] = out_path
218+
continue
219+
if not files_equal(in_path, out_path):
220+
changed_paths[in_path] = out_path
221+
continue
222+
in_path.unlink()
223+
224+
for in_path, out_path in changed_paths.items():
225+
in_path.replace(out_path)
226+
227+
return list(changed_paths.values())
228+
229+
230+
def main():
231+
files = None
232+
if len(sys.argv) >= 1:
233+
files = sys.argv[1:]
234+
paths = apply_unasync(files)
235+
paths = apply_isort(paths)
236+
changed_paths = apply_changes(paths)
237+
238+
if changed_paths:
239+
for path in changed_paths:
240+
print("Updated " + str(path))
241+
exit(1)
242+
243+
244+
if __name__ == "__main__":
245+
main()

docs/source/conf.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
#!/usr/bin/env python3
2-
#
31
# Neo4j Bolt Driver for Python documentation build configuration file, created by
42
# sphinx-quickstart on Mon Sep 21 11:48:02 2015.
53
#

0 commit comments

Comments
 (0)