diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 63feffc..244b224 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,8 +10,8 @@ jobs: name: Windows runs-on: windows-2019 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install dependencies @@ -24,7 +24,7 @@ jobs: - name: Build binary run: pyinstaller CurseBreaker.spec - name: Upload binary - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Windows path: dist/CurseBreaker.exe @@ -33,8 +33,8 @@ jobs: name: Linux runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install dependencies @@ -45,7 +45,7 @@ jobs: - name: Build binary run: pyinstaller CurseBreaker.spec - name: Upload binary - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: Linux path: dist/CurseBreaker @@ -54,8 +54,8 @@ jobs: name: macOS runs-on: macos-11 steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: python-version: '3.12' - name: Install dependencies @@ -66,7 +66,7 @@ jobs: - name: Build binary run: pyinstaller CurseBreaker.spec - name: Upload binary - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: macOS path: dist/CurseBreaker diff --git a/.sourcery.yaml b/.sourcery.yaml new file mode 100644 index 0000000..24c239b --- /dev/null +++ b/.sourcery.yaml @@ -0,0 +1,18 @@ +version: '1' + +ignore: +- CB\SLPP.py + +rule_settings: + enable: + - default + disable: + - list-comprehension + - dict-comprehension + - for-append-to-extend + - use-next + rule_types: + - refactoring + - suggestion + - comment + python_version: '3.12' \ No newline at end of file diff --git a/CB/Compat.py b/CB/Compat.py index 8c58937..2407dfb 100644 --- a/CB/Compat.py +++ b/CB/Compat.py @@ -26,13 +26,13 @@ def set_normal_term(self): if system != 'Windows': termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.old_term) - def getch(self): + def getch(self): # sourcery skip: assign-if-exp if system == 'Windows': return msvcrt.getch() else: return sys.stdin.read(1) - def kbhit(self): + def kbhit(self): # sourcery skip: remove-unnecessary-else if system == 'Windows': return msvcrt.kbhit() else: diff --git a/CB/Core.py b/CB/Core.py index 394de11..fdd8784 100644 --- a/CB/Core.py +++ b/CB/Core.py @@ -55,8 +55,8 @@ def init_config(self): with open(self.configPath, 'r') as f: try: self.config = json.load(f) - except (StopIteration, UnicodeDecodeError, json.JSONDecodeError): - raise RuntimeError + except (StopIteration, UnicodeDecodeError, json.JSONDecodeError) as e: + raise RuntimeError from e else: self.config = {'Addons': [], 'WAStash': [], @@ -83,69 +83,70 @@ def save_config(self): json.dump(self.config, outfile, sort_keys=True, indent=4, separators=(',', ': ')) def update_config(self): - if 'Version' not in self.config.keys() or self.config['Version'] != __version__: - urlupdate = {'elvui-classic': 'elvui', 'elvui-classic:dev': 'elvui:dev', 'tukui-classic': 'tukui', - 'sle:dev': 'shadow&light:dev', 'elvui:beta': 'elvui:dev'} - # 4.0.0 - if 'WACompanionVersion' in self.config and os.path.isdir(Path('Interface/AddOns/WeakAurasCompanion')): - shutil.rmtree(Path('Interface/AddOns/WeakAurasCompanion'), ignore_errors=True) - for addon in self.config['Addons']: - # 1.1.0 - if 'Checksums' not in addon.keys(): - checksums = {} - for directory in addon['Directories']: - checksums[directory] = dirhash(self.path / directory) - addon['Checksums'] = checksums - # 1.1.1 - if addon['Version'] is None: - addon['Version'] = '1' - # 2.2.0, 3.9.4, 3.12.0 - if addon['URL'].lower() in urlupdate: - addon['URL'] = urlupdate[addon['URL'].lower()] - # 2.4.0 - if addon['Name'] == 'TukUI': - addon['Name'] = 'Tukui' - addon['URL'] = 'Tukui' - # 2.7.3 - addon['Directories'] = list(filter(None, set(addon['Directories']))) - # 3.0.2 - if addon['URL'].endswith('/'): - addon['URL'] = addon['URL'][:-1] - # 3.3.0 - if 'Development' in addon.keys() and isinstance(addon['Development'], bool): - addon['Development'] = 1 - # 4.3.0 - if addon['URL'].startswith('https://www.tukui.org/classic-tbc-addons.php?id='): - addon['URL'] = addon['URL'].replace('https://www.tukui.org/classic-tbc-addons.php?id=', - 'https://www.tukui.org/classic-wotlk-addons.php?id=') - for add in [['2.1.0', 'WAUsername', ''], - ['2.2.0', 'WAAccountName', ''], - ['2.2.0', 'WAAPIKey', ''], - ['2.2.0', 'WACompanionVersion', 0], - ['2.8.0', 'IgnoreClientVersion', {}], - ['3.0.1', 'CFCacheTimestamp', 0], - ['3.1.10', 'CFCacheCloudFlare', {}], - ['3.7.0', 'CompactMode', False], - ['3.10.0', 'AutoUpdate', True], - ['3.12.0', 'ShowAuthors', True], - ['3.16.0', 'IgnoreDependencies', {}], - ['3.18.0', 'WAStash', []], - ['3.20.0', 'GHAPIKey', ''], - ['4.0.0', 'WAAAPIKey', ''], - ['4.0.0', 'CBCompanionVersion', 0], - ['4.2.0', 'ShowSources', False]]: - if add[1] not in self.config.keys(): - self.config[add[1]] = add[2] - for delete in [['1.3.0', 'URLCache'], - ['3.0.1', 'CurseCache'], - ['4.0.0', 'CFCacheCloudFlare'], - ['4.0.0', 'CFCacheTimestamp'], - ['4.0.0', 'IgnoreDependencies'], - ['4.0.0', 'WACompanionVersion']]: - if delete[1] in self.config.keys(): - self.config.pop(delete[1], None) - self.config['Version'] = __version__ - self.save_config() + if 'Version' in self.config.keys() and self.config['Version'] == __version__: + return + urlupdate = {'elvui-classic': 'elvui', 'elvui-classic:dev': 'elvui:dev', 'tukui-classic': 'tukui', + 'sle:dev': 'shadow&light:dev', 'elvui:beta': 'elvui:dev'} + # 4.0.0 + if 'WACompanionVersion' in self.config and os.path.isdir(Path('Interface/AddOns/WeakAurasCompanion')): + shutil.rmtree(Path('Interface/AddOns/WeakAurasCompanion'), ignore_errors=True) + for addon in self.config['Addons']: + # 1.1.0 + if 'Checksums' not in addon.keys(): + checksums = {} + for directory in addon['Directories']: + checksums[directory] = dirhash(self.path / directory) + addon['Checksums'] = checksums + # 1.1.1 + if addon['Version'] is None: + addon['Version'] = '1' + # 2.2.0, 3.9.4, 3.12.0 + if addon['URL'].lower() in urlupdate: + addon['URL'] = urlupdate[addon['URL'].lower()] + # 2.4.0 + if addon['Name'] == 'TukUI': + addon['Name'] = 'Tukui' + addon['URL'] = 'Tukui' + # 2.7.3 + addon['Directories'] = list(filter(None, set(addon['Directories']))) + # 3.0.2 + if addon['URL'].endswith('/'): + addon['URL'] = addon['URL'][:-1] + # 3.3.0 + if 'Development' in addon.keys() and isinstance(addon['Development'], bool): + addon['Development'] = 1 + # 4.3.0 + if addon['URL'].startswith('https://www.tukui.org/classic-tbc-addons.php?id='): + addon['URL'] = addon['URL'].replace('https://www.tukui.org/classic-tbc-addons.php?id=', + 'https://www.tukui.org/classic-wotlk-addons.php?id=') + for add in [['2.1.0', 'WAUsername', ''], + ['2.2.0', 'WAAccountName', ''], + ['2.2.0', 'WAAPIKey', ''], + ['2.2.0', 'WACompanionVersion', 0], + ['2.8.0', 'IgnoreClientVersion', {}], + ['3.0.1', 'CFCacheTimestamp', 0], + ['3.1.10', 'CFCacheCloudFlare', {}], + ['3.7.0', 'CompactMode', False], + ['3.10.0', 'AutoUpdate', True], + ['3.12.0', 'ShowAuthors', True], + ['3.16.0', 'IgnoreDependencies', {}], + ['3.18.0', 'WAStash', []], + ['3.20.0', 'GHAPIKey', ''], + ['4.0.0', 'WAAAPIKey', ''], + ['4.0.0', 'CBCompanionVersion', 0], + ['4.2.0', 'ShowSources', False]]: + if add[1] not in self.config.keys(): + self.config[add[1]] = add[2] + for delete in [['1.3.0', 'URLCache'], + ['3.0.1', 'CurseCache'], + ['4.0.0', 'CFCacheCloudFlare'], + ['4.0.0', 'CFCacheTimestamp'], + ['4.0.0', 'IgnoreDependencies'], + ['4.0.0', 'WACompanionVersion']]: + if delete[1] in self.config.keys(): + self.config.pop(delete[1], None) + self.config['Version'] = __version__ + self.save_config() def check_if_installed(self, url): for addon in self.config['Addons']: @@ -158,12 +159,8 @@ def check_if_installed_dirs(self, directories): return addon def check_if_dev(self, url): - addon = self.check_if_installed(url) - if addon: - if 'Development' in addon.keys(): - return addon['Development'] - else: - return 0 + if addon := self.check_if_installed(url): + return addon['Development'] if 'Development' in addon.keys() else 0 else: return 0 @@ -172,8 +169,7 @@ def check_if_overlap(self): found = set() for addon in self.config['Addons']: directories = directories + addon['Directories'] - dupes = [x for x in directories if x in found or found.add(x)] - if len(dupes) > 0: + if dupes := [x for x in directories if x in found or found.add(x)]: addons = [] for addon in self.config['Addons']: if set(addon['Directories']).intersection(dupes): @@ -184,20 +180,13 @@ def check_if_overlap(self): return False def check_if_blocked(self, addon): - if addon: - if 'Block' in addon.keys(): - return True - else: - return False - else: - return False + return bool(addon and 'Block' in addon.keys()) def check_if_dev_global(self): for addon in self.config['Addons']: if addon['URL'].startswith('https://addons.wago.io/addons/') and 'Development' in addon.keys(): return addon['Development'] - else: - return 0 + return 0 def cleanup(self, directories): if len(directories) > 0: @@ -247,6 +236,26 @@ def parse_url_source(self, url): else: return '?', None + def parse_new_addon(self, ignore, url): + if ignore: + self.config['IgnoreClientVersion'][url] = True + new = self.parse_url(url) + new.get_addon() + if addon := self.check_if_installed_dirs(new.directories): + return False, addon['Name'], addon['Version'] + self.cleanup(new.directories) + new.install(self.path) + checksums = {} + for directory in new.directories: + checksums[directory] = dirhash(self.path / directory) + self.config['Addons'].append({'Name': new.name, + 'URL': url, + 'Version': new.currentVersion, + 'Directories': new.directories, + 'Checksums': checksums}) + self.save_config() + return True, new.name, new.currentVersion + def add_addon(self, url, ignore): if url.endswith(':'): raise NotImplementedError('Provided URL is not supported.') @@ -260,33 +269,13 @@ def add_addon(self, url, ignore): url = f'https://github.com/{url[3:]}' if url.endswith('/'): url = url[:-1] - addon = self.check_if_installed(url) - if not addon: - if ignore: - self.config['IgnoreClientVersion'][url] = True - new = self.parse_url(url) - new.get_addon() - addon = self.check_if_installed_dirs(new.directories) - if addon: - return False, addon['Name'], addon['Version'] - self.cleanup(new.directories) - new.install(self.path) - checksums = {} - for directory in new.directories: - checksums[directory] = dirhash(self.path / directory) - self.config['Addons'].append({'Name': new.name, - 'URL': url, - 'Version': new.currentVersion, - 'Directories': new.directories, - 'Checksums': checksums - }) - self.save_config() - return True, new.name, new.currentVersion - return False, addon['Name'], addon['Version'] + if addon := self.check_if_installed(url): + return False, addon['Name'], addon['Version'] + else: + return self.parse_new_addon(ignore, url) def del_addon(self, url, keep): - old = self.check_if_installed(url) - if old: + if old := self.check_if_installed(url): if not keep: self.cleanup(old['Directories']) self.config['IgnoreClientVersion'].pop(old['URL'], None) @@ -296,41 +285,40 @@ def del_addon(self, url, keep): return old['Name'], old['Version'] return False, False - def update_addon(self, url, update, force): - old = self.check_if_installed(url) - if old: - dev = self.check_if_dev(old['URL']) - blocked = self.check_if_blocked(old) - oldversion = old['Version'] - if old['URL'] in self.checksumCache: - modified = self.checksumCache[old['URL']] - else: - modified = self.check_checksum(old, False) - if old['URL'].startswith(('https://www.townlong-yak.com/addons/', - 'https://www.curseforge.com/wow/addons/', - 'https://www.tukui.org/')): - return old['Name'], [], oldversion, oldversion, None, modified, blocked, 'Unsupported', old['URL'], \ + def update_addon(self, url, update, force): # sourcery skip: extract-method + if not (old := self.check_if_installed(url)): + return url, [], False, False, None, False, False, '?', None, None, None + dev = self.check_if_dev(old['URL']) + blocked = self.check_if_blocked(old) + oldversion = old['Version'] + if old['URL'] in self.checksumCache: + modified = self.checksumCache[old['URL']] + else: + modified = self.check_checksum(old, False) + if old['URL'].startswith(('https://www.townlong-yak.com/addons/', + 'https://www.curseforge.com/wow/addons/', + 'https://www.tukui.org/')): + return old['Name'], [], oldversion, oldversion, None, modified, blocked, 'Unsupported', old['URL'], \ None, dev - source, sourceurl = self.parse_url_source(old['URL']) - new = self.parse_url(old['URL']) - if force or (new.currentVersion != old['Version'] and update and not modified and not blocked): - new.get_addon() - self.cleanup(old['Directories']) - new.install(self.path) - checksums = {} - for directory in new.directories: - checksums[directory] = dirhash(self.path / directory) - old['Name'] = new.name - old['Version'] = new.currentVersion - old['Directories'] = new.directories - old['Checksums'] = checksums - self.save_config() - if force: - modified = False - blocked = False - return new.name, new.author, new.currentVersion, oldversion, new.uiVersion, modified, blocked, source, \ - sourceurl, new.changelogUrl, dev - return url, [], False, False, None, False, False, '?', None, None, None + source, sourceurl = self.parse_url_source(old['URL']) + new = self.parse_url(old['URL']) + if force or (new.currentVersion != old['Version'] and update and not modified and not blocked): + new.get_addon() + self.cleanup(old['Directories']) + new.install(self.path) + checksums = {} + for directory in new.directories: + checksums[directory] = dirhash(self.path / directory) + old['Name'] = new.name + old['Version'] = new.currentVersion + old['Directories'] = new.directories + old['Checksums'] = checksums + self.save_config() + if force: + modified = False + blocked = False + return new.name, new.author, new.currentVersion, oldversion, new.uiVersion, modified, blocked, source, \ + sourceurl, new.changelogUrl, dev def check_checksum(self, addon, bulk=True): checksums = {} @@ -370,8 +358,7 @@ def dev_toggle(self, url): self.save_config() return state else: - addon = self.check_if_installed(url) - if addon: + if addon := self.check_if_installed(url): if addon['URL'].startswith('https://addons.wago.io/addons/'): state = self.check_if_dev(url) if state == 0: @@ -387,8 +374,7 @@ def dev_toggle(self, url): return None def block_toggle(self, url): - addon = self.check_if_installed(url) - if addon: + if addon := self.check_if_installed(url): state = self.check_if_blocked(addon) if state: addon.pop('Block', None) @@ -409,17 +395,15 @@ def generic_toggle(self, option, inside=None): return self.config[option] def backup_check(self): - if self.config['Backup']['Enabled']: - if not os.path.isfile(Path('WTF-Backup', f'{datetime.datetime.now().strftime("%d%m%y")}.zip')): - listofbackups = [Path(x) for x in glob.glob('WTF-Backup/*.zip')] - if len(listofbackups) == self.config['Backup']['Number']: - oldest_file = min(listofbackups, key=os.path.getctime) - os.remove(oldest_file) - return True - else: - return False - else: + if not self.config['Backup']['Enabled']: return False + if os.path.isfile(Path('WTF-Backup', f'{datetime.datetime.now().strftime("%d%m%y")}.zip')): + return False + listofbackups = [Path(x) for x in glob.glob('WTF-Backup/*.zip')] + if len(listofbackups) == self.config['Backup']['Number']: + oldest_file = min(listofbackups, key=os.path.getctime) + os.remove(oldest_file) + return True def backup_wtf(self, console): archive = Path('WTF-Backup', f'{datetime.datetime.now().strftime("%d%m%y")}.zip') @@ -433,7 +417,7 @@ def backup_wtf(self, console): zipf = zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED) filecount = 0 for _, _, files in os.walk('WTF/', topdown=True, followlinks=True): - files = [f for f in files if not f[0] == '.'] + files = [f for f in files if f[0] != '.'] filecount += len(files) if filecount > 0: with Progress('{task.completed}/{task.total}', '|', BarColumn(bar_width=None), '|', auto_refresh=False, @@ -441,7 +425,7 @@ def backup_wtf(self, console): task = progress.add_task('', total=filecount) while not progress.finished: for root, _, files in os.walk('WTF/', topdown=True, followlinks=True): - files = [f for f in files if not f[0] == '.'] + files = [f for f in files if f[0] != '.'] for f in files: zipf.write(Path(root, f)) progress.update(task, advance=1, refresh=True) @@ -482,15 +466,12 @@ def search(self, query): raise RuntimeError('This feature only searches the database of the Wago Addons. ' 'So their API key is required.\n' 'It can be obtained here: https://addons.wago.io/patreon') - results = [] payload = requests.get(f'https://addons.wago.io/api/external/addons/_search?query={quote_plus(query.strip())}' f'&game_version={self.clientType}', headers=HEADERS, auth=APIAuth('Bearer', self.config['WAAAPIKey']), timeout=5) self.parse_wagoaddons_error(payload.status_code) payload = payload.json() - for result in payload['data']: - results.append(result['website_url']) - return results + return [result['website_url'] for result in payload['data']] def create_reg(self): with open('CurseBreaker.reg', 'w') as outfile: @@ -527,7 +508,7 @@ def parse_wagoapp_payload(self, url): return f'https://addons.wago.io/addons/{payload["slug"]}' # TODO Improve the check when Wago Addons API will be will be expanded - def bulk_check(self, addons): + def bulk_check(self, addons): # sourcery skip: extract-method ids_wowi = [] ids_wago = [] for addon in addons: @@ -536,13 +517,13 @@ def bulk_check(self, addons): elif addon['URL'].startswith('https://addons.wago.io/addons/') and \ addon['URL'] not in self.config['IgnoreClientVersion'].keys(): ids_wago.append({'slug': addon['URL'].replace('https://addons.wago.io/addons/', ''), 'id': ''}) - if len(ids_wowi) > 0: + if ids_wowi: payload = requests.get(f'https://api.mmoui.com/v3/game/WOW/filedetails/{",".join(ids_wowi)}.json', headers=HEADERS, timeout=5).json() if 'ERROR' not in payload: for addon in payload: self.wowiCache[str(addon['UID'])] = addon - if len(ids_wago) > 0 and self.config['WAAAPIKey'] != '': + if ids_wago and self.config['WAAAPIKey'] != '': if not self.wagoIdCache: self.wagoIdCache = requests.get(f'https://addons.wago.io/api/data/slugs?game_version={self.clientType}', headers=HEADERS, timeout=5) @@ -569,16 +550,15 @@ def bulk_tukui_check(self): self.tukuiCache = requests.get('https://api.tukui.org/v1/addons', headers=HEADERS, timeout=5).json() def detect_accounts(self): - if os.path.isdir(Path('WTF/Account')): - accounts = os.listdir(Path('WTF/Account')) - accounts_processed = [] - for account in accounts: - if os.path.isfile(Path(f'WTF/Account/{account}/SavedVariables/WeakAuras.lua')) or \ - os.path.isfile(Path(f'WTF/Account/{account}/SavedVariables/Plater.lua')): - accounts_processed.append(account) - return accounts_processed - else: + if not os.path.isdir(Path('WTF/Account')): return [] + accounts = os.listdir(Path('WTF/Account')) + accounts_processed = [] + for account in accounts: + if os.path.isfile(Path(f'WTF/Account/{account}/SavedVariables/WeakAuras.lua')) or \ + os.path.isfile(Path(f'WTF/Account/{account}/SavedVariables/Plater.lua')): + accounts_processed.append(account) + return accounts_processed def detect_addons(self): if self.config['WAAAPIKey'] == '': @@ -649,7 +629,7 @@ def parse_wagoaddons_error(self, code): raise RuntimeError('Provided Wago Addons API key is expired. Please acquire a new one.') elif code == 423: raise RuntimeError('Provided Wago Addons API key is blocked. Please acquire a new one.') - elif code == 429 or code == 500: + elif code in [429, 500]: raise RuntimeError('Temporary Wago Addons API issue. Please try later.') diff --git a/CB/GitHub.py b/CB/GitHub.py index 356f383..1e7cdca 100644 --- a/CB/GitHub.py +++ b/CB/GitHub.py @@ -16,8 +16,8 @@ def __init__(self, url, clienttype, apikey): try: self.payload = requests.get(f'https://api.github.com/repos/{project}/releases', headers=HEADERS, auth=APIAuth('token', self.apiKey), timeout=10) - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): - raise RuntimeError(f'{project}\nGitHub API failed to respond.') + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e: + raise RuntimeError(f'{project}\nGitHub API failed to respond.') from e if self.payload.status_code == 401: raise RuntimeError(f'{project}\nIncorrect or expired GitHub API personal access token.') elif self.payload.status_code == 403: @@ -33,7 +33,7 @@ def __init__(self, url, clienttype, apikey): self.payloads.append(release) if len(self.payloads) > 14: break - if len(self.payloads) == 0: + if not self.payloads: raise RuntimeError(f'{url}\nThis integration supports only the projects that provide packaged' f' releases.') self.name = project.split('/')[1] @@ -55,7 +55,10 @@ def parse(self): self.currentVersion = self.payloads[self.releaseDepth]['tag_name'] or self.payloads[self.releaseDepth]['name'] self.changelogUrl = self.payloads[self.releaseDepth]['html_url'] self.parse_metadata() - self.get_latest_package() + if self.metadata: + self.get_latest_package() + else: + self.get_latest_package_nometa() def parse_metadata(self): for release in self.payloads[self.releaseDepth]['assets']: @@ -68,60 +71,60 @@ def parse_metadata(self): self.metadata = None def get_latest_package(self): - if self.metadata: - targetfile = None - if self.clientType == 'classic': - targetflavor = 'classic' - elif self.clientType == 'wotlk': - targetflavor = 'wrath' - else: - targetflavor = 'mainline' - for release in self.metadata['releases']: - if not release['nolib']: - for flavor in release['metadata']: - if flavor['flavor'] == targetflavor: - targetfile = release['filename'] - if 'name' in release: - self.name = release['name'] - self.currentVersion = release['version'] - break - if targetfile: + targetfile = None + if self.clientType == 'classic': + targetflavor = 'classic' + elif self.clientType == 'wotlk': + targetflavor = 'wrath' + else: + targetflavor = 'mainline' + for release in self.metadata['releases']: + if not release['nolib']: + for flavor in release['metadata']: + if flavor['flavor'] == targetflavor: + targetfile = release['filename'] + if 'name' in release: + self.name = release['name'] + self.currentVersion = release['version'] break - if not targetfile: - self.releaseDepth += 1 - self.parse() - for release in self.payloads[self.releaseDepth]['assets']: - if release['name'] and release['name'] == targetfile: - self.downloadUrl = release['url'] + if targetfile: break - if not self.downloadUrl: - self.releaseDepth += 1 - self.parse() + if not targetfile: + self.releaseDepth += 1 + self.parse() + for release in self.payloads[self.releaseDepth]['assets']: + if release['name'] and release['name'] == targetfile: + self.downloadUrl = release['url'] + break + if not self.downloadUrl: + self.releaseDepth += 1 + self.parse() + + def get_latest_package_nometa(self): + latest = None + latestclassic = None + latestwrath = None + for release in self.payloads[self.releaseDepth]['assets']: + if release['name'] and release['name'].endswith('.zip') and '-nolib' not in release['name'] \ + and release['content_type'] in ['application/x-zip-compressed', 'application/zip', 'raw']: + if not latest and not release['name'].endswith(('-classic.zip', '-bc.zip', '-bcc.zip', + '-wrath.zip')): + latest = release['url'] + elif not latestclassic and release['name'].endswith('-classic.zip'): + latestclassic = release['url'] + elif not latestwrath and release['name'].endswith('-wrath.zip'): + latestwrath = release['url'] + if (self.clientType == 'retail' and latest) \ + or (self.clientType == 'classic' and latest and not latestclassic) \ + or (self.clientType == 'wotlk' and latest and not latestwrath): + self.downloadUrl = latest + elif self.clientType == 'classic' and latestclassic: + self.downloadUrl = latestclassic + elif self.clientType == 'wotlk' and latestwrath: + self.downloadUrl = latestwrath else: - latest = None - latestclassic = None - latestwrath = None - for release in self.payloads[self.releaseDepth]['assets']: - if release['name'] and release['name'].endswith('.zip') and '-nolib' not in release['name'] \ - and release['content_type'] in ['application/x-zip-compressed', 'application/zip', 'raw']: - if not latest and not release['name'].endswith(('-classic.zip', '-bc.zip', '-bcc.zip', - '-wrath.zip')): - latest = release['url'] - elif not latestclassic and release['name'].endswith('-classic.zip'): - latestclassic = release['url'] - elif not latestwrath and release['name'].endswith('-wrath.zip'): - latestwrath = release['url'] - if (self.clientType == 'retail' and latest) \ - or (self.clientType == 'classic' and latest and not latestclassic) \ - or (self.clientType == 'wotlk' and latest and not latestwrath): - self.downloadUrl = latest - elif self.clientType == 'classic' and latestclassic: - self.downloadUrl = latestclassic - elif self.clientType == 'wotlk' and latestwrath: - self.downloadUrl = latestwrath - else: - self.releaseDepth += 1 - self.parse() + self.releaseDepth += 1 + self.parse() @retry() def get_addon(self): @@ -134,7 +137,7 @@ def get_addon(self): if '/' not in os.path.dirname(file): self.directories.append(os.path.dirname(file)) self.directories = list(filter(None, set(self.directories))) - if len(self.directories) == 0: + if not self.directories: raise RuntimeError(f'{self.name}.\nProject package is corrupted or incorrectly packaged.') def install(self, path): @@ -151,8 +154,8 @@ def __init__(self, addon, apikey): try: self.payload = requests.get(f'https://api.github.com/repos/{repository}/branches/{self.branch}', headers=HEADERS, auth=APIAuth('token', self.apiKey), timeout=10) - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): - raise RuntimeError(f'{self.name}\nGitHub API failed to respond.') + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e: + raise RuntimeError(f'{self.name}\nGitHub API failed to respond.') from e if self.payload.status_code == 401: raise RuntimeError(f'{self.name}\nIncorrect or expired GitHub API personal access token.') elif self.payload.status_code == 403: diff --git a/CB/Tukui.py b/CB/Tukui.py index a261869..4f55726 100644 --- a/CB/Tukui.py +++ b/CB/Tukui.py @@ -30,7 +30,7 @@ def get_addon(self): if '/' not in os.path.dirname(file): self.directories.append(os.path.dirname(file)) self.directories = list(filter(None, set(self.directories))) - if len(self.directories) == 0: + if not self.directories: raise RuntimeError(f'{self.name}.\nProject package is corrupted or incorrectly packaged.') def install(self, path): diff --git a/CB/Wago.py b/CB/Wago.py index 9fc6ddb..597f423 100644 --- a/CB/Wago.py +++ b/CB/Wago.py @@ -47,11 +47,11 @@ def parse_storage(self): for wa in wadata['displays']: if 'url' in wadata['displays'][wa]: search = self.urlParser.search(wadata['displays'][wa]['url']) - if search is not None and search.group(1) and search.group(2): - if 'parent' not in wadata['displays'][wa] and 'ignoreWagoUpdate' not in wadata['displays'][wa]: - if 'skipWagoUpdate' in wadata['displays'][wa]: - self.ignored[search.group(1)] = int(wadata['displays'][wa]['skipWagoUpdate']) - self.list[search.group(1)] = int(search.group(2)) + if (search is not None and search.group(1) and search.group(2) and + 'parent' not in wadata['displays'][wa] and 'ignoreWagoUpdate' not in wadata['displays'][wa]): + if 'skipWagoUpdate' in wadata['displays'][wa]: + self.ignored[search.group(1)] = int(wadata['displays'][wa]['skipWagoUpdate']) + self.list[search.group(1)] = int(search.group(2)) class PlaterParser(BaseParser): @@ -65,11 +65,11 @@ def parse_storage_internal(self, data): for script in data: if data[script]['url']: search = self.urlParser.search(data[script]['url']) - if search is not None and search.group(1) and search.group(2): - if 'ignoreWagoUpdate' not in data[script]: - if 'skipWagoUpdate' in data[script]: - self.ignored[search.group(1)] = int(data[script]['skipWagoUpdate']) - self.list[search.group(1)] = int(search.group(2)) + if (search is not None and search.group(1) and search.group(2) and + 'ignoreWagoUpdate' not in data[script]): + if 'skipWagoUpdate' in data[script]: + self.ignored[search.group(1)] = int(data[script]['skipWagoUpdate']) + self.list[search.group(1)] = int(search.group(2)) def parse_storage(self): with open(Path(f'WTF/Account/{self.accountName}/SavedVariables/Plater.lua'), 'r', encoding='utf-8', @@ -77,20 +77,17 @@ def parse_storage(self): data = file.read().replace('PlaterDB = {', '{', 1).rsplit('PlaterLanguage = {', 1)[0] platerdata = loads(data) for profile in platerdata['profiles']: - data = platerdata['profiles'][profile]['script_data'] - if data: + if data := platerdata['profiles'][profile]['script_data']: self.parse_storage_internal(data) - data = platerdata['profiles'][profile]['hook_data'] - if data: + if data := platerdata['profiles'][profile]['hook_data']: self.parse_storage_internal(data) - data = platerdata['profiles'][profile]['url'] - if data: + if data := platerdata['profiles'][profile]['url']: search = self.urlParser.search(data) - if search is not None and search.group(1) and search.group(2): - if 'ignoreWagoUpdate' not in platerdata['profiles'][profile]: - if 'skipWagoUpdate' in platerdata['profiles'][profile]: - self.ignored[search.group(1)] = int(platerdata['profiles'][profile]['skipWagoUpdate']) - self.list[search.group(1)] = int(search.group(2)) + if (search is not None and search.group(1) and search.group(2) and + 'ignoreWagoUpdate' not in platerdata['profiles'][profile]): + if 'skipWagoUpdate' in platerdata['profiles'][profile]: + self.ignored[search.group(1)] = int(platerdata['profiles'][profile]['skipWagoUpdate']) + self.list[search.group(1)] = int(search.group(2)) class WagoUpdater: @@ -122,10 +119,10 @@ def check_updates(self, addon): raise RuntimeError for entry in payload: if 'username' in entry and (not self.username or entry['username'] != self.username): - if not entry['slug'] in addon.list: + if entry['slug'] not in addon.list: entry['slug'] = entry['_id'] - if entry['version'] > addon.list[entry['slug']] and (not entry['slug'] in addon.ignored or - (entry['slug'] in addon.ignored and entry['version'] != addon.ignored[entry['slug']])): + if (entry['version'] > addon.list[entry['slug']] and + (entry['slug'] not in addon.ignored or entry['version'] != addon.ignored[entry['slug']])): output[0].append([entry['name'], entry['url']]) self.update_entry(entry, addon) elif 'name' in entry: @@ -138,7 +135,7 @@ def check_updates(self, addon): def check_stash(self, wa, plater): output = [] if len(self.stash) > 0: - payload = requests.post(f'https://data.wago.io/api/check/', + payload = requests.post('https://data.wago.io/api/check/', json={'ids': self.stash}, headers=self.headers, timeout=15).json() for entry in payload: output.append(entry['name']) @@ -158,13 +155,12 @@ def check_stash(self, wa, plater): return output def parse_changelog(self, entry): - if 'changelog' in entry and 'text' in entry['changelog']: - if entry['changelog']['format'] == 'bbcode': - return self.bbParser.strip(entry['changelog']['text']) - elif entry['changelog']['format'] == 'markdown': - return self.mdParser.convert(entry['changelog']['text']) - else: + if 'changelog' not in entry or 'text' not in entry['changelog']: return '' + if entry['changelog']['format'] == 'bbcode': + return self.bbParser.strip(entry['changelog']['text']) + elif entry['changelog']['format'] == 'markdown': + return self.mdParser.convert(entry['changelog']['text']) @retry('Failed to parse Wago data. Wago might be down or provided API key is incorrect.') def update_entry(self, entry, addon): diff --git a/CB/WagoAddons.py b/CB/WagoAddons.py index 2c7345e..e97ba4f 100644 --- a/CB/WagoAddons.py +++ b/CB/WagoAddons.py @@ -27,23 +27,23 @@ def __init__(self, url, checkcache, clienttype, allowdev, apikey): self.payload = requests.get(f'https://addons.wago.io/api/external/addons/{project}?game_version=' f'{self.clientType}', headers=HEADERS, auth=APIAuth('Bearer', self.apiKey), timeout=5) - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): - raise RuntimeError(f'{url}\nWago Addons API failed to respond.') + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e: + raise RuntimeError(f'{url}\nWago Addons API failed to respond.') from e if self.payload.status_code == 401: raise RuntimeError(f'{url}\nWago Addons API key is missing or incorrect.') elif self.payload.status_code == 403: raise RuntimeError(f'{url}\nProvided Wago Addons API key is expired. Please acquire a new one.') elif self.payload.status_code == 423: raise RuntimeError(f'{url}\nProvided Wago Addons API key is blocked. Please acquire a new one.') - elif self.payload.status_code == 404 or self.payload.status_code == 429 or self.payload.status_code == 500: + elif self.payload.status_code in {404, 429, 500}: raise RuntimeError(f'{url}\nThis might be a temporary issue with Wago Addons API or the project was ' f'removed/renamed. In this case, uninstall it (and reinstall if it still exists) ' f'to fix this issue.') else: try: self.payload = self.payload.json() - except (StopIteration, JSONDecodeError): - raise RuntimeError(f'{url}\nThis might be a temporary issue with Wago Addons API.') + except (StopIteration, JSONDecodeError) as e: + raise RuntimeError(f'{url}\nThis might be a temporary issue with Wago Addons API.') from e self.name = self.payload['display_name'].strip().strip('\u200b') self.allowDev = allowdev self.downloadUrl = None @@ -81,7 +81,7 @@ def get_current_version(self): else: release.pop('alpha', None) release.pop('beta', None) - if len(release) == 0: + if not release: raise RuntimeError(f'{self.name}.\nFailed to find release for your client version.') release = self.payload['recent_release'][max(release, key=release.get)] @@ -98,7 +98,7 @@ def get_addon(self): if '/' not in os.path.dirname(file): self.directories.append(os.path.dirname(file)) self.directories = list(filter(None, set(self.directories))) - if len(self.directories) == 0: + if not self.directories: raise RuntimeError(f'{self.name}.\nProject package is corrupted or incorrectly packaged.') def install(self, path): diff --git a/CB/WoWInterface.py b/CB/WoWInterface.py index ddad293..0e94017 100644 --- a/CB/WoWInterface.py +++ b/CB/WoWInterface.py @@ -16,8 +16,8 @@ def __init__(self, url, checkcache): try: self.payload = requests.get(f'https://api.mmoui.com/v3/game/WOW/filedetails/{project}.json', headers=HEADERS, timeout=5).json() - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): - raise RuntimeError(f'{url}\nWoWInterface API failed to respond.') + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e: + raise RuntimeError(f'{url}\nWoWInterface API failed to respond.') from e if 'ERROR' in self.payload: raise RuntimeError(f'{url}\nThis might be a temporary error or this project is not supported ' f'by WoWInterface API.') @@ -39,7 +39,7 @@ def get_addon(self): if '/' not in os.path.dirname(file): self.directories.append(os.path.dirname(file)) self.directories = list(filter(None, set(self.directories))) - if len(self.directories) == 0: + if not self.directories: raise RuntimeError(f'{self.name}.\nProject package is corrupted or incorrectly packaged.') def install(self, path): diff --git a/CB/__init__.py b/CB/__init__.py index 574a6c7..ec7c1cf 100644 --- a/CB/__init__.py +++ b/CB/__init__.py @@ -21,15 +21,13 @@ def inner(*args, **kwargs): continue else: return result + if custom_error: + raise RuntimeError(custom_error) from None + elif description: + raise RuntimeError(f'Failed to parse addon data: {description}') from None else: - if custom_error: - raise RuntimeError(custom_error) from None - else: - if description: - raise RuntimeError(f'Failed to parse addon data: {description}') from None - else: - raise RuntimeError('Unknown error during parsing addon data. ' - 'There may be some issue with the website.') from None + raise RuntimeError('Unknown error during parsing addon data. ' + 'There may be some issue with the website.') from None return inner return wraps diff --git a/CurseBreaker.py b/CurseBreaker.py index 1fe5f11..5cd12ed 100644 --- a/CurseBreaker.py +++ b/CurseBreaker.py @@ -17,7 +17,7 @@ from shlex import split from pathlib import Path from datetime import datetime -from contextlib import nullcontext +from contextlib import nullcontext, suppress from rich import box from rich.text import Text from rich.rule import Rule @@ -63,11 +63,9 @@ def start(self): # Check if executable is in good location if not glob.glob('World*.app') and not glob.glob('Wow*.exe') or \ not os.path.isdir(Path('Interface/AddOns')) or not os.path.isdir('WTF'): - self.console.print('[bold red]This executable should be placed in the same directory where Wow.exe, ' - 'WowClassic.exe or World of Warcraft.app is located. Additionally, make sure that ' - 'this WoW installation was started at least once.[/bold red]\n') - pause(self.headless) - sys.exit(1) + self.handle_shutdown('[bold red]This executable should be placed in the same directory where Wow.exe, WowCl' + 'assic.exe or World of Warcraft.app is located. Additionally, make sure that this WoW ' + 'installation was started at least once.[/bold red]\n') # Detect client flavor if 'CURSEBREAKER_FLAVOR' in os.environ: flavor = os.environ.get('CURSEBREAKER_FLAVOR') @@ -82,27 +80,21 @@ def start(self): self.core.clientType = 'classic' set_terminal_title(f'CurseBreaker v{__version__} - Classic') else: - self.console.print('[bold red]This client release is currently unsupported by CurseBreaker.[/bold red]\n') - pause(self.headless) - sys.exit(1) + self.handle_shutdown('[bold red]This client release is currently unsupported by CurseBreaker.[/bold red]\n') # Check if client have write access try: with open('PermissionTest', 'w') as _: pass os.remove('PermissionTest') except IOError: - self.console.print('[bold red]CurseBreaker doesn\'t have write rights for the current directory.\n' - 'Try starting it with administrative privileges.[/bold red]\n') - pause(self.headless) - sys.exit(1) + self.handle_shutdown('[bold red]CurseBreaker doesn\'t have write rights for the current directory.\nTry sta' + 'rting it with administrative privileges.[/bold red]\n') self.auto_update() try: self.core.init_config() except RuntimeError: - self.console.print('[bold red]The config file is corrupted. Restore the earlier version from backup.' - '[/bold red]\n') - pause(self.headless) - sys.exit(1) + self.handle_shutdown('[bold red]The config file is corrupted. Restore the earlier version from backup.[/bol' + 'd red]\n') self.setup_table() # Wago Addons URI Support if len(sys.argv) == 2 and sys.argv[1].startswith('wago-app://addons/'): @@ -169,9 +161,7 @@ def start(self): self.console.print('Press [bold]I[/bold] to enter interactive mode or any other button to close' ' the application.') keypress = self.handle_keypress(0) - if keypress and keypress.lower() in [b'i', 'i']: - pass - else: + if not keypress or keypress.lower() not in [b'i', 'i']: sys.exit(0) if self.headless: sys.exit(1) @@ -188,7 +178,7 @@ def start(self): 'dons.') self.motd_parser() if self.core.backup_check(): - self.console.print(f'[green]Backing up WTF directory:[/green]') + self.console.print('[green]Backing up WTF directory:[/green]') self.core.backup_wtf(self.console) self.console.print('') # Prompt session @@ -211,56 +201,55 @@ def start(self): else: self.console.print('Command not found.') - def auto_update(self): - if getattr(sys, 'frozen', False) and 'CURSEBREAKER_VARDEXMODE' not in os.environ: + def auto_update(self): # sourcery skip: extract-method + if not getattr(sys, 'frozen', False) or 'CURSEBREAKER_VARDEXMODE' in os.environ: + return + try: + if os.path.isfile(f'{sys.executable}.old'): + with suppress(PermissionError): + os.remove(f'{sys.executable}.old') try: - if os.path.isfile(sys.executable + '.old'): - try: - os.remove(sys.executable + '.old') - except PermissionError: - pass - try: - payload = requests.get('https://api.github.com/repos/AcidWeb/CurseBreaker/releases/latest', - headers=HEADERS, timeout=10).json() - except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): - return - if 'name' in payload and 'body' in payload and 'assets' in payload: - remoteversion = payload['name'] - changelog = payload['body'] - url = None - for binary in payload['assets']: - if (self.os == 'Windows' and '.exe' in binary['name'])\ + payload = requests.get('https://api.github.com/repos/AcidWeb/CurseBreaker/releases/latest', + headers=HEADERS, timeout=10).json() + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + return + if 'name' in payload and 'body' in payload and 'assets' in payload: + remoteversion = payload['name'] + changelog = payload['body'] + url = None + for binary in payload['assets']: + if (self.os == 'Windows' and '.exe' in binary['name'])\ or (self.os == 'Darwin' and '.zip' in binary['name'])\ or (self.os == 'Linux' and '.gz' in binary['name']): - url = binary['browser_download_url'] - break - if url and Version(remoteversion[1:]) > Version(__version__): - self.console.print('[green]Updating CurseBreaker...[/green]') - shutil.move(sys.executable, sys.executable + '.old') - payload = requests.get(url, headers=HEADERS, timeout=10) - if self.os == 'Darwin': - zipfile.ZipFile(io.BytesIO(payload.content)).extractall(path=os.path.dirname( - os.path.abspath(sys.executable))) - else: - with open(sys.executable, 'wb') as f: - if self.os == 'Windows': - f.write(payload.content) - elif self.os == 'Linux': - f.write(gzip.decompress(payload.content)) - os.chmod(sys.executable, 0o775) - self.console.print(f'[bold green]Update complete! The application will be restarted now.' - f'[/bold green]\n\n[green]Changelog:[/green]\n{changelog}\n') - self.print_log() - pause(self.headless) - subprocess.call([sys.executable] + sys.argv[1:]) - sys.exit(0) - except Exception as e: - if os.path.isfile(sys.executable + '.old'): - shutil.move(sys.executable + '.old', sys.executable) - self.console.print(f'[bold red]Update failed!\n\nReason: {str(e)}[/bold red]\n') - self.print_log() - pause(self.headless) - sys.exit(1) + url = binary['browser_download_url'] + break + if url and Version(remoteversion[1:]) > Version(__version__): + self.console.print('[green]Updating CurseBreaker...[/green]') + shutil.move(sys.executable, f'{sys.executable}.old') + payload = requests.get(url, headers=HEADERS, timeout=10) + if self.os == 'Darwin': + zipfile.ZipFile(io.BytesIO(payload.content)).extractall(path=os.path.dirname( + os.path.abspath(sys.executable))) + else: + with open(sys.executable, 'wb') as f: + if self.os == 'Windows': + f.write(payload.content) + elif self.os == 'Linux': + f.write(gzip.decompress(payload.content)) + os.chmod(sys.executable, 0o775) + self.console.print(f'[bold green]Update complete! The application will be restarted now.' + f'[/bold green]\n\n[green]Changelog:[/green]\n{changelog}\n') + self.print_log() + pause(self.headless) + subprocess.call([sys.executable] + sys.argv[1:]) + sys.exit(0) + except Exception as e: + if os.path.isfile(f'{sys.executable}.old'): + shutil.move(f'{sys.executable}.old', sys.executable) + self.console.print(f'[bold red]Update failed!\n\nReason: {str(e)}[/bold red]\n') + self.print_log() + pause(self.headless) + sys.exit(1) def motd_parser(self): payload = requests.get('https://storage.googleapis.com/cursebreaker/motd', headers=HEADERS, timeout=5) @@ -285,13 +274,13 @@ def handle_exception(self, e, table=True): self.console.print(Traceback.from_exception(exc_type=e.__class__, exc_value=e, traceback=e.__traceback__, width=width)) + # noinspection PyUnboundLocalVariable def handle_keypress(self, wait): if not self.headless: kb = KBHit() starttime = time.time() keypress = None while True: - # noinspection PyUnboundLocalVariable if self.headless: break elif kb.kbhit(): @@ -304,6 +293,11 @@ def handle_keypress(self, wait): kb.set_normal_term() return keypress + def handle_shutdown(self, message): + self.console.print(message) + pause(self.headless) + sys.exit(1) + def print_header(self): if self.headless: self.console.print(f'[bold green]CurseBreaker[/bold green] [bold red]v{__version__}[/bold red] | ' @@ -321,26 +315,26 @@ def print_log(self): log.write(html) def print_author_reminder(self): - if random.randint(1, 10) == 1: - addon = random.choice(self.core.config['Addons']) - project_url = addon['URL'] - if not project_url.startswith('https://'): - project_url = project_url.lower() - if not project_url.endswith(':dev'): - project_url = f'{project_url}:dev' - project_url = self.core.masterConfig['CustomRepository'][project_url]['SupportURL'] - self.console.print(Panel(f'Hey! You use [bold white]{addon["Name"]}[/bold white] quite often. Maybe it\'s t' - f'ime to check out the project page how you can support the author(s)? [link=' - f'{project_url}]{project_url}[/link]', title=':sparkles: Support :sparkles:', - border_style='yellow')) - self.console.print('') + if random.randint(1, 10) != 1: + return + addon = random.choice(self.core.config['Addons']) + project_url = addon['URL'] + if not project_url.startswith('https://'): + project_url = project_url.lower() + if not project_url.endswith(':dev'): + project_url = f'{project_url}:dev' + project_url = self.core.masterConfig['CustomRepository'][project_url]['SupportURL'] + self.console.print(Panel(f'Hey! You use [bold white]{addon["Name"]}[/bold white] quite often. Maybe it\'s t' + f'ime to check out the project page how you can support the author(s)? [link=' + f'{project_url}]{project_url}[/link]', title=':sparkles: Support :sparkles:', + border_style='yellow')) + self.console.print('') def setup_console(self): if self.headless: self.console = Console(record=True) if self.os == 'Windows': - window = windll.kernel32.GetConsoleWindow() - if window: + if window := windll.kernel32.GetConsoleWindow(): windll.user32.ShowWindow(window, 0) elif detect_legacy_windows(): set_terminal_size(100, 50) @@ -371,9 +365,7 @@ def setup_completer(self): slugs.append(f'gh:{item}') for item in self.slugs['custom']: slugs.append(f'{item}') - accounts = [] - for account in self.core.detect_accounts(): - accounts.append(account) + accounts = list(self.core.detect_accounts()) self.completer = NestedCompleter.from_nested_dict({ 'install': WordCompleter(slugs, ignore_case=True, match_middle=True, WORD=True), 'uninstall': WordCompleter(addons, ignore_case=True), @@ -456,10 +448,10 @@ def c_install(self, args): if '-i' in pargs: optignore = True args = args.replace('-i', '', 1) - args = re.sub(r'([a-zA-Z0-9_:])([ ]+)([a-zA-Z0-9_:])', r'\1,\3', args) + args = re.sub(r'([a-zA-Z0-9_:])( +)([a-zA-Z0-9_:])', r'\1,\3', args) addons = [re.sub(r'[\[\]]', '', addon).strip() for addon in list(reader([args], skipinitialspace=True))[0]] exceptions = [] - if len(addons) > 0: + if addons: if self.core.clientType != 'retail': for addon in addons: if addon.startswith('https://www.wowinterface.com/downloads/') or addon.startswith('wowi:'): @@ -484,7 +476,7 @@ def c_install(self, args): exceptions.append(e) progress.update(task, advance=1, refresh=True) self.console.print(self.table) - if len(exceptions) > 0: + if exceptions: self.handle_exception(exceptions, False) else: self.console.print('[green]Usage:[/green]\n\tThis command accepts a space-separated list of links as an arg' @@ -512,10 +504,10 @@ def c_uninstall(self, args): for addon in addons: name, version = self.core.del_addon(addon, optkeep) if name: - self.table.add_row(f'[bold red]Uninstalled[/bold red]', + self.table.add_row('[bold red]Uninstalled[/bold red]', Text(name, no_wrap=True), Text(version, no_wrap=True)) else: - self.table.add_row(f'[bold black]Not installed[/bold black]', + self.table.add_row('[bold black]Not installed[/bold black]', Text(addon, no_wrap=True), Text('', no_wrap=True)) progress.update(task, advance=1, refresh=True) self.console.print(self.table) @@ -540,10 +532,8 @@ def c_update(self, args, addline=False, update=True, force=False, reverseprovide auto_refresh=False, console=None if self.headless else self.console) as progress: task = progress.add_task('', total=len(addons)) if not args: - try: + with suppress(RuntimeError): self.core.bulk_check(addons) - except RuntimeError: - pass self.core.bulk_check_checksum(addons, progress) while not progress.finished: for addon in addons: @@ -562,25 +552,22 @@ def c_update(self, args, addline=False, update=True, force=False, reverseprovide payload = [f'[bold red]Modified[/bold red]{additionalstatus}', self.parse_link(name, sourceurl, authors=authors), self.parse_link(versionold, changelog, dstate, uiversion=uiversion)] + elif compact and compacted > -1 and source != 'Unsupported': + compacted += 1 else: - if compact and compacted > -1 and source != 'Unsupported': - compacted += 1 - else: - payload = [f'[green]Up-to-date[/green]{additionalstatus}', - self.parse_link(name, sourceurl, authors=authors), - self.parse_link(versionold, changelog, dstate, - uiversion=uiversion)] - else: - if modified or blocked: - payload = [f'[bold red]Update suppressed[/bold red]{additionalstatus}', + payload = [f'[green]Up-to-date[/green]{additionalstatus}', self.parse_link(name, sourceurl, authors=authors), self.parse_link(versionold, changelog, dstate, uiversion=uiversion)] - else: - version = self.parse_link(versionnew, changelog, dstate, uiversion=uiversion) - version.stylize('yellow') - payload = [f'[yellow]{"Updated" if update else "Update available"}[/yellow]' - f'{additionalstatus}', self.parse_link(name, sourceurl, - authors=authors), version] + elif modified or blocked: + payload = [f'[bold red]Update suppressed[/bold red]{additionalstatus}', + self.parse_link(name, sourceurl, authors=authors), + self.parse_link(versionold, changelog, dstate, uiversion=uiversion)] + else: + version = self.parse_link(versionnew, changelog, dstate, uiversion=uiversion) + version.stylize('yellow') + payload = [f'[yellow]{"Updated" if update else "Update available"}[/yellow]' + f'{additionalstatus}', self.parse_link(name, sourceurl, + authors=authors), version] else: payload = [f'[bold black]Not installed[/bold black]{additionalstatus}', Text(addon, no_wrap=True), Text('', no_wrap=True)] @@ -599,26 +586,23 @@ def c_update(self, args, addline=False, update=True, force=False, reverseprovide self.console.print(self.table) if compacted > 0: self.console.print(f'Additionally [green]{compacted}[/green] addons are up-to-date.') - overlap = self.core.check_if_overlap() - if overlap: + if overlap := self.core.check_if_overlap(): self.console.print(f'\n[bold red]Detected addon directory overlap. This will cause issues. Affected add' f'ons:[/bold red]\n{overlap}') else: self.console.print('Apparently there are no addons installed by CurseBreaker (or you provided incorrect add' 'on name).\nCommand [green]import[/green] might be used to detect already installed addo' 'ns.', highlight=False) - if len(exceptions) > 0: + if exceptions: self.handle_exception(exceptions, False) + # noinspection PyTypeChecker def c_force_update(self, args): if args: self.c_update(args, False, True, True) - else: - # noinspection PyTypeChecker - answer = confirm(HTML('Execute a forced update of all addons and overwrite ALL local changes' - '?')) - if answer: - self.c_update(False, False, True, True) + elif confirm(HTML('Execute a forced update of all addons and overwrite ALL local changes?')): + self.c_update(False, False, True, True) def c_status(self, args): optsource = False @@ -660,8 +644,7 @@ def c_toggle(self, args): if args: args = args.strip() if args.startswith('channel'): - args = args[8:] - if args: + if args := args[8:]: status = self.core.dev_toggle(args) if status is None: self.console.print('[bold red]This addon doesn\'t exist or it is not installed yet.[/bold red]') @@ -684,8 +667,7 @@ def c_toggle(self, args): self.console.print('[green]Usage:[/green]\n\tThis command accepts an addon name (or "global") as an' ' argument.', highlight=False) elif args.startswith('pinning'): - args = args[8:] - if args: + if args := args[8:]: status = self.core.block_toggle(args) if status is None: self.console.print('[bold red]This addon does not exist or it is not installed yet.[/bold red]') @@ -696,8 +678,7 @@ def c_toggle(self, args): else: self.console.print('[green]Usage:[/green]\n\tThis command accepts an addon name as an argument.') elif args.startswith('wago'): - args = args[5:] - if args: + if args := args[5:]: if args == self.core.config['WAUsername']: self.console.print(f'Wago version check is now: [green]ENABLED[/green]\nEntries created by ' f'[bold white]{self.core.config["WAUsername"]}[/bold white] are now ' @@ -708,14 +689,13 @@ def c_toggle(self, args): self.console.print(f'Wago version check is now: [green]ENABLED[/green]\nEntries created by ' f'[bold white]{self.core.config["WAUsername"]}[/bold white] are now ' f'ignored.') + elif self.core.config['WAUsername'] == 'DISABLED': + self.core.config['WAUsername'] = '' + self.console.print('Wago version check is now: [green]ENABLED[/green]') else: - if self.core.config['WAUsername'] == 'DISABLED': - self.core.config['WAUsername'] = '' - self.console.print('Wago version check is now: [green]ENABLED[/green]') - else: - self.core.config['WAUsername'] = 'DISABLED' - shutil.rmtree(Path('Interface/AddOns/WeakAurasCompanion'), ignore_errors=True) - self.console.print('Wago version check is now: [red]DISABLED[/red]') + self.core.config['WAUsername'] = 'DISABLED' + shutil.rmtree(Path('Interface/AddOns/WeakAurasCompanion'), ignore_errors=True) + self.console.print('Wago version check is now: [red]DISABLED[/red]') self.core.save_config() elif args.startswith('authors'): status = self.core.generic_toggle('ShowAuthors') @@ -753,48 +733,37 @@ def c_toggle(self, args): 'green]\n\t\tEnables/disables automatic Wago updates.\n\t\tIf a username is provided che' 'ck will start to ignore the specified author.', highlight=False) + def _c_set_parse(self, msg, key, value): + self.console.print(msg) + self.core.config[key] = value.strip() + self.core.save_config() + def c_set(self, args): if args: args = args.strip() if args.startswith('wago_addons_api'): - args = args[16:] - if args: - self.console.print('Wago Addons API key is now set.') - self.core.config['WAAAPIKey'] = args.strip() - self.core.save_config() + if args := args[16:]: + self._c_set_parse('Wago Addons API key is now set.', 'WAAAPIKey', args) elif self.core.config['WAAAPIKey'] != '': - self.console.print('Wago Addons API key is now removed.') - self.core.config['WAAAPIKey'] = '' - self.core.save_config() + self._c_set_parse('Wago Addons API key is now removed.', 'WAAAPIKey', '') else: self.console.print('[green]Usage:[/green]\n\tThis command accepts API key as an argument.') elif args.startswith('wago_api'): - args = args[9:] - if args: - self.console.print('Wago API key is now set.') - self.core.config['WAAPIKey'] = args.strip() - self.core.save_config() + if args := args[9:]: + self._c_set_parse('Wago API key is now set.', 'WAAPIKey', args) elif self.core.config['WAAPIKey'] != '': - self.console.print('Wago API key is now removed.') - self.core.config['WAAPIKey'] = '' - self.core.save_config() + self._c_set_parse('Wago API key is now removed.', 'WAAPIKey', '') else: self.console.print('[green]Usage:[/green]\n\tThis command accepts API key as an argument.') elif args.startswith('gh_api'): - args = args[7:] - if args: - self.console.print('GitHub API key is now set.') - self.core.config['GHAPIKey'] = args.strip() - self.core.save_config() + if args := args[7:]: + self._c_set_parse('GitHub API key is now set.', 'GHAPIKey', args) elif self.core.config['GHAPIKey'] != '': - self.console.print('GitHub API key is now removed.') - self.core.config['GHAPIKey'] = '' - self.core.save_config() + self._c_set_parse('GitHub API key is now removed.', 'GHAPIKey', '') else: self.console.print('[green]Usage:[/green]\n\tThis command accepts API key as an argument.') elif args.startswith('wago_wow_account'): - args = args[17:] - if args: + if args := args[17:]: args = args.strip() if os.path.isfile(Path(f'WTF/Account/{args}/SavedVariables/WeakAuras.lua')) or \ os.path.isfile(Path(f'WTF/Account/{args}/SavedVariables/Plater.lua')): @@ -819,7 +788,7 @@ def c_set(self, args): 'count.\n\t[green]set gh_api [API key][/green]\n\t\tSets GitHub API key. Might be needed' ' to get around API rate limits.', highlight=False) - def c_wago_update(self, _, verbose=True, flush=True): + def c_wago_update(self, _, verbose=True, flush=True): # sourcery skip: extract-duplicate-method if os.path.isdir(Path('Interface/AddOns/WeakAuras')) or os.path.isdir(Path('Interface/AddOns/Plater')): accounts = self.core.detect_accounts() if self.core.config['WAAccountName'] != '' and self.core.config['WAAccountName'] not in accounts: @@ -904,7 +873,7 @@ def c_import(self, args): if args == 'install' and len(slugs) > 0: self.c_install(','.join(slugs)) else: - self.console.print(f'[green]New addons found:[/green]') + self.console.print('[green]New addons found:[/green]') for addon in names: self.console.print(addon, highlight=False) self.console.print(f'\n[yellow]Already installed addons:[/yellow]') @@ -983,8 +952,7 @@ def c_exit(self, _): if __name__ == '__main__': freeze_support() - clientpath = os.environ.get('CURSEBREAKER_PATH') - if clientpath: + if clientpath := os.environ.get('CURSEBREAKER_PATH'): os.chdir(clientpath) elif getattr(sys, 'frozen', False): os.chdir(os.path.dirname(os.path.abspath(sys.executable))) diff --git a/CurseBreaker.spec b/CurseBreaker.spec index 5615326..14a2d97 100644 --- a/CurseBreaker.spec +++ b/CurseBreaker.spec @@ -1,3 +1,6 @@ +import sys +sys.path.append(SPECPATH) + import platform from CB import __version__ as version diff --git a/Pipfile b/Pipfile index 2b27d10..a60846d 100644 --- a/Pipfile +++ b/Pipfile @@ -15,6 +15,7 @@ bbcode = "*" rich = "*" [dev-packages] +sourcery = "*" [requires] python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock index f959bce..b77308a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "a8ceab836cd8add089859509237fe87211e31d27dd244171bc3ad3bff8078926" + "sha256": "5575c6f9afc617cac84f49205b436b0bbf7f88ff4936441e430c0779a6b611a7" }, "pipfile-spec": 6, "requires": { @@ -250,5 +250,15 @@ "version": "==0.2.13" } }, - "develop": {} + "develop": { + "sourcery": { + "hashes": [ + "sha256:5c2d7723b7c1bbd3a904fa2d85ef074b9ef1091c917a4e8c57a88b70bcda0699", + "sha256:5d8ab8f61d0a55418b5debb999f76538d8e946a9a69a9e0be66593c1e17ff20f", + "sha256:ab3438770f4f1535a2a4d872f5a84d12773eeb00825b96a5f2ed321ec3912b5e" + ], + "index": "pypi", + "version": "==1.15.0" + } + } }