Skip to content

Commit 6b6ca3b

Browse files
authored
Add 'Rule' to unasync for per-directory config (#55)
* Add 'Rule' to unasync for per-directory config * Switch to unasync.cmdclass_build_py(), update docs
1 parent 92dbca6 commit 6b6ca3b

File tree

10 files changed

+182
-71
lines changed

10 files changed

+182
-71
lines changed

README.rst

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,34 @@ And then in your :code:`setup.py` place the following code.
4444
4545
setuptools.setup(
4646
...
47-
cmdclass={'build_py': unasync.build_py},
47+
cmdclass={'build_py': unasync.cmdclass_build_py()},
4848
...
4949
)
5050
5151
And when you will build your package you will get your synchronous code in **_sync** folder.
5252

53+
If you'd like to customize where certain rules are applied you can pass
54+
customized :code:`unasync.Rule` instances to :code:`unasync.cmdclass_build_py()`
55+
56+
.. code-block:: python
57+
58+
import unasync
59+
60+
setuptools.setup(
61+
...
62+
cmdclass={'build_py': unasync.cmdclass_build_py(rules=[
63+
# This rule transforms files within 'ahip' -> 'hip'
64+
# instead of the default '_async' -> '_sync'.
65+
unasync.Rule("/ahip/", "/hip/"),
66+
67+
# This rule's 'fromdir' is more specific so will take precedent
68+
# over the above rule if the path is within /ahip/tests/...
69+
# This rule adds an additional token replacement over the default replacements.
70+
unasync.Rule("/ahip/tests/", "/hip/tests/", replacements={"ahip", "hip"}),
71+
])},
72+
...
73+
)
74+
5375
Documentation
5476
=============
5577

docs/source/index.rst

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
.. documentation master file, created by
2-
sphinx-quickstart on Sat Jan 21 19:11:14 2017.
3-
You can adapt this file completely to your liking, but it should at least
4-
contain the root `toctree` directive.
5-
6-
71
=======
82
unasync
93
=======
@@ -56,7 +50,7 @@ And then in your :code:`setup.py` place the following code.
5650
5751
setuptools.setup(
5852
...
59-
cmdclass={'build_py': unasync.build_py},
53+
cmdclass={'build_py': unasync.cmdclass_build_py()},
6054
...
6155
)
6256
@@ -70,6 +64,28 @@ Then create a file **pyproject.toml** in the root of your project and mention **
7064
7165
And when you will build your package you will get your synchronous code in **_sync** folder.
7266

67+
If you'd like to customize where certain rules are applied you can pass
68+
customized :code:`unasync.Rule` instances to :code:`unasync.cmdclass_build_py()`
69+
70+
.. code-block:: python
71+
72+
import unasync
73+
74+
setuptools.setup(
75+
...
76+
cmdclass={'build_py': unasync.cmdclass_build_py(rules=[
77+
# This rule transforms files within 'ahip' -> 'hip'
78+
# instead of the default '_async' -> '_sync'.
79+
unasync.Rule("/ahip/", "/hip/"),
80+
81+
# This rule's 'fromdir' is more specific so will take precedent
82+
# over the above rule if the path is within /ahip/tests/...
83+
# This rule adds an additional token replacement over the default replacements.
84+
unasync.Rule("/ahip/tests/", "/hip/tests/", replacements={"ahip", "hip"}),
85+
])},
86+
...
87+
)
88+
7389
7490
.. toctree::
7591
:maxdepth: 2

src/unasync/__init__.py

Lines changed: 103 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@
1212

1313
from ._version import __version__ # NOQA
1414

15-
ASYNC_TO_SYNC = {
15+
__all__ = [
16+
"Rule",
17+
"cmdclass_build_py",
18+
]
19+
20+
21+
_ASYNC_TO_SYNC = {
1622
"__aenter__": "__enter__",
1723
"__aexit__": "__exit__",
1824
"__aiter__": "__iter__",
@@ -27,6 +33,81 @@
2733
"StopAsyncIteration": "StopIteration",
2834
}
2935

36+
37+
class Rule:
38+
"""A single set of rules for 'unasync'ing file(s)"""
39+
40+
def __init__(self, fromdir, todir, replacements=None):
41+
self.fromdir = fromdir.replace("/", os.sep)
42+
self.todir = todir.replace("/", os.sep)
43+
44+
# Add any additional user-defined token replacements to our list.
45+
self.token_replacements = _ASYNC_TO_SYNC.copy()
46+
for key, val in (replacements or {}).items():
47+
self.token_replacements[key] = val
48+
49+
def match(self, filepath):
50+
"""Determines if a Rule matches a given filepath and if so
51+
returns a higher comparable value if the match is more specific.
52+
"""
53+
file_segments = [x for x in filepath.split(os.sep) if x]
54+
from_segments = [x for x in self.fromdir.split(os.sep) if x]
55+
len_from_segments = len(from_segments)
56+
57+
if len_from_segments > len(file_segments):
58+
return False
59+
60+
for i in range(len(file_segments) - len_from_segments + 1):
61+
if file_segments[i : i + len_from_segments] == from_segments:
62+
return len_from_segments, i
63+
64+
return False
65+
66+
def unasync_file(self, filepath):
67+
with open(filepath, "rb") as f:
68+
write_kwargs = {}
69+
if sys.version_info[0] >= 3:
70+
encoding, _ = std_tokenize.detect_encoding(f.readline)
71+
write_kwargs["encoding"] = encoding
72+
f.seek(0)
73+
tokens = tokenize(f)
74+
tokens = self.unasync_tokens(tokens)
75+
result = untokenize(tokens)
76+
outfilepath = filepath.replace(self.fromdir, self.todir)
77+
makedirs_existok(os.path.dirname(outfilepath))
78+
with open(outfilepath, "w", **write_kwargs) as f:
79+
print(result, file=f, end="")
80+
81+
def unasync_tokens(self, tokens):
82+
# TODO __await__, ...?
83+
used_space = None
84+
for space, toknum, tokval in tokens:
85+
if tokval in ["async", "await"]:
86+
# When removing async or await, we want to use the whitespace that
87+
# was before async/await before the next token so that
88+
# `print(await stuff)` becomes `print(stuff)` and not
89+
# `print( stuff)`
90+
used_space = space
91+
else:
92+
if toknum == std_tokenize.NAME:
93+
tokval = self.unasync_name(tokval)
94+
elif toknum == std_tokenize.STRING:
95+
left_quote, name, right_quote = tokval[0], tokval[1:-1], tokval[-1]
96+
tokval = left_quote + self.unasync_name(name) + right_quote
97+
if used_space is None:
98+
used_space = space
99+
yield (used_space, tokval)
100+
used_space = None
101+
102+
def unasync_name(self, name):
103+
if name in self.token_replacements:
104+
return self.token_replacements[name]
105+
# Convert classes prefixed with 'Async' into 'Sync'
106+
elif len(name) > 5 and name.startswith("Async") and name[5].isupper():
107+
return "Sync" + name[5:]
108+
return name
109+
110+
30111
Token = collections.namedtuple("Token", ["type", "string", "start", "end", "line"])
31112

32113

@@ -60,37 +141,6 @@ def tokenize(f):
60141
last_end = (tok.end[0] + 1, 0)
61142

62143

63-
def unasync_name(name):
64-
if name in ASYNC_TO_SYNC:
65-
return ASYNC_TO_SYNC[name]
66-
# Convert classes prefixed with 'Async' into 'Sync'
67-
elif len(name) > 5 and name.startswith("Async") and name[5].isupper():
68-
return "Sync" + name[5:]
69-
return name
70-
71-
72-
def unasync_tokens(tokens):
73-
# TODO __await__, ...?
74-
used_space = None
75-
for space, toknum, tokval in tokens:
76-
if tokval in ["async", "await"]:
77-
# When removing async or await, we want to use the whitespace that
78-
# was before async/await before the next token so that
79-
# `print(await stuff)` becomes `print(stuff)` and not
80-
# `print( stuff)`
81-
used_space = space
82-
else:
83-
if toknum == std_tokenize.NAME:
84-
tokval = unasync_name(tokval)
85-
elif toknum == std_tokenize.STRING:
86-
left_quote, name, right_quote = tokval[0], tokval[1:-1], tokval[-1]
87-
tokval = left_quote + unasync_name(name) + right_quote
88-
if used_space is None:
89-
used_space = space
90-
yield (used_space, tokval)
91-
used_space = None
92-
93-
94144
def untokenize(tokens):
95145
return "".join(space + tokval for space, tokval in tokens)
96146

@@ -103,34 +153,21 @@ def makedirs_existok(dir):
103153
raise
104154

105155

106-
def unasync_file(filepath, fromdir, todir):
107-
with open(filepath, "rb") as f:
108-
write_kwargs = {}
109-
if sys.version_info[0] >= 3:
110-
encoding, _ = std_tokenize.detect_encoding(f.readline)
111-
write_kwargs["encoding"] = encoding
112-
f.seek(0)
113-
tokens = tokenize(f)
114-
tokens = unasync_tokens(tokens)
115-
result = untokenize(tokens)
116-
outfilepath = filepath.replace(fromdir, todir)
117-
makedirs_existok(os.path.dirname(outfilepath))
118-
with open(outfilepath, "w", **write_kwargs) as f:
119-
print(result, file=f, end="")
156+
_DEFAULT_RULE = Rule(fromdir="/_async/", todir="/_sync/")
120157

121158

122-
class build_py(orig.build_py):
159+
class _build_py(orig.build_py):
123160
"""
124161
Subclass build_py from setuptools to modify its behavior.
125162
126163
Convert files in _async dir from being asynchronous to synchronous
127164
and saves them in _sync dir.
128165
"""
129166

130-
RENAME_DIR_FROM_TO = ("_async", "_sync") # Controls what directory will be renamed.
167+
UNASYNC_RULES = (_DEFAULT_RULE,)
131168

132169
def run(self):
133-
dir_from, dir_to = self.RENAME_DIR_FROM_TO
170+
rules = self.UNASYNC_RULES
134171

135172
self._updated_files = []
136173

@@ -143,8 +180,17 @@ def run(self):
143180

144181
# Our modification!
145182
for f in self._updated_files:
146-
if os.sep + dir_from + os.sep in f:
147-
unasync_file(f, dir_from, dir_to)
183+
found_rule = None
184+
found_weight = None
185+
186+
for rule in rules:
187+
weight = rule.match(f)
188+
if weight and (found_weight is None or weight > found_weight):
189+
found_rule = rule
190+
found_weight = weight
191+
192+
if found_rule:
193+
found_rule.unasync_file(f)
148194

149195
# Remaining base class code
150196
self.byte_compile(self.get_outputs(include_bytecode=0))
@@ -156,8 +202,10 @@ def build_module(self, module, module_file, package):
156202
return outfile, copied
157203

158204

159-
def customize_build_py(rename_dir_from_to=("_async", "_sync")):
160-
class _build_py(build_py):
161-
RENAME_DIR_FROM_TO = rename_dir_from_to
205+
def cmdclass_build_py(rules=(_DEFAULT_RULE,)):
206+
"""Creates a 'build_py' class for use within 'cmdclass={"build_py": ...}'"""
207+
208+
class _custom_build_py(_build_py):
209+
UNASYNC_RULES = rules
162210

163-
return _build_py
211+
return _custom_build_py
File renamed without changes.

tests/data/example_custom_pkg/setup.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,18 @@
99
author_email="[email protected]",
1010
description="A package used to test customized unasync",
1111
url="https://github.com/pypa/sampleproject",
12-
packages=["ahip", "ahip.some_dir"],
12+
packages=["ahip", "ahip.some_dir", "ahip.tests"],
1313
cmdclass={
14-
"build_py": unasync.customize_build_py(rename_dir_from_to=("ahip", "hip"))
14+
"build_py": unasync.cmdclass_build_py(
15+
rules=[
16+
unasync.Rule(fromdir="/ahip/", todir="/hip/"),
17+
unasync.Rule(
18+
fromdir="/ahip/tests/",
19+
todir="/hip/tests/",
20+
replacements={"ahip": "hip"},
21+
),
22+
]
23+
)
1524
},
1625
package_dir={"": "src"},
1726
)

tests/data/example_custom_pkg/src/ahip/tests/__init__.py

Whitespace-only changes.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import ahip
2+
3+
4+
async def test_connection():
5+
async def f():
6+
return None
7+
8+
x = await f()

tests/data/example_mod/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@
1010
description="A package used to test unasync",
1111
url="https://github.com/pypa/sampleproject",
1212
py_modules=["_async.some_file"],
13-
cmdclass={"build_py": unasync.build_py},
13+
cmdclass={"build_py": unasync.cmdclass_build_py()},
1414
)

