Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDK-3862] Add sync support using unasync #537

Merged
merged 52 commits into from
Oct 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
5f1c1fa
Added tokenize_rt as a dev-dependency
sacOO7 Oct 4, 2023
ef8c7a7
Created unasync file to convert async code to sync code
sacOO7 Oct 4, 2023
86f5530
Added counter based tokenization strategy for replacing tokens
sacOO7 Oct 4, 2023
9626ef9
Added code to replace imports using tokenizer
sacOO7 Oct 4, 2023
1b5b4e0
Updated code for replacing imports
sacOO7 Oct 4, 2023
9af0ffc
Refactored unasync file for fixing imports
sacOO7 Oct 5, 2023
30bf9c3
Added unasync_test file for generating tests
sacOO7 Oct 5, 2023
9cad493
Updated unasync test file for generating rest only tests
sacOO7 Oct 5, 2023
d952ab0
Refactored unasync file, removed unnecessary build module code
sacOO7 Oct 5, 2023
d83597f
Refactored unasync_test, removed unnecessary module code
sacOO7 Oct 5, 2023
e80c1e6
Fixed flake8 issues for unasync_test file
sacOO7 Oct 5, 2023
67434a5
Added IDE specific files to gitignore
sacOO7 Oct 5, 2023
f2f89cc
Created sync directory to maintain generated sync code
sacOO7 Oct 5, 2023
c9f2386
Created sync directory under test to maintain ably rest test code
sacOO7 Oct 5, 2023
b4fa9f4
Added auto indentation code to unasync file
sacOO7 Oct 5, 2023
3a51a53
Fixed indentation based on new formula
sacOO7 Oct 5, 2023
b89af95
Merged unasync_test into unasync, refactored code
sacOO7 Oct 5, 2023
7d24da3
Executed updated unasync test for indentation
sacOO7 Oct 5, 2023
65b7936
Fixed indentation issues with generated test files
sacOO7 Oct 5, 2023
22836bc
Fixed indentation issues for restcapability
sacOO7 Oct 5, 2023
68c3048
Reformatted unasync file, removed unasync_test file
sacOO7 Oct 5, 2023
b7a95b8
Fixed test names warnings as per flake8
sacOO7 Oct 5, 2023
b37c5aa
prefixed tests with different name to avoid pytest run issues
sacOO7 Oct 5, 2023
88013a9
Fixed flake8 issues for unasync file
sacOO7 Oct 5, 2023
d91c171
Added missing string replacements to unasync generator
sacOO7 Oct 5, 2023
d9bbe93
Added more generic way to find submodules directory
sacOO7 Oct 5, 2023
8fe44b5
Refactored unasync, added more string replacements to fix tests
sacOO7 Oct 5, 2023
57f1c28
Regenerated sync tests
sacOO7 Oct 5, 2023
ee6bc64
Fixed linting issue for restchannelpublish
sacOO7 Oct 5, 2023
6160ded
Refactored unasync code, removed unnecessary garbage
sacOO7 Oct 5, 2023
4a83273
Refactored unasync.py, added feature to rename classes, updated tests…
sacOO7 Oct 6, 2023
732d048
Updated resttoken test to support assertion in sync tests
sacOO7 Oct 6, 2023
4c468a8
Renamed relevant public classes for sync support
sacOO7 Oct 6, 2023
d8d7b36
Fixed indentation issues caused by class rename in unasync file
sacOO7 Oct 6, 2023
68e4e1b
Generated sync files to resolve indentation issues
sacOO7 Oct 6, 2023
a340163
Fixed indentation issues as per flake8
sacOO7 Oct 6, 2023
0105b2b
Added extra step to generate sync rest code and tests to github workflow
sacOO7 Oct 6, 2023
5da20b1
Removed all generated sync files
sacOO7 Oct 6, 2023
01aefca
uncommented restcrypto test file
sacOO7 Oct 6, 2023
599cc2a
Removed uncessary type signature from unasync generator
sacOO7 Oct 6, 2023
16e86d9
Fixed crypto test for robust submodules path
sacOO7 Oct 6, 2023
2730648
updated readme for new sync api
sacOO7 Oct 6, 2023
8218a70
Apply suggestions from code review
sacOO7 Oct 9, 2023
3c057da
Added idea and ably sync packages to gitignore file
sacOO7 Oct 9, 2023
ba6f952
Refactored classes to be renamed in the list of rename_classes
sacOO7 Oct 9, 2023
a4e5105
Moved unasync script under scripts directory, updated pyproject.toml
sacOO7 Oct 9, 2023
7a84a89
Updated updating.md markdown file
sacOO7 Oct 9, 2023
5cfb920
Fixed indentation issues for unasync file
sacOO7 Oct 9, 2023
984ad07
refactor(unasync): move static class names to top of file
owenpearson Oct 9, 2023
4075079
docs: update migration guide sync api notice
owenpearson Oct 9, 2023
f17d3a6
docs: add sync api notice to README
owenpearson Oct 9, 2023
b6b463b
build: include generated files in published package
owenpearson Oct 9, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,7 @@ jobs:
run: poetry install -E crypto
- name: Lint with flake8
run: poetry run flake8
- name: Generate rest sync code and tests
run: poetry run unasync
- name: Test with pytest
run: poetry run pytest
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ app_spec
app_spec.pkl
ably/types/options.py.orig
test/ably/restsetup.py.orig

