Skip to content

Commit 9380f94

Browse files
committed
Refactor codebase to asyncio
Use asyncio to perform concurrent http requests or file operations wherever possible to reduce latency of batched operations. Replaces all threaded operations.
1 parent d0695d9 commit 9380f94

35 files changed

Lines changed: 610 additions & 694 deletions

package_control/automatic_upgrader.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .activity_indicator import ActivityIndicator
1212
from .console_write import console_write
1313
from .package_tasks import PackageTaskRunner
14+
from .vendor import anyio
1415

1516

1617
class AutomaticUpgrader:
@@ -28,8 +29,8 @@ def __init__(self, manager):
2829
self.next_run = 0
2930
self.current_version = int(sublime.version())
3031

31-
def run(self):
32-
self.load_last_run()
32+
async def run(self):
33+
await self.load_last_run()
3334

3435
if self.last_version != self.current_version and self.last_version != 0:
3536
console_write(
@@ -50,16 +51,16 @@ def run(self):
5051
)
5152
return
5253

53-
self.upgrade_packages()
54+
await self.upgrade_packages()
5455

55-
def load_last_run(self):
56+
async def load_last_run(self):
5657
"""
5758
Loads the last run time from disk into memory
5859
"""
5960

6061
try:
61-
with open(os.path.join(sys_path.pc_cache_dir(), 'last_run.json')) as fobj:
62-
last_run_data = json.load(fobj)
62+
async with await anyio.open_file(os.path.join(sys_path.pc_cache_dir(), 'last_run.json')) as fobj:
63+
last_run_data = json.loads(await fobj.read())
6364
self.last_run = int(last_run_data['timestamp'])
6465
self.last_version = int(last_run_data['st_version'])
6566
except (FileNotFoundError, ValueError, TypeError):
@@ -69,18 +70,18 @@ def load_last_run(self):
6970
if frequency and self.last_run:
7071
self.next_run = int(self.last_run) + (frequency * 60 * 60)
7172

72-
def save_last_run(self):
73+
async def save_last_run(self):
7374
"""
7475
Saves a record of when the last run was
7576
"""
7677

77-
with open(os.path.join(sys_path.pc_cache_dir(), 'last_run.json'), 'w') as fobj:
78-
json.dump({
78+
async with await anyio.open_file(os.path.join(sys_path.pc_cache_dir(), 'last_run.json'), 'w') as fobj:
79+
await fobj.write(json.dumps({
7980
'timestamp': int(time.time()),
8081
'st_version': self.current_version
81-
}, fp=fobj)
82+
}))
8283

83-
def upgrade_packages(self):
84+
async def upgrade_packages(self):
8485
"""
8586
Upgrades all packages that are not currently upgraded to the latest
8687
version. Also renames any installed packages to their new names.
@@ -92,17 +93,17 @@ def upgrade_packages(self):
9293
# upgrade existing libraries
9394
required_libraries = upgrader.manager.find_required_libraries()
9495
missing_libraries = upgrader.manager.find_missing_libraries(required_libraries=required_libraries)
95-
upgrader.manager.install_libraries(
96+
await upgrader.manager.install_libraries(
9697
libraries=required_libraries - missing_libraries,
9798
fail_early=False
9899
)
99100

100101
# run updater synchronously to delay any "You must restart ST" dialogues
101102
# Note: we are in PackageCleanup thread here
102-
completed = upgrader.upgrade_packages(
103+
completed = await upgrader.upgrade_packages(
103104
ignore_packages=upgrader.manager.settings.get('auto_upgrade_ignore'),
104105
unattended=True,
105106
progress=progress
106107
)
107108
if completed:
108-
self.save_last_run()
109+
await self.save_last_run()

package_control/bootstrap.py

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import asyncio
12
import json
23
import os
34
import zipfile
45
from textwrap import dedent
5-
from threading import Thread
66

77
import sublime
8+
import sublime_aio
89

910
from . import library, sys_path
1011
from .clear_directory import delete_directory
@@ -37,7 +38,7 @@ def disable_package_control():
3738
)
3839

3940

40-
def bootstrap():
41+
async def bootstrap():
4142
"""
4243
Bootstrap Package Control
4344
@@ -51,19 +52,17 @@ def bootstrap():
5152
if not os.path.exists(LOADER_PACKAGE_PATH):
5253
# Start shortly after Sublime starts so package renames don't cause errors
5354
# with key bindings, settings, etc. disappearing in the middle of parsing
54-
sublime.set_timeout(PackageCleanup().start, 2000)
55+
await asyncio.sleep(2)
56+
await PackageCleanup().run()
5557
return
5658