tests/data/example_pkg/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@
1010
description="A package used to test unasync",
1111
url="https://github.com/pypa/sampleproject",
1212
packages=["example_pkg", "example_pkg._async", "example_pkg._async.some_dir"],
13-
cmdclass={"build_py": unasync.build_py},
13+
cmdclass={"build_py": unasync.cmdclass_build_py()},
1414
package_dir={"": "src"},
1515
)

tests/test_unasync.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,16 @@ def list_files(startpath):
2828
return output
2929

3030

31+
def test_rule_on_short_path():
32+
rule = unasync.Rule("/ahip/tests/", "/hip/tests/")
33+
assert rule.match("/ahip/") is False
34+
35+
3136
@pytest.mark.parametrize("source_file", TEST_FILES)
3237
def test_unasync(tmpdir, source_file):
3338

34-
unasync.unasync_file(
35-
os.path.join(ASYNC_DIR, source_file), fromdir=ASYNC_DIR, todir=str(tmpdir)
36-
)
39+
rule = unasync.Rule(fromdir=ASYNC_DIR, todir=str(tmpdir))
40+
rule.unasync_file(os.path.join(ASYNC_DIR, source_file))
3741

3842
encoding = "latin-1" if "encoding" in source_file else "utf-8"
3943
with io.open(os.path.join(SYNC_DIR, source_file), encoding=encoding) as f:
@@ -109,6 +113,10 @@ def test_project_structure_after_customized_build_py_packages(tmpdir):
109113
subprocess.check_call(["python", "setup.py", "build"], cwd=pkg_dir, env=env)
110114

111115
_async_dir_tree = list_files(os.path.join(source_pkg_dir, "src/ahip/."))
112-
unasynced_dir_tree = list_files(os.path.join(pkg_dir, "build/lib/hip/."))
116+
unasynced_dir_path = os.path.join(pkg_dir, "build/lib/hip/.")
117+
unasynced_dir_tree = list_files(unasynced_dir_path)
113118

114119
assert _async_dir_tree == unasynced_dir_tree
120+
121+
with open(os.path.join(unasynced_dir_path, "tests/test_conn.py")) as f:
122+
assert "import hip\n" in f.read()

0 commit comments

Comments
 (0)