.idea/**/*
**/ably/sync/***
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ introduced by version 1.2.0.

### Using the Rest API

> [!NOTE]
> Please note that since version 2.0.2 we also provide a synchronous variant of the REST interface which is can be accessed as `from ably.sync import AblyRestSync`.

All examples assume a client and/or channel has been created in one of the following ways:

With closing the client manually:
Expand Down
16 changes: 16 additions & 0 deletions UPDATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ These include:

- Deprecation of support for Python versions 3.4, 3.5 and 3.6
- New, asynchronous API
- Deprecated synchronous API

### Deprecation of Python 3.4, 3.5 and 3.6

Expand All @@ -85,6 +86,21 @@ To see which versions of Python we test the SDK against, please look at our
The 1.2.0 version introduces a breaking change, which changes the way of interacting with the SDK from synchronous to asynchronous, using [the `asyncio` foundational library](https://docs.python.org/3.7/library/asyncio.html) to provide support for `async`/`await` syntax.
Because of this breaking change, every call that interacts with the Ably REST API must be refactored to this asynchronous way.

For backwards compatibility, in ably-python 2.0.2 we have added a backwards compatible REST client so that you can still use the synchronous version of the REST interface if you are migrating forwards from version 1.1.
In order to use the synchronous variant, you can import the `AblyRestSync` constructor from `ably.sync`:

```python
from ably.sync import AblyRestSync
owenpearson marked this conversation as resolved.
Show resolved Hide resolved

def main():
ably = AblyRestSync('api:key')
channel = ably.channels.get("channel_name")
channel.publish('event', 'message')

if __name__ == "__main__":
main()
```

#### Publishing Messages

This old style, synchronous example:
Expand Down
Empty file added ably/scripts/__init__.py
Empty file.
293 changes: 293 additions & 0 deletions ably/scripts/unasync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
import glob
import os
import tokenize as std_tokenize

import tokenize_rt

rename_classes = [
"AblyRest",
"Push",
"PushAdmin",
"Channel",
"Channels",
"Auth",
"Http",
"PaginatedResult",
"HttpPaginatedResponse"
]

_TOKEN_REPLACE = {
"__aenter__": "__enter__",
"__aexit__": "__exit__",
"__aiter__": "__iter__",
"__anext__": "__next__",
"asynccontextmanager": "contextmanager",
"AsyncIterable": "Iterable",
"AsyncIterator": "Iterator",
"AsyncGenerator": "Generator",
"StopAsyncIteration": "StopIteration",
}

_IMPORTS_REPLACE = {
}

_STRING_REPLACE = {
}

_CLASS_RENAME = {
}


class Rule:
"""A single set of rules for 'unasync'ing file(s)"""

def __init__(self, fromdir, todir, output_file_prefix="", additional_replacements=None):
self.fromdir = fromdir.replace("/", os.sep)
self.todir = todir.replace("/", os.sep)
self.ouput_file_prefix = output_file_prefix

# Add any additional user-defined token replacements to our list.
self.token_replacements = _TOKEN_REPLACE.copy()
for key, val in (additional_replacements or {}).items():
self.token_replacements[key] = val

def _match(self, filepath):
"""Determines if a Rule matches a given filepath and if so
returns a higher comparable value if the match is more specific.
"""
file_segments = [x for x in filepath.split(os.sep) if x]
from_segments = [x for x in self.fromdir.split(os.sep) if x]
len_from_segments = len(from_segments)

if len_from_segments > len(file_segments):
return False

for i in range(len(file_segments) - len_from_segments + 1):
if file_segments[i: i + len_from_segments] == from_segments:
return len_from_segments, i

return False

def _unasync_file(self, filepath):
with open(filepath, "rb") as f:
encoding, _ = std_tokenize.detect_encoding(f.readline)

with open(filepath, "rt", encoding=encoding) as f:
tokens = tokenize_rt.src_to_tokens(f.read())
tokens = self._unasync_tokens(tokens)
result = tokenize_rt.tokens_to_src(tokens)
new_file_path = os.path.join(os.path.dirname(filepath),
self.ouput_file_prefix + os.path.basename(filepath))
outfilepath = new_file_path.replace(self.fromdir, self.todir)
os.makedirs(os.path.dirname(outfilepath), exist_ok=True)
with open(outfilepath, "wb") as f:
f.write(result.encode(encoding))

def _unasync_tokens(self, tokens: list):
new_tokens = []
token_counter = 0
async_await_block_started = False
async_await_char_diff = -6 # (len("async") or len("await") is 6)
async_await_offset = 0

renamed_class_call_started = False
renamed_class_char_diff = 0
renamed_class_offset = 0

while token_counter < len(tokens):
token = tokens[token_counter]

if async_await_block_started or renamed_class_call_started:
# Fix indentation issues for async/await fn definition/call
if token.src == '\n':
new_tokens.append(token)
token_counter = token_counter + 1
next_newline_token = tokens[token_counter]
new_tab_src = next_newline_token.src

if (renamed_class_call_started and
tokens[token_counter + 1].utf8_byte_offset >= renamed_class_offset):
if renamed_class_char_diff < 0:
new_tab_src = new_tab_src[:renamed_class_char_diff]
else:
new_tab_src = new_tab_src + renamed_class_char_diff * " "

if (async_await_block_started and len(next_newline_token.src) >= 6 and
tokens[token_counter + 1].utf8_byte_offset >= async_await_offset + 6):
new_tab_src = new_tab_src[:async_await_char_diff] # remove last 6 white spaces

next_newline_token = next_newline_token._replace(src=new_tab_src)
new_tokens.append(next_newline_token)
token_counter = token_counter + 1
continue

if token.src == ')':
async_await_block_started = False
async_await_offset = 0
renamed_class_call_started = False
renamed_class_char_diff = 0

if token.src in ["async", "await"]:
# When removing async or await, we want to skip the following whitespace
token_counter = token_counter + 2
is_async_start = tokens[token_counter].src == 'def'
is_await_start = False
for i in range(token_counter, token_counter + 6):
if tokens[i].src == '(':
is_await_start = True
break
if is_async_start or is_await_start:
# Fix indentation issues for async/await fn definition/call
async_await_offset = token.utf8_byte_offset
async_await_block_started = True
continue

elif token.name == "NAME":
if token.src == "from":
if tokens[token_counter + 1].src == " ":
token_counter = self._replace_import(tokens, token_counter, new_tokens)
continue
else:
token_new_src = self._unasync_name(token.src)
if token.src == token_new_src:
token_new_src = self._class_rename(token.src)
if token.src != token_new_src:
renamed_class_offset = token.utf8_byte_offset
renamed_class_char_diff = len(token_new_src) - len(token.src)
for i in range(token_counter, token_counter + 6):
if tokens[i].src == '(':
renamed_class_call_started = True
break

token = token._replace(src=token_new_src)
elif token.name == "STRING":
src_token = token.src.replace("'", "")
if _STRING_REPLACE.get(src_token) is not None:
new_token = f"'{_STRING_REPLACE[src_token]}'"
token = token._replace(src=new_token)
else:
src_token = token.src.replace("\"", "")
if _STRING_REPLACE.get(src_token) is not None:
new_token = f"\"{_STRING_REPLACE[src_token]}\""
token = token._replace(src=new_token)

new_tokens.append(token)
token_counter = token_counter + 1

return new_tokens

def _replace_import(self, tokens, token_counter, new_tokens: list):
new_tokens.append(tokens[token_counter])
new_tokens.append(tokens[token_counter + 1])

full_lib_name = ''
lib_name_counter = token_counter + 2
if len(_IMPORTS_REPLACE.keys()) == 0:
return lib_name_counter

while True:
if tokens[lib_name_counter].src == " ":
break
full_lib_name = full_lib_name + tokens[lib_name_counter].src
lib_name_counter = lib_name_counter + 1

for key, value in _IMPORTS_REPLACE.items():
if key in full_lib_name:
updated_lib_name = full_lib_name.replace(key, value)
for lib_name_part in updated_lib_name.split("."):
lib_name_part = self._class_rename(lib_name_part)
new_tokens.append(tokenize_rt.Token("NAME", lib_name_part))
new_tokens.append(tokenize_rt.Token("OP", "."))
new_tokens.pop()
return lib_name_counter

lib_name_counter = token_counter + 2
return lib_name_counter

def _class_rename(self, name):
if name in _CLASS_RENAME:
return _CLASS_RENAME[name]
return name

def _unasync_name(self, name):
if name in self.token_replacements:
return self.token_replacements[name]
return name


def unasync_files(fpath_list, rules):
for f in fpath_list:
found_rule = None
found_weight = None

for rule in rules:
weight = rule._match(f)
if weight and (found_weight is None or weight > found_weight):
found_rule = rule
found_weight = weight

if found_rule:
found_rule._unasync_file(f)


def find_files(dir_path, file_name_regex):
return glob.glob(os.path.join(dir_path, "**", file_name_regex), recursive=True)


def run():
# Source files ==========================================

_TOKEN_REPLACE["AsyncClient"] = "Client"
_TOKEN_REPLACE["aclose"] = "close"

_IMPORTS_REPLACE["ably"] = "ably.sync"

# here...
for class_name in rename_classes:
_CLASS_RENAME[class_name] = f"{class_name}Sync"

_STRING_REPLACE["Auth"] = "AuthSync"

src_dir_path = os.path.join(os.getcwd(), "ably")
dest_dir_path = os.path.join(os.getcwd(), "ably", "sync")

relevant_src_files = (set(find_files(src_dir_path, "*.py")) -
set(find_files(dest_dir_path, "*.py")))

unasync_files(list(relevant_src_files), [Rule(fromdir=src_dir_path, todir=dest_dir_path)])

# Test files ==============================================

_TOKEN_REPLACE["asyncSetUp"] = "setUp"
_TOKEN_REPLACE["asyncTearDown"] = "tearDown"
_TOKEN_REPLACE["AsyncMock"] = "Mock"

_TOKEN_REPLACE["_Channel__publish_request_body"] = "_ChannelSync__publish_request_body"
_TOKEN_REPLACE["_Http__client"] = "_HttpSync__client"

_IMPORTS_REPLACE["test.ably"] = "test.ably.sync"

_STRING_REPLACE['/../assets/testAppSpec.json'] = '/../../assets/testAppSpec.json'
_STRING_REPLACE['ably.rest.auth.Auth.request_token'] = 'ably.sync.rest.auth.AuthSync.request_token'
_STRING_REPLACE['ably.rest.auth.TokenRequest'] = 'ably.sync.rest.auth.TokenRequest'
_STRING_REPLACE['ably.rest.rest.Http.post'] = 'ably.sync.rest.rest.HttpSync.post'
_STRING_REPLACE['httpx.AsyncClient.send'] = 'httpx.Client.send'
_STRING_REPLACE['ably.util.exceptions.AblyException.raise_for_response'] = \
'ably.sync.util.exceptions.AblyException.raise_for_response'
_STRING_REPLACE['ably.rest.rest.AblyRest.time'] = 'ably.sync.rest.rest.AblyRestSync.time'
_STRING_REPLACE['ably.rest.auth.Auth._timestamp'] = 'ably.sync.rest.auth.AuthSync._timestamp'

# round 1
src_dir_path = os.path.join(os.getcwd(), "test", "ably")
dest_dir_path = os.path.join(os.getcwd(), "test", "ably", "sync")
src_files = [os.path.join(os.getcwd(), "test", "ably", "testapp.py"),
os.path.join(os.getcwd(), "test", "ably", "utils.py")]

unasync_files(src_files, [Rule(fromdir=src_dir_path, todir=dest_dir_path)])

# round 2
src_dir_path = os.path.join(os.getcwd(), "test", "ably", "rest")
dest_dir_path = os.path.join(os.getcwd(), "test", "ably", "sync", "rest")
src_files = find_files(src_dir_path, "*.py")

unasync_files(src_files, [Rule(fromdir=src_dir_path, todir=dest_dir_path, output_file_prefix="sync_")])
Loading
Loading