57-
sublime.set_timeout(_bootstrap, 10)
58-
59-
60-
def _bootstrap():
6159
PackageDisabler.disable_packages({PackageDisabler.LOADER: LOADER_PACKAGE_NAME})
6260
# Give ST a second to disable 0_package_control_loader
63-
sublime.set_timeout(Thread(target=_migrate_dependencies).start, 1000)
61+
await asyncio.sleep(1)
62+
await _migrate_dependencies()
6463

6564

66-
def _migrate_dependencies():
65+
async def _migrate_dependencies():
6766
"""
6867
Moves old Package Control 3-style dependencies to the new 4-style
6968
libraries, which use the Lib folder
@@ -110,17 +109,16 @@ def _migrate_dependencies():
110109

111110
os.remove(LOADER_PACKAGE_PATH)
112111

113-
def _reenable_loader():
114-
PackageDisabler.reenable_packages({PackageDisabler.LOADER: LOADER_PACKAGE_NAME})
115-
show_message(
116-
'''
117-
Dependencies have just been migrated to python libraries.
112+
await asyncio.sleep(500)
118113

119-
You may need to restart Sublime Text.
120-
'''
121-
)
114+
PackageDisabler.reenable_packages({PackageDisabler.LOADER: LOADER_PACKAGE_NAME})
115+
show_message(
116+
'''
117+
Dependencies have just been migrated to python libraries.
122118
123-
sublime.set_timeout(_reenable_loader, 500)
119+
You may need to restart Sublime Text.
120+
'''
121+
)
124122

125123
except (OSError) as e:
126124
console_write('Error trying to migrate dependencies - %s' % e)

package_control/clients/bitbucket_client.py

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def repo_url(user_name, repo_name):
6969

7070
return 'https://bitbucket.com/%s/%s' % (quote(user_name), quote(repo_name))
7171

72-
def download_info(self, url, tag_prefix=None):
72+
async def download_info(self, url, tag_prefix=None):
7373
"""
7474
Retrieve information about downloading a package
7575
@@ -97,12 +97,12 @@ def download_info(self, url, tag_prefix=None):
9797
`date` - the ISO-8601 timestamp string when the version was published
9898
"""
9999

100-
output = self.download_info_from_branch(url)
100+
output = await self.download_info_from_branch(url)
101101
if output is None:
102-
output = self.download_info_from_tags(url, tag_prefix)
102+
output = await self.download_info_from_tags(url, tag_prefix)
103103
return output
104104

105-
def download_info_from_branch(self, url, default_branch=None):
105+
async def download_info_from_branch(self, url, default_branch=None):
106106
"""
107107
Retrieve information about downloading a package
108108
@@ -135,18 +135,18 @@ def download_info_from_branch(self, url, default_branch=None):
135135
if branch is None:
136136
branch = default_branch
137137
if branch is None:
138-
repo_info = self.fetch_json(self._api_url(user_repo))
138+
repo_info = await self.fetch_json(self._api_url(user_repo))
139139
branch = repo_info['mainbranch'].get('name', 'master')
140140

141141
branch_url = self._api_url(user_repo, '/refs/branches/%s' % branch)
142-
branch_info = self.fetch_json(branch_url)
142+
branch_info = await self.fetch_json(branch_url)
143143

144144
timestamp = branch_info['target']['date'][0:19].replace('T', ' ')
145145
version = re.sub(r'[\-: ]', '.', timestamp)
146146

147147
return [self._make_download_info(user_repo, branch, version, timestamp)]
148148

149-
def download_info_from_releases(self, url, asset_templates, tag_prefix=None):
149+
async def download_info_from_releases(self, url, asset_templates, tag_prefix=None):
150150
"""
151151
BitBucket doesn't support releases in ways GitHub/Gitlab do.
152152
@@ -158,7 +158,7 @@ def download_info_from_releases(self, url, asset_templates, tag_prefix=None):
158158

159159
return None
160160

161-
def download_info_from_tags(self, url, tag_prefix=None):
161+
async def download_info_from_tags(self, url, tag_prefix=None):
162162
"""
163163
Retrieve information about downloading a package
164164
@@ -188,12 +188,12 @@ def download_info_from_tags(self, url, tag_prefix=None):
188188
if not tags_match:
189189
return None
190190

191-
def _get_releases(user_repo, tag_prefix, page_size=100):
191+
async def _get_releases(user_repo, tag_prefix, page_size=100):
192192
used_versions = set()
193193
query_string = urlencode({'pagelen': page_size})
194194
tags_url = self._api_url(user_repo, '/refs/tags?%s' % query_string)
195195
while tags_url:
196-
tags_json = self.fetch_json(tags_url)
196+
tags_json = await self.fetch_json(tags_url)
197197
for tag in tags_json['values']:
198198
version = version_match_prefix(tag['name'], tag_prefix)
199199
if version and version not in used_versions:
@@ -212,7 +212,7 @@ def _get_releases(user_repo, tag_prefix, page_size=100):
212212
num_releases = 0
213213

214214
output = []
215-
for release in sorted(_get_releases(user_repo, tag_prefix), reverse=True):
215+
async for release in _get_releases(user_repo, tag_prefix):
216216
version, tag, timestamp = release
217217

218218
output.append(self._make_download_info(user_repo, tag, str(version), timestamp))
@@ -221,9 +221,10 @@ def _get_releases(user_repo, tag_prefix, page_size=100):
221221
if max_releases > 0 and num_releases >= max_releases:
222222
break
223223

224+
output.sort(reverse=True)
224225
return output
225226

226-
def repo_info(self, url):
227+
async def repo_info(self, url):
227228
"""
228229
Retrieve general information about a repository
229230
@@ -254,7 +255,7 @@ def repo_info(self, url):
254255

255256
user_repo = "%s/%s" % (user_name, repo_name)
256257
api_url = self._api_url(user_repo)
257-
repo_info = self.fetch_json(api_url)
258+
repo_info = await self.fetch_json(api_url)
258259

259260
if branch is None:
260261
branch = repo_info['mainbranch'].get('name', 'master')
@@ -266,7 +267,7 @@ def repo_info(self, url):
266267
author = repo_info['owner'].get('username')
267268

268269
is_client = self.settings.get('min_api_calls', False)
269-
readme_url = None if is_client else self._readme_url(user_repo, branch)
270+
readme_url = None if is_client else await self._readme_url(user_repo, branch)
270271

271272
return {
272273
'name': repo_info['name'],
@@ -279,7 +280,7 @@ def repo_info(self, url):
279280
'default_branch': branch
280281
}
281282

282-
def user_info(self, url):
283+
async def user_info(self, url):
283284
"""
284285
For API compatibility with other clients.
285286
@@ -341,7 +342,7 @@ def _api_url(self, user_repo, suffix=''):
341342

342343
return 'https://api.bitbucket.org/2.0/repositories/%s%s' % (user_repo, suffix)
343344

344-
def _readme_url(self, user_repo, branch, prefer_cached=False):
345+
async def _readme_url(self, user_repo, branch, prefer_cached=False):
345346
"""
346347
Parse the root directory listing for the repo and return the URL
347348
to any file that looks like a readme
@@ -367,7 +368,7 @@ def _readme_url(self, user_repo, branch, prefer_cached=False):
367368

368369
try:
369370
while listing_url:
370-
root_dir_info = self.fetch_json(listing_url, prefer_cached)
371+
root_dir_info = await self.fetch_json(listing_url, prefer_cached)
371372

372373
for entry in root_dir_info['values']:
373374
if entry['path'].lower() in _readme_filenames:

0 commit comments

Comments
 (0)