diff --git a/LICENSE b/LICENSE index 113b6eb..cc2bcb9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Fede Calendino +Copyright (c) 2022 Fede Calendino Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 47bd8c0..b6d22f8 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ### [Alfred Workflow](https://www.alfredapp.com/workflows/) to generate secure passwords 🔑️ -![PWD Gen default](/img/default.png) +![default](/img/screenshots/default.png) -![PWD Gen example](/img/low.png) +![usage](/img/screenshots/usage.png) diff --git a/alfred-pwd-gen b/alfred-pwd-gen new file mode 100644 index 0000000..e69de29 diff --git a/generator.py b/generator.py index d48e0b2..d375b35 100644 --- a/generator.py +++ b/generator.py @@ -17,7 +17,7 @@ def generate(letters, digits, symbols): random.shuffle(characters) - return ''.join(characters) + return "".join(characters) def _calc_streght(password): @@ -34,4 +34,4 @@ def streght(password): if current > baseline: return 16 - return int(15 * current/baseline) + return int(15 * current / baseline) diff --git a/img/default.png b/img/default.png deleted file mode 100644 index 41ea52e..0000000 Binary files a/img/default.png and /dev/null differ diff --git a/img/low.png b/img/low.png deleted file mode 100644 index 90b344e..0000000 Binary files a/img/low.png and /dev/null differ diff --git a/img/screenshots/default.png b/img/screenshots/default.png new file mode 100644 index 0000000..bed7a41 Binary files /dev/null and b/img/screenshots/default.png differ diff --git a/img/screenshots/usage.png b/img/screenshots/usage.png new file mode 100644 index 0000000..5954791 Binary files /dev/null and b/img/screenshots/usage.png differ diff --git a/info.plist b/info.plist index 7db57b6..a51f3b3 100644 --- a/info.plist +++ b/info.plist @@ -120,10 +120,8 @@ 25 - variablesdontexport - version - 1.2 + 1.3 webaddress https://github.com/fedecalendino/alfred-pwd-gen diff --git a/main.py b/main.py index bc6d36a..284803b 100644 --- a/main.py +++ b/main.py @@ -44,16 +44,17 @@ def format_subtitle(letters, digits, symbols, streght): def main(workflow): letters, digits, symbols = parse_args(workflow.args) - password = generator.generate(letters, digits, symbols) - streght = generator.streght(password) - - workflow.add_item( - title=" {}".format(password), - subtitle=format_subtitle(letters, digits, symbols, streght), - arg=password, - copytext=password, - valid=True - ) + for _ in range(5): + password = generator.generate(letters, digits, symbols) + streght = generator.streght(password) + + workflow.add_item( + title=" {}".format(password), + subtitle=format_subtitle(letters, digits, symbols, streght), + arg=password, + copytext=password, + valid=True, + ) if __name__ == u"__main__": diff --git a/workflow/__init__.py b/workflow/__init__.py index 17636a4..5736ad9 100644 --- a/workflow/__init__.py +++ b/workflow/__init__.py @@ -60,49 +60,49 @@ ) -__title__ = 'Alfred-Workflow' -__version__ = open(os.path.join(os.path.dirname(__file__), 'version')).read() -__author__ = 'Dean Jackson' -__licence__ = 'MIT' -__copyright__ = 'Copyright 2014-2019 Dean Jackson' +__title__ = "Alfred-Workflow" +__version__ = open(os.path.join(os.path.dirname(__file__), "version")).read() +__author__ = "Dean Jackson" +__licence__ = "MIT" +__copyright__ = "Copyright 2014-2019 Dean Jackson" __all__ = [ - 'Variables', - 'Workflow', - 'Workflow3', - 'manager', - 'PasswordNotFound', - 'KeychainError', - 'ICON_ACCOUNT', - 'ICON_BURN', - 'ICON_CLOCK', - 'ICON_COLOR', - 'ICON_COLOUR', - 'ICON_EJECT', - 'ICON_ERROR', - 'ICON_FAVORITE', - 'ICON_FAVOURITE', - 'ICON_GROUP', - 'ICON_HELP', - 'ICON_HOME', - 'ICON_INFO', - 'ICON_NETWORK', - 'ICON_NOTE', - 'ICON_SETTINGS', - 'ICON_SWIRL', - 'ICON_SWITCH', - 'ICON_SYNC', - 'ICON_TRASH', - 'ICON_USER', - 'ICON_WARNING', - 'ICON_WEB', - 'MATCH_ALL', - 'MATCH_ALLCHARS', - 'MATCH_ATOM', - 'MATCH_CAPITALS', - 'MATCH_INITIALS', - 'MATCH_INITIALS_CONTAIN', - 'MATCH_INITIALS_STARTSWITH', - 'MATCH_STARTSWITH', - 'MATCH_SUBSTRING', + "Variables", + "Workflow", + "Workflow3", + "manager", + "PasswordNotFound", + "KeychainError", + "ICON_ACCOUNT", + "ICON_BURN", + "ICON_CLOCK", + "ICON_COLOR", + "ICON_COLOUR", + "ICON_EJECT", + "ICON_ERROR", + "ICON_FAVORITE", + "ICON_FAVOURITE", + "ICON_GROUP", + "ICON_HELP", + "ICON_HOME", + "ICON_INFO", + "ICON_NETWORK", + "ICON_NOTE", + "ICON_SETTINGS", + "ICON_SWIRL", + "ICON_SWITCH", + "ICON_SYNC", + "ICON_TRASH", + "ICON_USER", + "ICON_WARNING", + "ICON_WEB", + "MATCH_ALL", + "MATCH_ALLCHARS", + "MATCH_ATOM", + "MATCH_CAPITALS", + "MATCH_INITIALS", + "MATCH_INITIALS_CONTAIN", + "MATCH_INITIALS_STARTSWITH", + "MATCH_STARTSWITH", + "MATCH_SUBSTRING", ] diff --git a/workflow/background.py b/workflow/background.py index c2bd735..1b6a744 100644 --- a/workflow/background.py +++ b/workflow/background.py @@ -27,7 +27,7 @@ from workflow import Workflow -__all__ = ['is_running', 'run_in_background'] +__all__ = ["is_running", "run_in_background"] _wf = None @@ -52,7 +52,7 @@ def _arg_cache(name): :rtype: ``unicode`` filepath """ - return wf().cachefile(name + '.argcache') + return wf().cachefile(name + ".argcache") def _pid_file(name): @@ -64,7 +64,7 @@ def _pid_file(name): :rtype: ``unicode`` filepath """ - return wf().cachefile(name + '.pid') + return wf().cachefile(name + ".pid") def _process_exists(pid): @@ -96,7 +96,7 @@ def _job_pid(name): if not os.path.exists(pidfile): return - with open(pidfile, 'rb') as fp: + with open(pidfile, "rb") as fp: pid = int(fp.read()) if _process_exists(pid): @@ -120,8 +120,9 @@ def is_running(name): return False -def _background(pidfile, stdin='/dev/null', stdout='/dev/null', - stderr='/dev/null'): # pragma: no cover +def _background( + pidfile, stdin="/dev/null", stdout="/dev/null", stderr="/dev/null" +): # pragma: no cover """Fork the current process into a background daemon. :param pidfile: file to write PID of daemon process to. @@ -134,42 +135,43 @@ def _background(pidfile, stdin='/dev/null', stdout='/dev/null', :type stderr: filepath """ + def _fork_and_exit_parent(errmsg, wait=False, write=False): try: pid = os.fork() if pid > 0: if write: # write PID of child process to `pidfile` - tmp = pidfile + '.tmp' - with open(tmp, 'wb') as fp: + tmp = pidfile + ".tmp" + with open(tmp, "wb") as fp: fp.write(str(pid)) os.rename(tmp, pidfile) if wait: # wait for child process to exit os.waitpid(pid, 0) os._exit(0) except OSError as err: - _log().critical('%s: (%d) %s', errmsg, err.errno, err.strerror) + _log().critical("%s: (%d) %s", errmsg, err.errno, err.strerror) raise err # Do first fork and wait for second fork to finish. - _fork_and_exit_parent('fork #1 failed', wait=True) + _fork_and_exit_parent("fork #1 failed", wait=True) # Decouple from parent environment. os.chdir(wf().workflowdir) os.setsid() # Do second fork and write PID to pidfile. - _fork_and_exit_parent('fork #2 failed', write=True) + _fork_and_exit_parent("fork #2 failed", write=True) # Now I am a daemon! # Redirect standard file descriptors. - si = open(stdin, 'r', 0) - so = open(stdout, 'a+', 0) - se = open(stderr, 'a+', 0) - if hasattr(sys.stdin, 'fileno'): + si = open(stdin, "r", 0) + so = open(stdout, "a+", 0) + se = open(stderr, "a+", 0) + if hasattr(sys.stdin, "fileno"): os.dup2(si.fileno(), sys.stdin.fileno()) - if hasattr(sys.stdout, 'fileno'): + if hasattr(sys.stdout, "fileno"): os.dup2(so.fileno(), sys.stdout.fileno()) - if hasattr(sys.stderr, 'fileno'): + if hasattr(sys.stderr, "fileno"): os.dup2(se.fileno(), sys.stderr.fileno()) @@ -219,25 +221,25 @@ def run_in_background(name, args, **kwargs): """ if is_running(name): - _log().info('[%s] job already running', name) + _log().info("[%s] job already running", name) return argcache = _arg_cache(name) # Cache arguments - with open(argcache, 'wb') as fp: - pickle.dump({'args': args, 'kwargs': kwargs}, fp) - _log().debug('[%s] command cached: %s', name, argcache) + with open(argcache, "wb") as fp: + pickle.dump({"args": args, "kwargs": kwargs}, fp) + _log().debug("[%s] command cached: %s", name, argcache) # Call this script - cmd = ['/usr/bin/python', __file__, name] - _log().debug('[%s] passing job to background runner: %r', name, cmd) + cmd = ["/usr/bin/python", __file__, name] + _log().debug("[%s] passing job to background runner: %r", name, cmd) retcode = subprocess.call(cmd) if retcode: # pragma: no cover - _log().error('[%s] background runner failed with %d', name, retcode) + _log().error("[%s] background runner failed with %d", name, retcode) else: - _log().debug('[%s] background job started', name) + _log().debug("[%s] background job started", name) return retcode @@ -253,7 +255,7 @@ def main(wf): # pragma: no cover name = wf.args[0] argcache = _arg_cache(name) if not os.path.exists(argcache): - msg = '[{0}] command cache not found: {1}'.format(name, argcache) + msg = "[{0}] command cache not found: {1}".format(name, argcache) log.critical(msg) raise IOError(msg) @@ -262,29 +264,29 @@ def main(wf): # pragma: no cover _background(pidfile) # Load cached arguments - with open(argcache, 'rb') as fp: + with open(argcache, "rb") as fp: data = pickle.load(fp) # Cached arguments - args = data['args'] - kwargs = data['kwargs'] + args = data["args"] + kwargs = data["kwargs"] # Delete argument cache file os.unlink(argcache) try: # Run the command - log.debug('[%s] running command: %r', name, args) + log.debug("[%s] running command: %r", name, args) retcode = subprocess.call(args, **kwargs) if retcode: - log.error('[%s] command failed with status %d', name, retcode) + log.error("[%s] command failed with status %d", name, retcode) finally: os.unlink(pidfile) - log.debug('[%s] job complete', name) + log.debug("[%s] job complete", name) -if __name__ == '__main__': # pragma: no cover +if __name__ == "__main__": # pragma: no cover wf().run(main) diff --git a/workflow/notify.py b/workflow/notify.py index 28ec0b9..fe7dfa9 100644 --- a/workflow/notify.py +++ b/workflow/notify.py @@ -43,20 +43,20 @@ #: Available system sounds from System Preferences > Sound > Sound Effects SOUNDS = ( - 'Basso', - 'Blow', - 'Bottle', - 'Frog', - 'Funk', - 'Glass', - 'Hero', - 'Morse', - 'Ping', - 'Pop', - 'Purr', - 'Sosumi', - 'Submarine', - 'Tink', + "Basso", + "Blow", + "Bottle", + "Frog", + "Funk", + "Glass", + "Hero", + "Morse", + "Ping", + "Pop", + "Purr", + "Sosumi", + "Submarine", + "Tink", ) @@ -90,7 +90,7 @@ def notifier_program(): Returns: unicode: Path to Notify.app ``applet`` executable. """ - return wf().datafile('Notify.app/Contents/MacOS/applet') + return wf().datafile("Notify.app/Contents/MacOS/applet") def notifier_icon_path(): @@ -99,7 +99,7 @@ def notifier_icon_path(): Returns: unicode: Path to ``applet.icns`` within the app bundle. """ - return wf().datafile('Notify.app/Contents/Resources/applet.icns') + return wf().datafile("Notify.app/Contents/Resources/applet.icns") def install_notifier(): @@ -108,21 +108,21 @@ def install_notifier(): Changes the bundle ID of the installed app and gives it the workflow's icon. """ - archive = os.path.join(os.path.dirname(__file__), 'Notify.tgz') + archive = os.path.join(os.path.dirname(__file__), "Notify.tgz") destdir = wf().datadir - app_path = os.path.join(destdir, 'Notify.app') + app_path = os.path.join(destdir, "Notify.app") n = notifier_program() - log().debug('installing Notify.app to %r ...', destdir) + log().debug("installing Notify.app to %r ...", destdir) # z = zipfile.ZipFile(archive, 'r') # z.extractall(destdir) - tgz = tarfile.open(archive, 'r:gz') + tgz = tarfile.open(archive, "r:gz") tgz.extractall(destdir) if not os.path.exists(n): # pragma: nocover - raise RuntimeError('Notify.app could not be installed in ' + destdir) + raise RuntimeError("Notify.app could not be installed in " + destdir) # Replace applet icon icon = notifier_icon_path() - workflow_icon = wf().workflowfile('icon.png') + workflow_icon = wf().workflowfile("icon.png") if os.path.exists(icon): os.unlink(icon) @@ -142,11 +142,11 @@ def install_notifier(): ws.setIcon_forFile_options_(img, app_path, 0) # Change bundle ID of installed app - ip_path = os.path.join(app_path, 'Contents/Info.plist') - bundle_id = '{0}.{1}'.format(wf().bundleid, uuid.uuid4().hex) + ip_path = os.path.join(app_path, "Contents/Info.plist") + bundle_id = "{0}.{1}".format(wf().bundleid, uuid.uuid4().hex) data = plistlib.readPlist(ip_path) - log().debug('changing bundle ID to %r', bundle_id) - data['CFBundleIdentifier'] = bundle_id + log().debug("changing bundle ID to %r", bundle_id) + data["CFBundleIdentifier"] = bundle_id plistlib.writePlist(data, ip_path) @@ -172,7 +172,7 @@ def validate_sound(sound): return None -def notify(title='', text='', sound=None): +def notify(title="", text="", sound=None): """Post notification via Notify.app helper. Args: @@ -186,10 +186,10 @@ def notify(title='', text='', sound=None): Returns: bool: ``True`` if notification was posted, else ``False``. """ - if title == text == '': - raise ValueError('Empty notification') + if title == text == "": + raise ValueError("Empty notification") - sound = validate_sound(sound) or '' + sound = validate_sound(sound) or "" n = notifier_program() @@ -197,16 +197,16 @@ def notify(title='', text='', sound=None): install_notifier() env = os.environ.copy() - enc = 'utf-8' - env['NOTIFY_TITLE'] = title.encode(enc) - env['NOTIFY_MESSAGE'] = text.encode(enc) - env['NOTIFY_SOUND'] = sound.encode(enc) + enc = "utf-8" + env["NOTIFY_TITLE"] = title.encode(enc) + env["NOTIFY_MESSAGE"] = text.encode(enc) + env["NOTIFY_SOUND"] = sound.encode(enc) cmd = [n] retcode = subprocess.call(cmd, env=env) if retcode == 0: return True - log().error('Notify.app exited with status {0}.'.format(retcode)) + log().error("Notify.app exited with status {0}.".format(retcode)) return False @@ -221,17 +221,13 @@ def convert_image(inpath, outpath, size): Raises: RuntimeError: Raised if ``sips`` exits with non-zero status. """ - cmd = [ - b'sips', - b'-z', str(size), str(size), - inpath, - b'--out', outpath] + cmd = [b"sips", b"-z", str(size), str(size), inpath, b"--out", outpath] # log().debug(cmd) - with open(os.devnull, 'w') as pipe: + with open(os.devnull, "w") as pipe: retcode = subprocess.call(cmd, stdout=pipe, stderr=subprocess.STDOUT) if retcode != 0: - raise RuntimeError('sips exited with %d' % retcode) + raise RuntimeError("sips exited with %d" % retcode) def png_to_icns(png_path, icns_path): @@ -248,13 +244,13 @@ def png_to_icns(png_path, icns_path): Raises: RuntimeError: Raised if ``iconutil`` or ``sips`` fail. """ - tempdir = tempfile.mkdtemp(prefix='aw-', dir=wf().datadir) + tempdir = tempfile.mkdtemp(prefix="aw-", dir=wf().datadir) try: - iconset = os.path.join(tempdir, 'Icon.iconset') + iconset = os.path.join(tempdir, "Icon.iconset") if os.path.exists(iconset): # pragma: nocover - raise RuntimeError('iconset already exists: ' + iconset) + raise RuntimeError("iconset already exists: " + iconset) os.makedirs(iconset) @@ -262,11 +258,11 @@ def png_to_icns(png_path, icns_path): # sizes needed configs = [] for i in (16, 32, 128, 256, 512): - configs.append(('icon_{0}x{0}.png'.format(i), i)) - configs.append((('icon_{0}x{0}@2x.png'.format(i), i * 2))) + configs.append(("icon_{0}x{0}.png".format(i), i)) + configs.append((("icon_{0}x{0}@2x.png".format(i), i * 2))) - shutil.copy(png_path, os.path.join(iconset, 'icon_256x256.png')) - shutil.copy(png_path, os.path.join(iconset, 'icon_128x128@2x.png')) + shutil.copy(png_path, os.path.join(iconset, "icon_256x256.png")) + shutil.copy(png_path, os.path.join(iconset, "icon_128x128@2x.png")) for name, size in configs: outpath = os.path.join(iconset, name) @@ -274,19 +270,14 @@ def png_to_icns(png_path, icns_path): continue convert_image(png_path, outpath, size) - cmd = [ - b'iconutil', - b'-c', b'icns', - b'-o', icns_path, - iconset] + cmd = [b"iconutil", b"-c", b"icns", b"-o", icns_path, iconset] retcode = subprocess.call(cmd) if retcode != 0: - raise RuntimeError('iconset exited with %d' % retcode) + raise RuntimeError("iconset exited with %d" % retcode) if not os.path.exists(icns_path): # pragma: nocover - raise ValueError( - 'generated ICNS file not found: ' + repr(icns_path)) + raise ValueError("generated ICNS file not found: " + repr(icns_path)) finally: try: shutil.rmtree(tempdir) @@ -294,7 +285,7 @@ def png_to_icns(png_path, icns_path): pass -if __name__ == '__main__': # pragma: nocover +if __name__ == "__main__": # pragma: nocover # Simple command-line script to test module with # This won't work on 2.6, as `argparse` isn't available # by default. @@ -304,19 +295,20 @@ def png_to_icns(png_path, icns_path): def ustr(s): """Coerce `s` to normalised Unicode.""" - return normalize('NFD', s.decode('utf-8')) + return normalize("NFD", s.decode("utf-8")) p = argparse.ArgumentParser() - p.add_argument('-p', '--png', help="PNG image to convert to ICNS.") - p.add_argument('-l', '--list-sounds', help="Show available sounds.", - action='store_true') - p.add_argument('-t', '--title', - help="Notification title.", type=ustr, - default='') - p.add_argument('-s', '--sound', type=ustr, - help="Optional notification sound.", default='') - p.add_argument('text', type=ustr, - help="Notification body text.", default='', nargs='?') + p.add_argument("-p", "--png", help="PNG image to convert to ICNS.") + p.add_argument( + "-l", "--list-sounds", help="Show available sounds.", action="store_true" + ) + p.add_argument("-t", "--title", help="Notification title.", type=ustr, default="") + p.add_argument( + "-s", "--sound", type=ustr, help="Optional notification sound.", default="" + ) + p.add_argument( + "text", type=ustr, help="Notification body text.", default="", nargs="?" + ) o = p.parse_args() # List available sounds @@ -329,20 +321,20 @@ def ustr(s): if o.png: icns = os.path.join( os.path.dirname(o.png), - os.path.splitext(os.path.basename(o.png))[0] + '.icns') + os.path.splitext(os.path.basename(o.png))[0] + ".icns", + ) - print('converting {0!r} to {1!r} ...'.format(o.png, icns), - file=sys.stderr) + print("converting {0!r} to {1!r} ...".format(o.png, icns), file=sys.stderr) if os.path.exists(icns): - raise ValueError('destination file already exists: ' + icns) + raise ValueError("destination file already exists: " + icns) png_to_icns(o.png, icns) sys.exit(0) # Post notification - if o.title == o.text == '': - print('ERROR: empty notification.', file=sys.stderr) + if o.title == o.text == "": + print("ERROR: empty notification.", file=sys.stderr) sys.exit(1) else: notify(o.title, o.text, o.sound) diff --git a/workflow/update.py b/workflow/update.py index c039f7a..3cb0e5f 100644 --- a/workflow/update.py +++ b/workflow/update.py @@ -37,8 +37,8 @@ # __all__ = [] -RELEASES_BASE = 'https://api.github.com/repos/{}/releases' -match_workflow = re.compile(r'\.alfred(\d+)?workflow$').search +RELEASES_BASE = "https://api.github.com/repos/{}/releases" +match_workflow = re.compile(r"\.alfred(\d+)?workflow$").search _wf = None @@ -70,9 +70,12 @@ class Download(object): @classmethod def from_dict(cls, d): """Create a `Download` from a `dict`.""" - return cls(url=d['url'], filename=d['filename'], - version=Version(d['version']), - prerelease=d['prerelease']) + return cls( + url=d["url"], + filename=d["filename"], + version=Version(d["version"]), + prerelease=d["prerelease"], + ) @classmethod def from_releases(cls, js): @@ -95,34 +98,35 @@ def from_releases(cls, js): releases = json.loads(js) downloads = [] for release in releases: - tag = release['tag_name'] + tag = release["tag_name"] dupes = defaultdict(int) try: version = Version(tag) except ValueError as err: - wf().logger.debug('ignored release: bad version "%s": %s', - tag, err) + wf().logger.debug('ignored release: bad version "%s": %s', tag, err) continue dls = [] - for asset in release.get('assets', []): - url = asset.get('browser_download_url') + for asset in release.get("assets", []): + url = asset.get("browser_download_url") filename = os.path.basename(url) m = match_workflow(filename) if not m: - wf().logger.debug('unwanted file: %s', filename) + wf().logger.debug("unwanted file: %s", filename) continue ext = m.group(0) dupes[ext] = dupes[ext] + 1 - dls.append(Download(url, filename, version, - release['prerelease'])) + dls.append(Download(url, filename, version, release["prerelease"])) valid = True for ext, n in dupes.items(): if n > 1: - wf().logger.debug('ignored release "%s": multiple assets ' - 'with extension "%s"', tag, ext) + wf().logger.debug( + 'ignored release "%s": multiple assets ' 'with extension "%s"', + tag, + ext, + ) valid = False break @@ -156,23 +160,29 @@ def alfred_version(self): """Minimum Alfred version based on filename extension.""" m = match_workflow(self.filename) if not m or not m.group(1): - return Version('0') + return Version("0") return Version(m.group(1)) @property def dict(self): """Convert `Download` to `dict`.""" - return dict(url=self.url, filename=self.filename, - version=str(self.version), prerelease=self.prerelease) + return dict( + url=self.url, + filename=self.filename, + version=str(self.version), + prerelease=self.prerelease, + ) def __str__(self): """Format `Download` for printing.""" - u = ('Download(url={dl.url!r}, ' - 'filename={dl.filename!r}, ' - 'version={dl.version!r}, ' - 'prerelease={dl.prerelease!r})'.format(dl=self)) + u = ( + "Download(url={dl.url!r}, " + "filename={dl.filename!r}, " + "version={dl.version!r}, " + "prerelease={dl.prerelease!r})".format(dl=self) + ) - return u.encode('utf-8') + return u.encode("utf-8") def __repr__(self): """Code-like representation of `Download`.""" @@ -180,10 +190,12 @@ def __repr__(self): def __eq__(self, other): """Compare Downloads based on version numbers.""" - if self.url != other.url \ - or self.filename != other.filename \ - or self.version != other.version \ - or self.prerelease != other.prerelease: + if ( + self.url != other.url + or self.filename != other.filename + or self.version != other.version + or self.prerelease != other.prerelease + ): return False return True @@ -222,7 +234,7 @@ class Version(object): """ #: Match version and pre-release/build information in version strings - match_version = re.compile(r'([0-9][0-9\.]*)(.+)?').match + match_version = re.compile(r"([0-9][0-9\.]*)(.+)?").match def __init__(self, vstr): """Create new `Version` object. @@ -231,23 +243,23 @@ def __init__(self, vstr): vstr (basestring): Semantic version string. """ if not vstr: - raise ValueError('invalid version number: {!r}'.format(vstr)) + raise ValueError("invalid version number: {!r}".format(vstr)) self.vstr = vstr self.major = 0 self.minor = 0 self.patch = 0 - self.suffix = '' - self.build = '' + self.suffix = "" + self.build = "" self._parse(vstr) def _parse(self, vstr): - if vstr.startswith('v'): + if vstr.startswith("v"): m = self.match_version(vstr[1:]) else: m = self.match_version(vstr) if not m: - raise ValueError('invalid version number: ' + vstr) + raise ValueError("invalid version number: " + vstr) version, suffix = m.groups() parts = self._parse_dotted_string(version) @@ -257,24 +269,23 @@ def _parse(self, vstr): if len(parts): self.patch = parts.pop(0) if not len(parts) == 0: - raise ValueError('version number too long: ' + vstr) + raise ValueError("version number too long: " + vstr) if suffix: # Build info - idx = suffix.find('+') + idx = suffix.find("+") if idx > -1: - self.build = suffix[idx+1:] + self.build = suffix[idx + 1 :] suffix = suffix[:idx] if suffix: - if not suffix.startswith('-'): - raise ValueError( - 'suffix must start with - : ' + suffix) + if not suffix.startswith("-"): + raise ValueError("suffix must start with - : " + suffix) self.suffix = suffix[1:] def _parse_dotted_string(self, s): """Parse string ``s`` into list of ints and strings.""" parsed = [] - parts = s.split('.') + parts = s.split(".") for p in parts: if p.isdigit(): p = int(p) @@ -289,7 +300,7 @@ def tuple(self): def __lt__(self, other): """Implement comparison.""" if not isinstance(other, Version): - raise ValueError('not a Version instance: {0!r}'.format(other)) + raise ValueError("not a Version instance: {0!r}".format(other)) t = self.tuple[:3] o = other.tuple[:3] if t < o: @@ -299,15 +310,16 @@ def __lt__(self, other): return True if other.suffix and not self.suffix: return False - return self._parse_dotted_string(self.suffix) \ - < self._parse_dotted_string(other.suffix) + return self._parse_dotted_string(self.suffix) < self._parse_dotted_string( + other.suffix + ) # t > o return False def __eq__(self, other): """Implement comparison.""" if not isinstance(other, Version): - raise ValueError('not a Version instance: {0!r}'.format(other)) + raise ValueError("not a Version instance: {0!r}".format(other)) return self.tuple == other.tuple def __ne__(self, other): @@ -317,13 +329,13 @@ def __ne__(self, other): def __gt__(self, other): """Implement comparison.""" if not isinstance(other, Version): - raise ValueError('not a Version instance: {0!r}'.format(other)) + raise ValueError("not a Version instance: {0!r}".format(other)) return other.__lt__(self) def __le__(self, other): """Implement comparison.""" if not isinstance(other, Version): - raise ValueError('not a Version instance: {0!r}'.format(other)) + raise ValueError("not a Version instance: {0!r}".format(other)) return not other.__lt__(self) def __ge__(self, other): @@ -332,11 +344,11 @@ def __ge__(self, other): def __str__(self): """Return semantic version string.""" - vstr = '{0}.{1}.{2}'.format(self.major, self.minor, self.patch) + vstr = "{0}.{1}.{2}".format(self.major, self.minor, self.patch) if self.suffix: - vstr = '{0}-{1}'.format(vstr, self.suffix) + vstr = "{0}-{1}".format(vstr, self.suffix) if self.build: - vstr = '{0}+{1}'.format(vstr, self.build) + vstr = "{0}+{1}".format(vstr, self.build) return vstr def __repr__(self): @@ -357,11 +369,10 @@ def retrieve_download(dl): """ if not match_workflow(dl.filename): - raise ValueError('attachment not a workflow: ' + dl.filename) + raise ValueError("attachment not a workflow: " + dl.filename) path = os.path.join(tempfile.gettempdir(), dl.filename) - wf().logger.debug('downloading update from ' - '%r to %r ...', dl.url, path) + wf().logger.debug("downloading update from " "%r to %r ...", dl.url, path) r = web.get(dl.url) r.raise_for_status() @@ -381,8 +392,8 @@ def build_api_url(repo): unicode: URL to the API endpoint for the repo's releases """ - if len(repo.split('/')) != 2: - raise ValueError('invalid GitHub repo: {!r}'.format(repo)) + if len(repo.split("/")) != 2: + raise ValueError("invalid GitHub repo: {!r}".format(repo)) return RELEASES_BASE.format(repo) @@ -401,12 +412,12 @@ def get_downloads(repo): url = build_api_url(repo) def _fetch(): - wf().logger.info('retrieving releases for %r ...', repo) + wf().logger.info("retrieving releases for %r ...", repo) r = web.get(url) r.raise_for_status() return r.content - key = 'github-releases-' + repo.replace('/', '-') + key = "github-releases-" + repo.replace("/", "-") js = wf().cached_data(key, _fetch, max_age=60) return Download.from_releases(js) @@ -414,7 +425,7 @@ def _fetch(): def latest_download(dls, alfred_version=None, prereleases=False): """Return newest `Download`.""" - alfred_version = alfred_version or os.getenv('alfred_version') + alfred_version = alfred_version or os.getenv("alfred_version") version = None if alfred_version: version = Version(alfred_version) @@ -422,21 +433,24 @@ def latest_download(dls, alfred_version=None, prereleases=False): dls.sort(reverse=True) for dl in dls: if dl.prerelease and not prereleases: - wf().logger.debug('ignored prerelease: %s', dl.version) + wf().logger.debug("ignored prerelease: %s", dl.version) continue if version and dl.alfred_version > version: - wf().logger.debug('ignored incompatible (%s > %s): %s', - dl.alfred_version, version, dl.filename) + wf().logger.debug( + "ignored incompatible (%s > %s): %s", + dl.alfred_version, + version, + dl.filename, + ) continue - wf().logger.debug('latest version: %s (%s)', dl.version, dl.filename) + wf().logger.debug("latest version: %s (%s)", dl.version, dl.filename) return dl return None -def check_update(repo, current_version, prereleases=False, - alfred_version=None): +def check_update(repo, current_version, prereleases=False, alfred_version=None): """Check whether a newer release is available on GitHub. Args: @@ -454,38 +468,41 @@ def check_update(repo, current_version, prereleases=False, be cached. """ - key = '__workflow_latest_version' + key = "__workflow_latest_version" # data stored when no update is available no_update = { - 'available': False, - 'download': None, - 'version': None, + "available": False, + "download": None, + "version": None, } current = Version(current_version) dls = get_downloads(repo) if not len(dls): - wf().logger.warning('no valid downloads for %s', repo) + wf().logger.warning("no valid downloads for %s", repo) wf().cache_data(key, no_update) return False - wf().logger.info('%d download(s) for %s', len(dls), repo) + wf().logger.info("%d download(s) for %s", len(dls), repo) dl = latest_download(dls, alfred_version, prereleases) if not dl: - wf().logger.warning('no compatible downloads for %s', repo) + wf().logger.warning("no compatible downloads for %s", repo) wf().cache_data(key, no_update) return False - wf().logger.debug('latest=%r, installed=%r', dl.version, current) + wf().logger.debug("latest=%r, installed=%r", dl.version, current) if dl.version > current: - wf().cache_data(key, { - 'version': str(dl.version), - 'download': dl.dict, - 'available': True, - }) + wf().cache_data( + key, + { + "version": str(dl.version), + "download": dl.dict, + "available": True, + }, + ) return True wf().cache_data(key, no_update) @@ -498,50 +515,49 @@ def install_update(): :returns: ``True`` if an update is installed, else ``False`` """ - key = '__workflow_latest_version' + key = "__workflow_latest_version" # data stored when no update is available no_update = { - 'available': False, - 'download': None, - 'version': None, + "available": False, + "download": None, + "version": None, } status = wf().cached_data(key, max_age=0) - if not status or not status.get('available'): - wf().logger.info('no update available') + if not status or not status.get("available"): + wf().logger.info("no update available") return False - dl = status.get('download') + dl = status.get("download") if not dl: - wf().logger.info('no download information') + wf().logger.info("no download information") return False path = retrieve_download(Download.from_dict(dl)) - wf().logger.info('installing updated workflow ...') - subprocess.call(['open', path]) # nosec + wf().logger.info("installing updated workflow ...") + subprocess.call(["open", path]) # nosec wf().cache_data(key, no_update) return True -if __name__ == '__main__': # pragma: nocover +if __name__ == "__main__": # pragma: nocover import sys prereleases = False def show_help(status=0): """Print help message.""" - print('usage: update.py (check|install) ' - '[--prereleases] ') + print("usage: update.py (check|install) " "[--prereleases] ") sys.exit(status) argv = sys.argv[:] - if '-h' in argv or '--help' in argv: + if "-h" in argv or "--help" in argv: show_help() - if '--prereleases' in argv: - argv.remove('--prereleases') + if "--prereleases" in argv: + argv.remove("--prereleases") prereleases = True if len(argv) != 4: @@ -553,9 +569,9 @@ def show_help(status=0): try: - if action == 'check': + if action == "check": check_update(repo, version, prereleases) - elif action == 'install': + elif action == "install": install_update() else: show_help(1) diff --git a/workflow/util.py b/workflow/util.py index ab5e954..b606bda 100644 --- a/workflow/util.py +++ b/workflow/util.py @@ -31,28 +31,28 @@ # "com.runningwithcrayons.Alfred" depending on version. # # Open Alfred in search (regular) mode -JXA_SEARCH = 'Application({app}).search({arg});' +JXA_SEARCH = "Application({app}).search({arg});" # Open Alfred's File Actions on an argument -JXA_ACTION = 'Application({app}).action({arg});' +JXA_ACTION = "Application({app}).action({arg});" # Open Alfred's navigation mode at path -JXA_BROWSE = 'Application({app}).browse({arg});' +JXA_BROWSE = "Application({app}).browse({arg});" # Set the specified theme -JXA_SET_THEME = 'Application({app}).setTheme({arg});' +JXA_SET_THEME = "Application({app}).setTheme({arg});" # Call an External Trigger -JXA_TRIGGER = 'Application({app}).runTrigger({arg}, {opts});' +JXA_TRIGGER = "Application({app}).runTrigger({arg}, {opts});" # Save a variable to the workflow configuration sheet/info.plist -JXA_SET_CONFIG = 'Application({app}).setConfiguration({arg}, {opts});' +JXA_SET_CONFIG = "Application({app}).setConfiguration({arg}, {opts});" # Delete a variable from the workflow configuration sheet/info.plist -JXA_UNSET_CONFIG = 'Application({app}).removeConfiguration({arg}, {opts});' +JXA_UNSET_CONFIG = "Application({app}).removeConfiguration({arg}, {opts});" # Tell Alfred to reload a workflow from disk -JXA_RELOAD_WORKFLOW = 'Application({app}).reloadWorkflow({arg});' +JXA_RELOAD_WORKFLOW = "Application({app}).reloadWorkflow({arg});" class AcquisitionError(Exception): """Raised if a lock cannot be acquired.""" -AppInfo = namedtuple('AppInfo', ['name', 'path', 'bundleid']) +AppInfo = namedtuple("AppInfo", ["name", "path", "bundleid"]) """Information about an installed application. Returned by :func:`appinfo`. All attributes are Unicode. @@ -86,14 +86,14 @@ def jxa_app_name(): unicode: Application name or ID. """ - if os.getenv('alfred_version', '').startswith('3'): + if os.getenv("alfred_version", "").startswith("3"): # Alfred 3 - return u'Alfred 3' + return u"Alfred 3" # Alfred 4+ - return u'com.runningwithcrayons.Alfred' + return u"com.runningwithcrayons.Alfred" -def unicodify(s, encoding='utf-8', norm=None): +def unicodify(s, encoding="utf-8", norm=None): """Ensure string is Unicode. .. versionadded:: 1.31 @@ -115,6 +115,7 @@ def unicodify(s, encoding='utf-8', norm=None): if norm: from unicodedata import normalize + s = normalize(norm, s) return s @@ -139,7 +140,7 @@ def utf8ify(s): return s if isinstance(s, unicode): - return s.encode('utf-8') + return s.encode("utf-8") return str(s) @@ -204,17 +205,17 @@ def run_applescript(script, *args, **kwargs): str: Output of run command. """ - lang = 'AppleScript' - if 'lang' in kwargs: - lang = kwargs['lang'] - del kwargs['lang'] + lang = "AppleScript" + if "lang" in kwargs: + lang = kwargs["lang"] + del kwargs["lang"] - cmd = ['/usr/bin/osascript', '-l', lang] + cmd = ["/usr/bin/osascript", "-l", lang] if os.path.exists(script): cmd += [script] else: - cmd += ['-e', script] + cmd += ["-e", script] cmd.extend(args) @@ -236,7 +237,7 @@ def run_jxa(script, *args): str: Output of script. """ - return run_applescript(script, *args, lang='JavaScript') + return run_applescript(script, *args, lang="JavaScript") def run_trigger(name, bundleid=None, arg=None): @@ -253,17 +254,19 @@ def run_trigger(name, bundleid=None, arg=None): arg (str, optional): Argument to pass to trigger. """ - bundleid = bundleid or os.getenv('alfred_workflow_bundleid') + bundleid = bundleid or os.getenv("alfred_workflow_bundleid") appname = jxa_app_name() - opts = {'inWorkflow': bundleid} + opts = {"inWorkflow": bundleid} if arg: - opts['withArgument'] = arg + opts["withArgument"] = arg - script = JXA_TRIGGER.format(app=json.dumps(appname), - arg=json.dumps(name), - opts=json.dumps(opts, sort_keys=True)) + script = JXA_TRIGGER.format( + app=json.dumps(appname), + arg=json.dumps(name), + opts=json.dumps(opts, sort_keys=True), + ) - run_applescript(script, lang='JavaScript') + run_applescript(script, lang="JavaScript") def set_theme(theme_name): @@ -276,9 +279,8 @@ def set_theme(theme_name): """ appname = jxa_app_name() - script = JXA_SET_THEME.format(app=json.dumps(appname), - arg=json.dumps(theme_name)) - run_applescript(script, lang='JavaScript') + script = JXA_SET_THEME.format(app=json.dumps(appname), arg=json.dumps(theme_name)) + run_applescript(script, lang="JavaScript") def set_config(name, value, bundleid=None, exportable=False): @@ -297,19 +299,21 @@ def set_config(name, value, bundleid=None, exportable=False): as exportable (Don't Export checkbox). """ - bundleid = bundleid or os.getenv('alfred_workflow_bundleid') + bundleid = bundleid or os.getenv("alfred_workflow_bundleid") appname = jxa_app_name() opts = { - 'toValue': value, - 'inWorkflow': bundleid, - 'exportable': exportable, + "toValue": value, + "inWorkflow": bundleid, + "exportable": exportable, } - script = JXA_SET_CONFIG.format(app=json.dumps(appname), - arg=json.dumps(name), - opts=json.dumps(opts, sort_keys=True)) + script = JXA_SET_CONFIG.format( + app=json.dumps(appname), + arg=json.dumps(name), + opts=json.dumps(opts, sort_keys=True), + ) - run_applescript(script, lang='JavaScript') + run_applescript(script, lang="JavaScript") def unset_config(name, bundleid=None): @@ -325,15 +329,17 @@ def unset_config(name, bundleid=None): bundleid (str, optional): Bundle ID of workflow variable belongs to. """ - bundleid = bundleid or os.getenv('alfred_workflow_bundleid') + bundleid = bundleid or os.getenv("alfred_workflow_bundleid") appname = jxa_app_name() - opts = {'inWorkflow': bundleid} + opts = {"inWorkflow": bundleid} - script = JXA_UNSET_CONFIG.format(app=json.dumps(appname), - arg=json.dumps(name), - opts=json.dumps(opts, sort_keys=True)) + script = JXA_UNSET_CONFIG.format( + app=json.dumps(appname), + arg=json.dumps(name), + opts=json.dumps(opts, sort_keys=True), + ) - run_applescript(script, lang='JavaScript') + run_applescript(script, lang="JavaScript") def search_in_alfred(query=None): @@ -347,10 +353,10 @@ def search_in_alfred(query=None): query (unicode, optional): Search query. """ - query = query or u'' + query = query or u"" appname = jxa_app_name() script = JXA_SEARCH.format(app=json.dumps(appname), arg=json.dumps(query)) - run_applescript(script, lang='JavaScript') + run_applescript(script, lang="JavaScript") def browse_in_alfred(path): @@ -364,7 +370,7 @@ def browse_in_alfred(path): """ appname = jxa_app_name() script = JXA_BROWSE.format(app=json.dumps(appname), arg=json.dumps(path)) - run_applescript(script, lang='JavaScript') + run_applescript(script, lang="JavaScript") def action_in_alfred(paths): @@ -378,7 +384,7 @@ def action_in_alfred(paths): """ appname = jxa_app_name() script = JXA_ACTION.format(app=json.dumps(appname), arg=json.dumps(paths)) - run_applescript(script, lang='JavaScript') + run_applescript(script, lang="JavaScript") def reload_workflow(bundleid=None): @@ -393,12 +399,13 @@ def reload_workflow(bundleid=None): bundleid (unicode, optional): Bundle ID of workflow to reload. """ - bundleid = bundleid or os.getenv('alfred_workflow_bundleid') + bundleid = bundleid or os.getenv("alfred_workflow_bundleid") appname = jxa_app_name() - script = JXA_RELOAD_WORKFLOW.format(app=json.dumps(appname), - arg=json.dumps(bundleid)) + script = JXA_RELOAD_WORKFLOW.format( + app=json.dumps(appname), arg=json.dumps(bundleid) + ) - run_applescript(script, lang='JavaScript') + run_applescript(script, lang="JavaScript") def appinfo(name): @@ -414,22 +421,24 @@ def appinfo(name): """ cmd = [ - 'mdfind', - '-onlyin', '/Applications', - '-onlyin', '/System/Applications', - '-onlyin', os.path.expanduser('~/Applications'), - '(kMDItemContentTypeTree == com.apple.application &&' - '(kMDItemDisplayName == "{0}" || kMDItemFSName == "{0}.app"))' - .format(name) + "mdfind", + "-onlyin", + "/Applications", + "-onlyin", + "/System/Applications", + "-onlyin", + os.path.expanduser("~/Applications"), + "(kMDItemContentTypeTree == com.apple.application &&" + '(kMDItemDisplayName == "{0}" || kMDItemFSName == "{0}.app"))'.format(name), ] output = run_command(cmd).strip() if not output: return None - path = output.split('\n')[0] + path = output.split("\n")[0] - cmd = ['mdls', '-raw', '-name', 'kMDItemCFBundleIdentifier', path] + cmd = ["mdls", "-raw", "-name", "kMDItemCFBundleIdentifier", path] bid = run_command(cmd).strip() if not bid: # pragma: no cover return None @@ -452,7 +461,7 @@ def atomic_writer(fpath, mode): :type mode: string """ - suffix = '.{}.tmp'.format(os.getpid()) + suffix = ".{}.tmp".format(os.getpid()) temppath = fpath + suffix with open(temppath, mode) as fp: try: @@ -496,7 +505,7 @@ class LockFile(object): def __init__(self, protected_path, timeout=0.0, delay=0.05): """Create new :class:`LockFile` object.""" - self.lockfile = protected_path + '.lock' + self.lockfile = protected_path + ".lock" self._lockfile = None self.timeout = timeout self.delay = delay @@ -525,7 +534,7 @@ def acquire(self, blocking=True): while True: # Raise error if we've been waiting too long to acquire the lock if self.timeout and (time.time() - start) >= self.timeout: - raise AcquisitionError('lock acquisition timed out') + raise AcquisitionError("lock acquisition timed out") # If already locked, wait then try again if self.locked: @@ -534,7 +543,7 @@ def acquire(self, blocking=True): # Create in append mode so we don't lose any contents if self._lockfile is None: - self._lockfile = open(self.lockfile, 'a') + self._lockfile = open(self.lockfile, "a") # Try to acquire the lock try: @@ -608,7 +617,7 @@ class uninterruptible(object): """ - def __init__(self, func, class_name=''): + def __init__(self, func, class_name=""): """Decorate `func`.""" self.func = func functools.update_wrapper(self, func) @@ -640,5 +649,4 @@ def __call__(self, *args, **kwargs): def __get__(self, obj=None, klass=None): """Decorator API.""" - return self.__class__(self.func.__get__(obj, klass), - klass.__name__) + return self.__class__(self.func.__get__(obj, klass), klass.__name__) diff --git a/workflow/web.py b/workflow/web.py index 83212a8..0fa022a 100644 --- a/workflow/web.py +++ b/workflow/web.py @@ -25,56 +25,57 @@ import urlparse import zlib -__version__ = open(os.path.join(os.path.dirname(__file__), 'version')).read() +__version__ = open(os.path.join(os.path.dirname(__file__), "version")).read() -USER_AGENT = (u'Alfred-Workflow/' + __version__ + - ' (+http://www.deanishe.net/alfred-workflow)') +USER_AGENT = ( + u"Alfred-Workflow/" + __version__ + " (+http://www.deanishe.net/alfred-workflow)" +) # Valid characters for multipart form data boundaries BOUNDARY_CHARS = string.digits + string.ascii_letters # HTTP response codes RESPONSES = { - 100: 'Continue', - 101: 'Switching Protocols', - 200: 'OK', - 201: 'Created', - 202: 'Accepted', - 203: 'Non-Authoritative Information', - 204: 'No Content', - 205: 'Reset Content', - 206: 'Partial Content', - 300: 'Multiple Choices', - 301: 'Moved Permanently', - 302: 'Found', - 303: 'See Other', - 304: 'Not Modified', - 305: 'Use Proxy', - 307: 'Temporary Redirect', - 400: 'Bad Request', - 401: 'Unauthorized', - 402: 'Payment Required', - 403: 'Forbidden', - 404: 'Not Found', - 405: 'Method Not Allowed', - 406: 'Not Acceptable', - 407: 'Proxy Authentication Required', - 408: 'Request Timeout', - 409: 'Conflict', - 410: 'Gone', - 411: 'Length Required', - 412: 'Precondition Failed', - 413: 'Request Entity Too Large', - 414: 'Request-URI Too Long', - 415: 'Unsupported Media Type', - 416: 'Requested Range Not Satisfiable', - 417: 'Expectation Failed', - 500: 'Internal Server Error', - 501: 'Not Implemented', - 502: 'Bad Gateway', - 503: 'Service Unavailable', - 504: 'Gateway Timeout', - 505: 'HTTP Version Not Supported' + 100: "Continue", + 101: "Switching Protocols", + 200: "OK", + 201: "Created", + 202: "Accepted", + 203: "Non-Authoritative Information", + 204: "No Content", + 205: "Reset Content", + 206: "Partial Content", + 300: "Multiple Choices", + 301: "Moved Permanently", + 302: "Found", + 303: "See Other", + 304: "Not Modified", + 305: "Use Proxy", + 307: "Temporary Redirect", + 400: "Bad Request", + 401: "Unauthorized", + 402: "Payment Required", + 403: "Forbidden", + 404: "Not Found", + 405: "Method Not Allowed", + 406: "Not Acceptable", + 407: "Proxy Authentication Required", + 408: "Request Timeout", + 409: "Conflict", + 410: "Gone", + 411: "Length Required", + 412: "Precondition Failed", + 413: "Request Entity Too Large", + 414: "Request-URI Too Long", + 415: "Unsupported Media Type", + 416: "Requested Range Not Satisfiable", + 417: "Expectation Failed", + 500: "Internal Server Error", + 501: "Not Implemented", + 502: "Bad Gateway", + 503: "Service Unavailable", + 504: "Gateway Timeout", + 505: "HTTP Version Not Supported", } @@ -93,9 +94,9 @@ def str_dict(dic): dic2 = {} for k, v in dic.items(): if isinstance(k, unicode): - k = k.encode('utf-8') + k = k.encode("utf-8") if isinstance(v, unicode): - v = v.encode('utf-8') + v = v.encode("utf-8") dic2[k] = v return dic2 @@ -135,10 +136,10 @@ def __contains__(self, key): return dict.__contains__(self, key.lower()) def __getitem__(self, key): - return dict.__getitem__(self, key.lower())['val'] + return dict.__getitem__(self, key.lower())["val"] def __setitem__(self, key, value): - return dict.__setitem__(self, key.lower(), {'key': key, 'val': value}) + return dict.__setitem__(self, key.lower(), {"key": key, "val": value}) def get(self, key, default=None): """Return value for case-insensitive key or default.""" @@ -147,7 +148,7 @@ def get(self, key, default=None): except KeyError: return default else: - return v['val'] + return v["val"] def update(self, other): """Update values from other ``dict``.""" @@ -156,30 +157,30 @@ def update(self, other): def items(self): """Return ``(key, value)`` pairs.""" - return [(v['key'], v['val']) for v in dict.itervalues(self)] + return [(v["key"], v["val"]) for v in dict.itervalues(self)] def keys(self): """Return original keys.""" - return [v['key'] for v in dict.itervalues(self)] + return [v["key"] for v in dict.itervalues(self)] def values(self): """Return all values.""" - return [v['val'] for v in dict.itervalues(self)] + return [v["val"] for v in dict.itervalues(self)] def iteritems(self): """Iterate over ``(key, value)`` pairs.""" for v in dict.itervalues(self): - yield v['key'], v['val'] + yield v["key"], v["val"] def iterkeys(self): """Iterate over original keys.""" for v in dict.itervalues(self): - yield v['key'] + yield v["key"] def itervalues(self): """Interate over values.""" for v in dict.itervalues(self): - yield v['val'] + yield v["val"] class Request(urllib2.Request): @@ -187,7 +188,7 @@ class Request(urllib2.Request): def __init__(self, *args, **kwargs): """Create a new :class:`Request`.""" - self._method = kwargs.pop('method', None) + self._method = kwargs.pop("method", None) urllib2.Request.__init__(self, *args, **kwargs) def get_method(self): @@ -265,8 +266,9 @@ def __init__(self, request, stream=False): # Transfer-Encoding appears to not be used in the wild # (contrary to the HTTP standard), but no harm in testing # for it - if 'gzip' in headers.get('content-encoding', '') or \ - 'gzip' in headers.get('transfer-encoding', ''): + if "gzip" in headers.get("content-encoding", "") or "gzip" in headers.get( + "transfer-encoding", "" + ): self._gzipped = True @property @@ -282,8 +284,7 @@ def stream(self): @stream.setter def stream(self, value): if self._content_loaded: - raise RuntimeError("`content` has already been read from " - "this Response.") + raise RuntimeError("`content` has already been read from " "this Response.") self._stream = value @@ -294,7 +295,7 @@ def json(self): :rtype: list, dict or unicode """ - return json.loads(self.content, self.encoding or 'utf-8') + return json.loads(self.content, self.encoding or "utf-8") @property def encoding(self): @@ -343,8 +344,7 @@ def text(self): """ if self.encoding: - return unicodedata.normalize('NFC', unicode(self.content, - self.encoding)) + return unicodedata.normalize("NFC", unicode(self.content, self.encoding)) return self.content def iter_content(self, chunk_size=4096, decode_unicode=False): @@ -360,23 +360,24 @@ def iter_content(self, chunk_size=4096, decode_unicode=False): """ if not self.stream: - raise RuntimeError("You cannot call `iter_content` on a " - "Response unless you passed `stream=True`" - " to `get()`/`post()`/`request()`.") + raise RuntimeError( + "You cannot call `iter_content` on a " + "Response unless you passed `stream=True`" + " to `get()`/`post()`/`request()`." + ) if self._content_loaded: - raise RuntimeError( - "`content` has already been read from this Response.") + raise RuntimeError("`content` has already been read from this Response.") def decode_stream(iterator, r): - dec = codecs.getincrementaldecoder(r.encoding)(errors='replace') + dec = codecs.getincrementaldecoder(r.encoding)(errors="replace") for chunk in iterator: data = dec.decode(chunk) if data: yield data - data = dec.decode(b'', final=True) + data = dec.decode(b"", final=True) if data: # pragma: no cover yield data @@ -416,7 +417,7 @@ def save_to_path(self, filepath): self.stream = True - with open(filepath, 'wb') as fileobj: + with open(filepath, "wb") as fileobj: for data in self.iter_content(): fileobj.write(data) @@ -439,39 +440,40 @@ def _get_encoding(self): headers = self.raw.info() encoding = None - if headers.getparam('charset'): - encoding = headers.getparam('charset') + if headers.getparam("charset"): + encoding = headers.getparam("charset") # HTTP Content-Type header for param in headers.getplist(): - if param.startswith('charset='): + if param.startswith("charset="): encoding = param[8:] break if not self.stream: # Try sniffing response content # Encoding declared in document should override HTTP headers - if self.mimetype == 'text/html': # sniff HTML headers - m = re.search(r"""""", - self.content) + if self.mimetype == "text/html": # sniff HTML headers + m = re.search(r"""""", self.content) if m: encoding = m.group(1) - elif ((self.mimetype.startswith('application/') - or self.mimetype.startswith('text/')) - and 'xml' in self.mimetype): - m = re.search(r"""]*\?>""", - self.content) + elif ( + self.mimetype.startswith("application/") + or self.mimetype.startswith("text/") + ) and "xml" in self.mimetype: + m = re.search( + r"""]*\?>""", self.content + ) if m: encoding = m.group(1) # Format defaults - if self.mimetype == 'application/json' and not encoding: + if self.mimetype == "application/json" and not encoding: # The default encoding for JSON - encoding = 'utf-8' + encoding = "utf-8" - elif self.mimetype == 'application/xml' and not encoding: + elif self.mimetype == "application/xml" and not encoding: # The default for 'application/xml' - encoding = 'utf-8' + encoding = "utf-8" if encoding: encoding = encoding.lower() @@ -479,9 +481,19 @@ def _get_encoding(self): return encoding -def request(method, url, params=None, data=None, headers=None, cookies=None, - files=None, auth=None, timeout=60, allow_redirects=False, - stream=False): +def request( + method, + url, + params=None, + data=None, + headers=None, + cookies=None, + files=None, + auth=None, + timeout=60, + allow_redirects=False, + stream=False, +): """Initiate an HTTP(S) request. Returns :class:`Response` object. :param method: 'GET' or 'POST' @@ -549,16 +561,15 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, else: headers = CaseInsensitiveDictionary(headers) - if 'user-agent' not in headers: - headers['user-agent'] = USER_AGENT + if "user-agent" not in headers: + headers["user-agent"] = USER_AGENT # Accept gzip-encoded content - encodings = [s.strip() for s in - headers.get('accept-encoding', '').split(',')] - if 'gzip' not in encodings: - encodings.append('gzip') + encodings = [s.strip() for s in headers.get("accept-encoding", "").split(",")] + if "gzip" not in encodings: + encodings.append("gzip") - headers['accept-encoding'] = ', '.join(encodings) + headers["accept-encoding"] = ", ".join(encodings) if files: if not data: @@ -572,7 +583,7 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, headers = str_dict(headers) if isinstance(url, unicode): - url = url.encode('utf-8') + url = url.encode("utf-8") if params: # GET args (POST args are handled in encode_multipart_formdata) @@ -591,50 +602,126 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, return Response(req, stream) -def get(url, params=None, headers=None, cookies=None, auth=None, - timeout=60, allow_redirects=True, stream=False): +def get( + url, + params=None, + headers=None, + cookies=None, + auth=None, + timeout=60, + allow_redirects=True, + stream=False, +): """Initiate a GET request. Arguments as for :func:`request`. :returns: :class:`Response` instance """ - return request('GET', url, params, headers=headers, cookies=cookies, - auth=auth, timeout=timeout, allow_redirects=allow_redirects, - stream=stream) - - -def delete(url, params=None, data=None, headers=None, cookies=None, auth=None, - timeout=60, allow_redirects=True, stream=False): + return request( + "GET", + url, + params, + headers=headers, + cookies=cookies, + auth=auth, + timeout=timeout, + allow_redirects=allow_redirects, + stream=stream, + ) + + +def delete( + url, + params=None, + data=None, + headers=None, + cookies=None, + auth=None, + timeout=60, + allow_redirects=True, + stream=False, +): """Initiate a DELETE request. Arguments as for :func:`request`. :returns: :class:`Response` instance """ - return request('DELETE', url, params, data, headers=headers, - cookies=cookies, auth=auth, timeout=timeout, - allow_redirects=allow_redirects, stream=stream) - - -def post(url, params=None, data=None, headers=None, cookies=None, files=None, - auth=None, timeout=60, allow_redirects=False, stream=False): + return request( + "DELETE", + url, + params, + data, + headers=headers, + cookies=cookies, + auth=auth, + timeout=timeout, + allow_redirects=allow_redirects, + stream=stream, + ) + + +def post( + url, + params=None, + data=None, + headers=None, + cookies=None, + files=None, + auth=None, + timeout=60, + allow_redirects=False, + stream=False, +): """Initiate a POST request. Arguments as for :func:`request`. :returns: :class:`Response` instance """ - return request('POST', url, params, data, headers, cookies, files, auth, - timeout, allow_redirects, stream) - - -def put(url, params=None, data=None, headers=None, cookies=None, files=None, - auth=None, timeout=60, allow_redirects=False, stream=False): + return request( + "POST", + url, + params, + data, + headers, + cookies, + files, + auth, + timeout, + allow_redirects, + stream, + ) + + +def put( + url, + params=None, + data=None, + headers=None, + cookies=None, + files=None, + auth=None, + timeout=60, + allow_redirects=False, + stream=False, +): """Initiate a PUT request. Arguments as for :func:`request`. :returns: :class:`Response` instance """ - return request('PUT', url, params, data, headers, cookies, files, auth, - timeout, allow_redirects, stream) + return request( + "PUT", + url, + params, + data, + headers, + cookies, + files, + auth, + timeout, + allow_redirects, + stream, + ) def encode_multipart_formdata(fields, files): @@ -662,6 +749,7 @@ def encode_multipart_formdata(fields, files): will be used. """ + def get_content_type(filename): """Return or guess mimetype of ``filename``. @@ -671,50 +759,51 @@ def get_content_type(filename): :rtype: str """ - return mimetypes.guess_type(filename)[0] or 'application/octet-stream' + return mimetypes.guess_type(filename)[0] or "application/octet-stream" - boundary = '-----' + ''.join(random.choice(BOUNDARY_CHARS) - for i in range(30)) - CRLF = '\r\n' + boundary = "-----" + "".join(random.choice(BOUNDARY_CHARS) for i in range(30)) + CRLF = "\r\n" output = [] # Normal form fields for (name, value) in fields.items(): if isinstance(name, unicode): - name = name.encode('utf-8') + name = name.encode("utf-8") if isinstance(value, unicode): - value = value.encode('utf-8') - output.append('--' + boundary) + value = value.encode("utf-8") + output.append("--" + boundary) output.append('Content-Disposition: form-data; name="%s"' % name) - output.append('') + output.append("") output.append(value) # Files to upload for name, d in files.items(): - filename = d[u'filename'] - content = d[u'content'] - if u'mimetype' in d: - mimetype = d[u'mimetype'] + filename = d[u"filename"] + content = d[u"content"] + if u"mimetype" in d: + mimetype = d[u"mimetype"] else: mimetype = get_content_type(filename) if isinstance(name, unicode): - name = name.encode('utf-8') + name = name.encode("utf-8") if isinstance(filename, unicode): - filename = filename.encode('utf-8') + filename = filename.encode("utf-8") if isinstance(mimetype, unicode): - mimetype = mimetype.encode('utf-8') - output.append('--' + boundary) - output.append('Content-Disposition: form-data; ' - 'name="%s"; filename="%s"' % (name, filename)) - output.append('Content-Type: %s' % mimetype) - output.append('') + mimetype = mimetype.encode("utf-8") + output.append("--" + boundary) + output.append( + "Content-Disposition: form-data; " + 'name="%s"; filename="%s"' % (name, filename) + ) + output.append("Content-Type: %s" % mimetype) + output.append("") output.append(content) - output.append('--' + boundary + '--') - output.append('') + output.append("--" + boundary + "--") + output.append("") body = CRLF.join(output) headers = { - 'Content-Type': 'multipart/form-data; boundary=%s' % boundary, - 'Content-Length': str(len(body)), + "Content-Type": "multipart/form-data; boundary=%s" % boundary, + "Content-Length": str(len(body)), } return (headers, body) diff --git a/workflow/workflow.py b/workflow/workflow.py index 3935227..a00baeb 100644 --- a/workflow/workflow.py +++ b/workflow/workflow.py @@ -68,32 +68,32 @@ # The system icons are all in this directory. There are many more than # are listed here -ICON_ROOT = '/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources' +ICON_ROOT = "/System/Library/CoreServices/CoreTypes.bundle/Contents/Resources" -ICON_ACCOUNT = os.path.join(ICON_ROOT, 'Accounts.icns') -ICON_BURN = os.path.join(ICON_ROOT, 'BurningIcon.icns') -ICON_CLOCK = os.path.join(ICON_ROOT, 'Clock.icns') -ICON_COLOR = os.path.join(ICON_ROOT, 'ProfileBackgroundColor.icns') +ICON_ACCOUNT = os.path.join(ICON_ROOT, "Accounts.icns") +ICON_BURN = os.path.join(ICON_ROOT, "BurningIcon.icns") +ICON_CLOCK = os.path.join(ICON_ROOT, "Clock.icns") +ICON_COLOR = os.path.join(ICON_ROOT, "ProfileBackgroundColor.icns") ICON_COLOUR = ICON_COLOR # Queen's English, if you please -ICON_EJECT = os.path.join(ICON_ROOT, 'EjectMediaIcon.icns') +ICON_EJECT = os.path.join(ICON_ROOT, "EjectMediaIcon.icns") # Shown when a workflow throws an error -ICON_ERROR = os.path.join(ICON_ROOT, 'AlertStopIcon.icns') -ICON_FAVORITE = os.path.join(ICON_ROOT, 'ToolbarFavoritesIcon.icns') +ICON_ERROR = os.path.join(ICON_ROOT, "AlertStopIcon.icns") +ICON_FAVORITE = os.path.join(ICON_ROOT, "ToolbarFavoritesIcon.icns") ICON_FAVOURITE = ICON_FAVORITE -ICON_GROUP = os.path.join(ICON_ROOT, 'GroupIcon.icns') -ICON_HELP = os.path.join(ICON_ROOT, 'HelpIcon.icns') -ICON_HOME = os.path.join(ICON_ROOT, 'HomeFolderIcon.icns') -ICON_INFO = os.path.join(ICON_ROOT, 'ToolbarInfo.icns') -ICON_NETWORK = os.path.join(ICON_ROOT, 'GenericNetworkIcon.icns') -ICON_NOTE = os.path.join(ICON_ROOT, 'AlertNoteIcon.icns') -ICON_SETTINGS = os.path.join(ICON_ROOT, 'ToolbarAdvanced.icns') -ICON_SWIRL = os.path.join(ICON_ROOT, 'ErasingIcon.icns') -ICON_SWITCH = os.path.join(ICON_ROOT, 'General.icns') -ICON_SYNC = os.path.join(ICON_ROOT, 'Sync.icns') -ICON_TRASH = os.path.join(ICON_ROOT, 'TrashIcon.icns') -ICON_USER = os.path.join(ICON_ROOT, 'UserIcon.icns') -ICON_WARNING = os.path.join(ICON_ROOT, 'AlertCautionIcon.icns') -ICON_WEB = os.path.join(ICON_ROOT, 'BookmarkIcon.icns') +ICON_GROUP = os.path.join(ICON_ROOT, "GroupIcon.icns") +ICON_HELP = os.path.join(ICON_ROOT, "HelpIcon.icns") +ICON_HOME = os.path.join(ICON_ROOT, "HomeFolderIcon.icns") +ICON_INFO = os.path.join(ICON_ROOT, "ToolbarInfo.icns") +ICON_NETWORK = os.path.join(ICON_ROOT, "GenericNetworkIcon.icns") +ICON_NOTE = os.path.join(ICON_ROOT, "AlertNoteIcon.icns") +ICON_SETTINGS = os.path.join(ICON_ROOT, "ToolbarAdvanced.icns") +ICON_SWIRL = os.path.join(ICON_ROOT, "ErasingIcon.icns") +ICON_SWITCH = os.path.join(ICON_ROOT, "General.icns") +ICON_SYNC = os.path.join(ICON_ROOT, "Sync.icns") +ICON_TRASH = os.path.join(ICON_ROOT, "TrashIcon.icns") +ICON_USER = os.path.join(ICON_ROOT, "UserIcon.icns") +ICON_WARNING = os.path.join(ICON_ROOT, "AlertCautionIcon.icns") +ICON_WEB = os.path.join(ICON_ROOT, "BookmarkIcon.icns") #################################################################### # non-ASCII to ASCII diacritic folding. @@ -101,241 +101,241 @@ #################################################################### ASCII_REPLACEMENTS = { - 'À': 'A', - 'Á': 'A', - 'Â': 'A', - 'Ã': 'A', - 'Ä': 'A', - 'Å': 'A', - 'Æ': 'AE', - 'Ç': 'C', - 'È': 'E', - 'É': 'E', - 'Ê': 'E', - 'Ë': 'E', - 'Ì': 'I', - 'Í': 'I', - 'Î': 'I', - 'Ï': 'I', - 'Ð': 'D', - 'Ñ': 'N', - 'Ò': 'O', - 'Ó': 'O', - 'Ô': 'O', - 'Õ': 'O', - 'Ö': 'O', - 'Ø': 'O', - 'Ù': 'U', - 'Ú': 'U', - 'Û': 'U', - 'Ü': 'U', - 'Ý': 'Y', - 'Þ': 'Th', - 'ß': 'ss', - 'à': 'a', - 'á': 'a', - 'â': 'a', - 'ã': 'a', - 'ä': 'a', - 'å': 'a', - 'æ': 'ae', - 'ç': 'c', - 'è': 'e', - 'é': 'e', - 'ê': 'e', - 'ë': 'e', - 'ì': 'i', - 'í': 'i', - 'î': 'i', - 'ï': 'i', - 'ð': 'd', - 'ñ': 'n', - 'ò': 'o', - 'ó': 'o', - 'ô': 'o', - 'õ': 'o', - 'ö': 'o', - 'ø': 'o', - 'ù': 'u', - 'ú': 'u', - 'û': 'u', - 'ü': 'u', - 'ý': 'y', - 'þ': 'th', - 'ÿ': 'y', - 'Ł': 'L', - 'ł': 'l', - 'Ń': 'N', - 'ń': 'n', - 'Ņ': 'N', - 'ņ': 'n', - 'Ň': 'N', - 'ň': 'n', - 'Ŋ': 'ng', - 'ŋ': 'NG', - 'Ō': 'O', - 'ō': 'o', - 'Ŏ': 'O', - 'ŏ': 'o', - 'Ő': 'O', - 'ő': 'o', - 'Œ': 'OE', - 'œ': 'oe', - 'Ŕ': 'R', - 'ŕ': 'r', - 'Ŗ': 'R', - 'ŗ': 'r', - 'Ř': 'R', - 'ř': 'r', - 'Ś': 'S', - 'ś': 's', - 'Ŝ': 'S', - 'ŝ': 's', - 'Ş': 'S', - 'ş': 's', - 'Š': 'S', - 'š': 's', - 'Ţ': 'T', - 'ţ': 't', - 'Ť': 'T', - 'ť': 't', - 'Ŧ': 'T', - 'ŧ': 't', - 'Ũ': 'U', - 'ũ': 'u', - 'Ū': 'U', - 'ū': 'u', - 'Ŭ': 'U', - 'ŭ': 'u', - 'Ů': 'U', - 'ů': 'u', - 'Ű': 'U', - 'ű': 'u', - 'Ŵ': 'W', - 'ŵ': 'w', - 'Ŷ': 'Y', - 'ŷ': 'y', - 'Ÿ': 'Y', - 'Ź': 'Z', - 'ź': 'z', - 'Ż': 'Z', - 'ż': 'z', - 'Ž': 'Z', - 'ž': 'z', - 'ſ': 's', - 'Α': 'A', - 'Β': 'B', - 'Γ': 'G', - 'Δ': 'D', - 'Ε': 'E', - 'Ζ': 'Z', - 'Η': 'E', - 'Θ': 'Th', - 'Ι': 'I', - 'Κ': 'K', - 'Λ': 'L', - 'Μ': 'M', - 'Ν': 'N', - 'Ξ': 'Ks', - 'Ο': 'O', - 'Π': 'P', - 'Ρ': 'R', - 'Σ': 'S', - 'Τ': 'T', - 'Υ': 'U', - 'Φ': 'Ph', - 'Χ': 'Kh', - 'Ψ': 'Ps', - 'Ω': 'O', - 'α': 'a', - 'β': 'b', - 'γ': 'g', - 'δ': 'd', - 'ε': 'e', - 'ζ': 'z', - 'η': 'e', - 'θ': 'th', - 'ι': 'i', - 'κ': 'k', - 'λ': 'l', - 'μ': 'm', - 'ν': 'n', - 'ξ': 'x', - 'ο': 'o', - 'π': 'p', - 'ρ': 'r', - 'ς': 's', - 'σ': 's', - 'τ': 't', - 'υ': 'u', - 'φ': 'ph', - 'χ': 'kh', - 'ψ': 'ps', - 'ω': 'o', - 'А': 'A', - 'Б': 'B', - 'В': 'V', - 'Г': 'G', - 'Д': 'D', - 'Е': 'E', - 'Ж': 'Zh', - 'З': 'Z', - 'И': 'I', - 'Й': 'I', - 'К': 'K', - 'Л': 'L', - 'М': 'M', - 'Н': 'N', - 'О': 'O', - 'П': 'P', - 'Р': 'R', - 'С': 'S', - 'Т': 'T', - 'У': 'U', - 'Ф': 'F', - 'Х': 'Kh', - 'Ц': 'Ts', - 'Ч': 'Ch', - 'Ш': 'Sh', - 'Щ': 'Shch', - 'Ъ': "'", - 'Ы': 'Y', - 'Ь': "'", - 'Э': 'E', - 'Ю': 'Iu', - 'Я': 'Ia', - 'а': 'a', - 'б': 'b', - 'в': 'v', - 'г': 'g', - 'д': 'd', - 'е': 'e', - 'ж': 'zh', - 'з': 'z', - 'и': 'i', - 'й': 'i', - 'к': 'k', - 'л': 'l', - 'м': 'm', - 'н': 'n', - 'о': 'o', - 'п': 'p', - 'р': 'r', - 'с': 's', - 'т': 't', - 'у': 'u', - 'ф': 'f', - 'х': 'kh', - 'ц': 'ts', - 'ч': 'ch', - 'ш': 'sh', - 'щ': 'shch', - 'ъ': "'", - 'ы': 'y', - 'ь': "'", - 'э': 'e', - 'ю': 'iu', - 'я': 'ia', + "À": "A", + "Á": "A", + "Â": "A", + "Ã": "A", + "Ä": "A", + "Å": "A", + "Æ": "AE", + "Ç": "C", + "È": "E", + "É": "E", + "Ê": "E", + "Ë": "E", + "Ì": "I", + "Í": "I", + "Î": "I", + "Ï": "I", + "Ð": "D", + "Ñ": "N", + "Ò": "O", + "Ó": "O", + "Ô": "O", + "Õ": "O", + "Ö": "O", + "Ø": "O", + "Ù": "U", + "Ú": "U", + "Û": "U", + "Ü": "U", + "Ý": "Y", + "Þ": "Th", + "ß": "ss", + "à": "a", + "á": "a", + "â": "a", + "ã": "a", + "ä": "a", + "å": "a", + "æ": "ae", + "ç": "c", + "è": "e", + "é": "e", + "ê": "e", + "ë": "e", + "ì": "i", + "í": "i", + "î": "i", + "ï": "i", + "ð": "d", + "ñ": "n", + "ò": "o", + "ó": "o", + "ô": "o", + "õ": "o", + "ö": "o", + "ø": "o", + "ù": "u", + "ú": "u", + "û": "u", + "ü": "u", + "ý": "y", + "þ": "th", + "ÿ": "y", + "Ł": "L", + "ł": "l", + "Ń": "N", + "ń": "n", + "Ņ": "N", + "ņ": "n", + "Ň": "N", + "ň": "n", + "Ŋ": "ng", + "ŋ": "NG", + "Ō": "O", + "ō": "o", + "Ŏ": "O", + "ŏ": "o", + "Ő": "O", + "ő": "o", + "Œ": "OE", + "œ": "oe", + "Ŕ": "R", + "ŕ": "r", + "Ŗ": "R", + "ŗ": "r", + "Ř": "R", + "ř": "r", + "Ś": "S", + "ś": "s", + "Ŝ": "S", + "ŝ": "s", + "Ş": "S", + "ş": "s", + "Š": "S", + "š": "s", + "Ţ": "T", + "ţ": "t", + "Ť": "T", + "ť": "t", + "Ŧ": "T", + "ŧ": "t", + "Ũ": "U", + "ũ": "u", + "Ū": "U", + "ū": "u", + "Ŭ": "U", + "ŭ": "u", + "Ů": "U", + "ů": "u", + "Ű": "U", + "ű": "u", + "Ŵ": "W", + "ŵ": "w", + "Ŷ": "Y", + "ŷ": "y", + "Ÿ": "Y", + "Ź": "Z", + "ź": "z", + "Ż": "Z", + "ż": "z", + "Ž": "Z", + "ž": "z", + "ſ": "s", + "Α": "A", + "Β": "B", + "Γ": "G", + "Δ": "D", + "Ε": "E", + "Ζ": "Z", + "Η": "E", + "Θ": "Th", + "Ι": "I", + "Κ": "K", + "Λ": "L", + "Μ": "M", + "Ν": "N", + "Ξ": "Ks", + "Ο": "O", + "Π": "P", + "Ρ": "R", + "Σ": "S", + "Τ": "T", + "Υ": "U", + "Φ": "Ph", + "Χ": "Kh", + "Ψ": "Ps", + "Ω": "O", + "α": "a", + "β": "b", + "γ": "g", + "δ": "d", + "ε": "e", + "ζ": "z", + "η": "e", + "θ": "th", + "ι": "i", + "κ": "k", + "λ": "l", + "μ": "m", + "ν": "n", + "ξ": "x", + "ο": "o", + "π": "p", + "ρ": "r", + "ς": "s", + "σ": "s", + "τ": "t", + "υ": "u", + "φ": "ph", + "χ": "kh", + "ψ": "ps", + "ω": "o", + "А": "A", + "Б": "B", + "В": "V", + "Г": "G", + "Д": "D", + "Е": "E", + "Ж": "Zh", + "З": "Z", + "И": "I", + "Й": "I", + "К": "K", + "Л": "L", + "М": "M", + "Н": "N", + "О": "O", + "П": "P", + "Р": "R", + "С": "S", + "Т": "T", + "У": "U", + "Ф": "F", + "Х": "Kh", + "Ц": "Ts", + "Ч": "Ch", + "Ш": "Sh", + "Щ": "Shch", + "Ъ": "'", + "Ы": "Y", + "Ь": "'", + "Э": "E", + "Ю": "Iu", + "Я": "Ia", + "а": "a", + "б": "b", + "в": "v", + "г": "g", + "д": "d", + "е": "e", + "ж": "zh", + "з": "z", + "и": "i", + "й": "i", + "к": "k", + "л": "l", + "м": "m", + "н": "n", + "о": "o", + "п": "p", + "р": "r", + "с": "s", + "т": "t", + "у": "u", + "ф": "f", + "х": "kh", + "ц": "ts", + "ч": "ch", + "ш": "sh", + "щ": "shch", + "ъ": "'", + "ы": "y", + "ь": "'", + "э": "e", + "ю": "iu", + "я": "ia", # 'ᴀ': '', # 'ᴁ': '', # 'ᴂ': '', @@ -374,18 +374,18 @@ # 'ᴣ': '', # 'ᴤ': '', # 'ᴥ': '', - 'ᴦ': 'G', - 'ᴧ': 'L', - 'ᴨ': 'P', - 'ᴩ': 'R', - 'ᴪ': 'PS', - 'ẞ': 'Ss', - 'Ỳ': 'Y', - 'ỳ': 'y', - 'Ỵ': 'Y', - 'ỵ': 'y', - 'Ỹ': 'Y', - 'ỹ': 'y', + "ᴦ": "G", + "ᴧ": "L", + "ᴨ": "P", + "ᴩ": "R", + "ᴪ": "PS", + "ẞ": "Ss", + "Ỳ": "Y", + "ỳ": "y", + "Ỵ": "Y", + "ỵ": "y", + "Ỹ": "Y", + "ỹ": "y", } #################################################################### @@ -393,14 +393,14 @@ #################################################################### DUMB_PUNCTUATION = { - '‘': "'", - '’': "'", - '‚': "'", - '“': '"', - '”': '"', - '„': '"', - '–': '-', - '—': '-' + "‘": "'", + "’": "'", + "‚": "'", + "“": '"', + "”": '"', + "„": '"', + "–": "-", + "—": "-", } @@ -413,7 +413,7 @@ INITIALS = string.ascii_uppercase + string.digits #: Split on non-letters, numbers -split_on_delimiters = re.compile('[^a-zA-Z0-9]').split +split_on_delimiters = re.compile("[^a-zA-Z0-9]").split # Match filter flags #: Match items that start with ``query`` @@ -483,6 +483,7 @@ class PasswordExists(KeychainError): # Helper functions #################################################################### + def isascii(text): """Test if ``text`` contains only ASCII characters. @@ -493,7 +494,7 @@ def isascii(text): """ try: - text.encode('ascii') + text.encode("ascii") except UnicodeEncodeError: return False return True @@ -503,6 +504,7 @@ def isascii(text): # Implementation classes #################################################################### + class SerializerManager(object): """Contains registered serializers. @@ -540,8 +542,8 @@ def register(self, name, serializer): """ # Basic validation - getattr(serializer, 'load') - getattr(serializer, 'dump') + getattr(serializer, "load") + getattr(serializer, "dump") self._serializers[name] = serializer @@ -568,8 +570,7 @@ def unregister(self, name): """ if name not in self._serializers: - raise ValueError('No such serializer registered : {0}'.format( - name)) + raise ValueError("No such serializer registered : {0}".format(name)) serializer = self._serializers[name] del self._serializers[name] @@ -619,7 +620,7 @@ def dump(cls, obj, file_obj): :type file_obj: ``file`` object """ - return json.dump(obj, file_obj, indent=2, encoding='utf-8') + return json.dump(obj, file_obj, indent=2, encoding="utf-8") class CPickleSerializer(object): @@ -701,9 +702,9 @@ def dump(cls, obj, file_obj): # Set up default manager and register built-in serializers manager = SerializerManager() -manager.register('cpickle', CPickleSerializer) -manager.register('pickle', PickleSerializer) -manager.register('json', JSONSerializer) +manager.register("cpickle", CPickleSerializer) +manager.register("pickle", PickleSerializer) +manager.register("json", JSONSerializer) class Item(object): @@ -717,10 +718,22 @@ class Item(object): """ - def __init__(self, title, subtitle='', modifier_subtitles=None, - arg=None, autocomplete=None, valid=False, uid=None, - icon=None, icontype=None, type=None, largetext=None, - copytext=None, quicklookurl=None): + def __init__( + self, + title, + subtitle="", + modifier_subtitles=None, + arg=None, + autocomplete=None, + valid=False, + uid=None, + icon=None, + icontype=None, + type=None, + largetext=None, + copytext=None, + quicklookurl=None, + ): """Same arguments as :meth:`Workflow.add_item`.""" self.title = title self.subtitle = subtitle @@ -747,35 +760,36 @@ def elem(self): # Attributes on element attr = {} if self.valid: - attr['valid'] = 'yes' + attr["valid"] = "yes" else: - attr['valid'] = 'no' + attr["valid"] = "no" # Allow empty string for autocomplete. This is a useful value, # as TABing the result will revert the query back to just the # keyword if self.autocomplete is not None: - attr['autocomplete'] = self.autocomplete + attr["autocomplete"] = self.autocomplete # Optional attributes - for name in ('uid', 'type'): + for name in ("uid", "type"): value = getattr(self, name, None) if value: attr[name] = value - root = ET.Element('item', attr) - ET.SubElement(root, 'title').text = self.title - ET.SubElement(root, 'subtitle').text = self.subtitle + root = ET.Element("item", attr) + ET.SubElement(root, "title").text = self.title + ET.SubElement(root, "subtitle").text = self.subtitle # Add modifier subtitles - for mod in ('cmd', 'ctrl', 'alt', 'shift', 'fn'): + for mod in ("cmd", "ctrl", "alt", "shift", "fn"): if mod in self.modifier_subtitles: - ET.SubElement(root, 'subtitle', - {'mod': mod}).text = self.modifier_subtitles[mod] + ET.SubElement( + root, "subtitle", {"mod": mod} + ).text = self.modifier_subtitles[mod] # Add arg as element instead of attribute on , as it's more # flexible (newlines aren't allowed in attributes) if self.arg: - ET.SubElement(root, 'arg').text = self.arg + ET.SubElement(root, "arg").text = self.arg # Add icon if there is one if self.icon: @@ -783,18 +797,16 @@ def elem(self): attr = dict(type=self.icontype) else: attr = {} - ET.SubElement(root, 'icon', attr).text = self.icon + ET.SubElement(root, "icon", attr).text = self.icon if self.largetext: - ET.SubElement(root, 'text', - {'type': 'largetype'}).text = self.largetext + ET.SubElement(root, "text", {"type": "largetype"}).text = self.largetext if self.copytext: - ET.SubElement(root, 'text', - {'type': 'copy'}).text = self.copytext + ET.SubElement(root, "text", {"type": "copy"}).text = self.copytext if self.quicklookurl: - ET.SubElement(root, 'quicklookurl').text = self.quicklookurl + ET.SubElement(root, "quicklookurl").text = self.quicklookurl return root @@ -834,7 +846,7 @@ def _load(self): """Load cached settings from JSON file `self._filepath`.""" data = {} with LockFile(self._filepath, 0.5): - with open(self._filepath, 'rb') as fp: + with open(self._filepath, "rb") as fp: data.update(json.load(fp)) self._original = deepcopy(data) @@ -858,9 +870,8 @@ def save(self): data.update(self) with LockFile(self._filepath, 0.5): - with atomic_writer(self._filepath, 'wb') as fp: - json.dump(data, fp, sort_keys=True, indent=2, - encoding='utf-8') + with atomic_writer(self._filepath, "wb") as fp: + json.dump(data, fp, sort_keys=True, indent=2, encoding="utf-8") # dict methods def __setitem__(self, key, value): @@ -936,10 +947,16 @@ class Workflow(object): # won't want to change this item_class = Item - def __init__(self, default_settings=None, update_settings=None, - input_encoding='utf-8', normalization='NFC', - capture_args=True, libraries=None, - help_url=None): + def __init__( + self, + default_settings=None, + update_settings=None, + input_encoding="utf-8", + normalization="NFC", + capture_args=True, + libraries=None, + help_url=None, + ): """Create new :class:`Workflow` object.""" self._default_settings = default_settings or {} self._update_settings = update_settings or {} @@ -953,8 +970,8 @@ def __init__(self, default_settings=None, update_settings=None, self._bundleid = None self._debugging = None self._name = None - self._cache_serializer = 'cpickle' - self._data_serializer = 'cpickle' + self._cache_serializer = "cpickle" + self._data_serializer = "cpickle" self._info = None self._info_loaded = False self._logger = None @@ -969,7 +986,7 @@ def __init__(self, default_settings=None, update_settings=None, #: Prefix for all magic arguments. #: The default value is ``workflow:`` so keyword #: ``config`` would match user query ``workflow:config``. - self.magic_prefix = 'workflow:' + self.magic_prefix = "workflow:" #: Mapping of available magic arguments. The built-in magic #: arguments are registered by default. To add your own magic arguments #: (or override built-ins), add a key:value pair where the key is @@ -997,7 +1014,8 @@ def __init__(self, default_settings=None, update_settings=None, def alfred_version(self): """Alfred version as :class:`~workflow.update.Version` object.""" from update import Version - return Version(self.alfred_env.get('version')) + + return Version(self.alfred_env.get("version")) @property def alfred_env(self): @@ -1052,25 +1070,26 @@ def alfred_env(self): data = {} for key in ( - 'debug', - 'preferences', - 'preferences_localhash', - 'theme', - 'theme_background', - 'theme_subtext', - 'version', - 'version_build', - 'workflow_bundleid', - 'workflow_cache', - 'workflow_data', - 'workflow_name', - 'workflow_uid', - 'workflow_version'): - - value = os.getenv('alfred_' + key, '') + "debug", + "preferences", + "preferences_localhash", + "theme", + "theme_background", + "theme_subtext", + "version", + "version_build", + "workflow_bundleid", + "workflow_cache", + "workflow_data", + "workflow_name", + "workflow_uid", + "workflow_version", + ): + + value = os.getenv("alfred_" + key, "") if value: - if key in ('debug', 'version_build', 'theme_subtext'): + if key in ("debug", "version_build", "theme_subtext"): value = int(value) else: value = self.decode(value) @@ -1097,10 +1116,10 @@ def bundleid(self): """ if not self._bundleid: - if self.alfred_env.get('workflow_bundleid'): - self._bundleid = self.alfred_env.get('workflow_bundleid') + if self.alfred_env.get("workflow_bundleid"): + self._bundleid = self.alfred_env.get("workflow_bundleid") else: - self._bundleid = unicode(self.info['bundleid'], 'utf-8') + self._bundleid = unicode(self.info["bundleid"], "utf-8") return self._bundleid @@ -1112,7 +1131,7 @@ def debugging(self): :rtype: ``bool`` """ - return self.alfred_env.get('debug') == 1 + return self.alfred_env.get("debug") == 1 @property def name(self): @@ -1123,10 +1142,10 @@ def name(self): """ if not self._name: - if self.alfred_env.get('workflow_name'): - self._name = self.decode(self.alfred_env.get('workflow_name')) + if self.alfred_env.get("workflow_name"): + self._name = self.decode(self.alfred_env.get("workflow_name")) else: - self._name = self.decode(self.info['name']) + self._name = self.decode(self.info["name"]) return self._name @@ -1151,27 +1170,28 @@ def version(self): version = None # environment variable has priority - if self.alfred_env.get('workflow_version'): - version = self.alfred_env['workflow_version'] + if self.alfred_env.get("workflow_version"): + version = self.alfred_env["workflow_version"] # Try `update_settings` elif self._update_settings: - version = self._update_settings.get('version') + version = self._update_settings.get("version") # `version` file if not version: - filepath = self.workflowfile('version') + filepath = self.workflowfile("version") if os.path.exists(filepath): - with open(filepath, 'rb') as fileobj: + with open(filepath, "rb") as fileobj: version = fileobj.read() # info.plist if not version: - version = self.info.get('version') + version = self.info.get("version") if version: from update import Version + version = Version(version) self._version = version @@ -1204,7 +1224,7 @@ def args(self): # Handle magic args if len(args) and self._capture_args: for name in self.magic_arguments: - key = '{0}{1}'.format(self.magic_prefix, name) + key = "{0}{1}".format(self.magic_prefix, name) if key in args: msg = self.magic_arguments[name]() @@ -1235,8 +1255,8 @@ def cachedir(self): unicode: full path to workflow's cache directory """ - if self.alfred_env.get('workflow_cache'): - dirpath = self.alfred_env.get('workflow_cache') + if self.alfred_env.get("workflow_cache"): + dirpath = self.alfred_env.get("workflow_cache") else: dirpath = self._default_cachedir @@ -1248,9 +1268,10 @@ def _default_cachedir(self): """Alfred 2's default cache directory.""" return os.path.join( os.path.expanduser( - '~/Library/Caches/com.runningwithcrayons.Alfred-2/' - 'Workflow Data/'), - self.bundleid) + "~/Library/Caches/com.runningwithcrayons.Alfred-2/" "Workflow Data/" + ), + self.bundleid, + ) @property def datadir(self): @@ -1271,8 +1292,8 @@ def datadir(self): unicode: full path to workflow data directory """ - if self.alfred_env.get('workflow_data'): - dirpath = self.alfred_env.get('workflow_data') + if self.alfred_env.get("workflow_data"): + dirpath = self.alfred_env.get("workflow_data") else: dirpath = self._default_datadir @@ -1282,9 +1303,10 @@ def datadir(self): @property def _default_datadir(self): """Alfred 2's default data directory.""" - return os.path.join(os.path.expanduser( - '~/Library/Application Support/Alfred 2/Workflow Data/'), - self.bundleid) + return os.path.join( + os.path.expanduser("~/Library/Application Support/Alfred 2/Workflow Data/"), + self.bundleid, + ) @property def workflowdir(self): @@ -1300,7 +1322,8 @@ def workflowdir(self): # a workflow is being run in Alfred candidates = [ os.path.abspath(os.getcwdu()), - os.path.dirname(os.path.abspath(os.path.dirname(__file__)))] + os.path.dirname(os.path.abspath(os.path.dirname(__file__))), + ] # climb the directory tree until we find `info.plist` for dirpath in candidates: @@ -1309,11 +1332,11 @@ def workflowdir(self): dirpath = self.decode(dirpath) while True: - if os.path.exists(os.path.join(dirpath, 'info.plist')): + if os.path.exists(os.path.join(dirpath, "info.plist")): self._workflowdir = dirpath break - elif dirpath == '/': + elif dirpath == "/": # no `info.plist` found break @@ -1376,7 +1399,7 @@ def logfile(self): :rtype: ``unicode`` """ - return self.cachefile('%s.log' % self.bundleid) + return self.cachefile("%s.log" % self.bundleid) @property def logger(self): @@ -1394,7 +1417,7 @@ def logger(self): return self._logger # Initialise new logger and optionally handlers - logger = logging.getLogger('') + logger = logging.getLogger("") # Only add one set of handlers # Exclude from coverage, as pytest will have configured the @@ -1402,14 +1425,13 @@ def logger(self): if not len(logger.handlers): # pragma: no cover fmt = logging.Formatter( - '%(asctime)s %(filename)s:%(lineno)s' - ' %(levelname)-8s %(message)s', - datefmt='%H:%M:%S') + "%(asctime)s %(filename)s:%(lineno)s" " %(levelname)-8s %(message)s", + datefmt="%H:%M:%S", + ) logfile = logging.handlers.RotatingFileHandler( - self.logfile, - maxBytes=1024 * 1024, - backupCount=1) + self.logfile, maxBytes=1024 * 1024, backupCount=1 + ) logfile.setFormatter(fmt) logger.addHandler(logfile) @@ -1445,7 +1467,7 @@ def settings_path(self): """ if not self._settings_path: - self._settings_path = self.datafile('settings.json') + self._settings_path = self.datafile("settings.json") return self._settings_path @property @@ -1465,9 +1487,8 @@ def settings(self): """ if not self._settings: - self.logger.debug('reading settings from %s', self.settings_path) - self._settings = Settings(self.settings_path, - self._default_settings) + self.logger.debug("reading settings from %s", self.settings_path) + self._settings = Settings(self.settings_path, self._default_settings) return self._settings @property @@ -1506,10 +1527,11 @@ def cache_serializer(self, serializer_name): """ if manager.serializer(serializer_name) is None: raise ValueError( - 'Unknown serializer : `{0}`. Register your serializer ' - 'with `manager` first.'.format(serializer_name)) + "Unknown serializer : `{0}`. Register your serializer " + "with `manager` first.".format(serializer_name) + ) - self.logger.debug('default cache serializer: %s', serializer_name) + self.logger.debug("default cache serializer: %s", serializer_name) self._cache_serializer = serializer_name @@ -1548,10 +1570,11 @@ def data_serializer(self, serializer_name): """ if manager.serializer(serializer_name) is None: raise ValueError( - 'Unknown serializer : `{0}`. Register your serializer ' - 'with `manager` first.'.format(serializer_name)) + "Unknown serializer : `{0}`. Register your serializer " + "with `manager` first.".format(serializer_name) + ) - self.logger.debug('default data serializer: %s', serializer_name) + self.logger.debug("default data serializer: %s", serializer_name) self._data_serializer = serializer_name @@ -1565,39 +1588,40 @@ def stored_data(self, name): :param name: name of datastore """ - metadata_path = self.datafile('.{0}.alfred-workflow'.format(name)) + metadata_path = self.datafile(".{0}.alfred-workflow".format(name)) if not os.path.exists(metadata_path): - self.logger.debug('no data stored for `%s`', name) + self.logger.debug("no data stored for `%s`", name) return None - with open(metadata_path, 'rb') as file_obj: + with open(metadata_path, "rb") as file_obj: serializer_name = file_obj.read().strip() serializer = manager.serializer(serializer_name) if serializer is None: raise ValueError( - 'Unknown serializer `{0}`. Register a corresponding ' - 'serializer with `manager.register()` ' - 'to load this data.'.format(serializer_name)) + "Unknown serializer `{0}`. Register a corresponding " + "serializer with `manager.register()` " + "to load this data.".format(serializer_name) + ) - self.logger.debug('data `%s` stored as `%s`', name, serializer_name) + self.logger.debug("data `%s` stored as `%s`", name, serializer_name) - filename = '{0}.{1}'.format(name, serializer_name) + filename = "{0}.{1}".format(name, serializer_name) data_path = self.datafile(filename) if not os.path.exists(data_path): - self.logger.debug('no data stored: %s', name) + self.logger.debug("no data stored: %s", name) if os.path.exists(metadata_path): os.unlink(metadata_path) return None - with open(data_path, 'rb') as file_obj: + with open(data_path, "rb") as file_obj: data = serializer.load(file_obj) - self.logger.debug('stored data loaded: %s', data_path) + self.logger.debug("stored data loaded: %s", data_path) return data @@ -1626,29 +1650,31 @@ def delete_paths(paths): for path in paths: if os.path.exists(path): os.unlink(path) - self.logger.debug('deleted data file: %s', path) + self.logger.debug("deleted data file: %s", path) serializer_name = serializer or self.data_serializer # In order for `stored_data()` to be able to load data stored with # an arbitrary serializer, yet still have meaningful file extensions, # the format (i.e. extension) is saved to an accompanying file - metadata_path = self.datafile('.{0}.alfred-workflow'.format(name)) - filename = '{0}.{1}'.format(name, serializer_name) + metadata_path = self.datafile(".{0}.alfred-workflow".format(name)) + filename = "{0}.{1}".format(name, serializer_name) data_path = self.datafile(filename) if data_path == self.settings_path: raise ValueError( - 'Cannot save data to' + - '`{0}` with format `{1}`. '.format(name, serializer_name) + - "This would overwrite Alfred-Workflow's settings file.") + "Cannot save data to" + + "`{0}` with format `{1}`. ".format(name, serializer_name) + + "This would overwrite Alfred-Workflow's settings file." + ) serializer = manager.serializer(serializer_name) if serializer is None: raise ValueError( - 'Invalid serializer `{0}`. Register your serializer with ' - '`manager.register()` first.'.format(serializer_name)) + "Invalid serializer `{0}`. Register your serializer with " + "`manager.register()` first.".format(serializer_name) + ) if data is None: # Delete cached data delete_paths((metadata_path, data_path)) @@ -1658,15 +1684,15 @@ def delete_paths(paths): @uninterruptible def _store(): # Save file extension - with atomic_writer(metadata_path, 'wb') as file_obj: + with atomic_writer(metadata_path, "wb") as file_obj: file_obj.write(serializer_name) - with atomic_writer(data_path, 'wb') as file_obj: + with atomic_writer(data_path, "wb") as file_obj: serializer.dump(data, file_obj) _store() - self.logger.debug('saved data: %s', data_path) + self.logger.debug("saved data: %s", data_path) def cached_data(self, name, data_func=None, max_age=60): """Return cached data if younger than ``max_age`` seconds. @@ -1686,13 +1712,13 @@ def cached_data(self, name, data_func=None, max_age=60): """ serializer = manager.serializer(self.cache_serializer) - cache_path = self.cachefile('%s.%s' % (name, self.cache_serializer)) + cache_path = self.cachefile("%s.%s" % (name, self.cache_serializer)) age = self.cached_data_age(name) if (age < max_age or max_age == 0) and os.path.exists(cache_path): - with open(cache_path, 'rb') as file_obj: - self.logger.debug('loading cached data: %s', cache_path) + with open(cache_path, "rb") as file_obj: + self.logger.debug("loading cached data: %s", cache_path) return serializer.load(file_obj) if not data_func: @@ -1716,18 +1742,18 @@ def cache_data(self, name, data): """ serializer = manager.serializer(self.cache_serializer) - cache_path = self.cachefile('%s.%s' % (name, self.cache_serializer)) + cache_path = self.cachefile("%s.%s" % (name, self.cache_serializer)) if data is None: if os.path.exists(cache_path): os.unlink(cache_path) - self.logger.debug('deleted cache file: %s', cache_path) + self.logger.debug("deleted cache file: %s", cache_path) return - with atomic_writer(cache_path, 'wb') as file_obj: + with atomic_writer(cache_path, "wb") as file_obj: serializer.dump(data, file_obj) - self.logger.debug('cached data: %s', cache_path) + self.logger.debug("cached data: %s", cache_path) def cached_data_fresh(self, name, max_age): """Whether cache `name` is less than `max_age` seconds old. @@ -1755,16 +1781,25 @@ def cached_data_age(self, name): :rtype: ``int`` """ - cache_path = self.cachefile('%s.%s' % (name, self.cache_serializer)) + cache_path = self.cachefile("%s.%s" % (name, self.cache_serializer)) if not os.path.exists(cache_path): return 0 return time.time() - os.stat(cache_path).st_mtime - def filter(self, query, items, key=lambda x: x, ascending=False, - include_score=False, min_score=0, max_results=0, - match_on=MATCH_ALL, fold_diacritics=True): + def filter( + self, + query, + items, + key=lambda x: x, + ascending=False, + include_score=False, + min_score=0, + max_results=0, + match_on=MATCH_ALL, + fold_diacritics=True, + ): """Fuzzy search filter. Returns list of ``items`` that match ``query``. ``query`` is case-insensitive. Any item that does not contain the @@ -1873,23 +1908,23 @@ def filter(self, query, items, key=lambda x: x, ascending=False, return items # Use user override if there is one - fold_diacritics = self.settings.get('__workflow_diacritic_folding', - fold_diacritics) + fold_diacritics = self.settings.get( + "__workflow_diacritic_folding", fold_diacritics + ) results = [] for item in items: skip = False score = 0 - words = [s.strip() for s in query.split(' ')] + words = [s.strip() for s in query.split(" ")] value = key(item).strip() - if value == '': + if value == "": continue for word in words: - if word == '': + if word == "": continue - s, rule = self._filter_item(value, word, match_on, - fold_diacritics) + s, rule = self._filter_item(value, word, match_on, fold_diacritics) if not s: # Skip items that don't match part of the query skip = True @@ -1902,8 +1937,9 @@ def filter(self, query, items, key=lambda x: x, ascending=False, # use "reversed" `score` (i.e. highest becomes lowest) and # `value` as sort key. This means items with the same score # will be sorted in alphabetical not reverse alphabetical order - results.append(((100.0 / score, value.lower(), score), - (item, score, rule))) + results.append( + ((100.0 / score, value.lower(), score), (item, score, rule)) + ) # sort on keys, then discard the keys results.sort(reverse=ascending) @@ -1950,7 +1986,7 @@ def _filter_item(self, value, query, match_on, fold_diacritics): # query matches capitalised letters in item, # e.g. of = OmniFocus if match_on & MATCH_CAPITALS: - initials = ''.join([c for c in value if c in INITIALS]) + initials = "".join([c for c in value if c in INITIALS]) if initials.lower().startswith(query): score = 100.0 - (len(initials) / len(query)) @@ -1958,13 +1994,15 @@ def _filter_item(self, value, query, match_on, fold_diacritics): # split the item into "atoms", i.e. words separated by # spaces or other non-word characters - if (match_on & MATCH_ATOM or - match_on & MATCH_INITIALS_CONTAIN or - match_on & MATCH_INITIALS_STARTSWITH): + if ( + match_on & MATCH_ATOM + or match_on & MATCH_INITIALS_CONTAIN + or match_on & MATCH_INITIALS_STARTSWITH + ): atoms = [s.lower() for s in split_on_delimiters(value)] # print('atoms : %s --> %s' % (value, atoms)) # initials of the atoms - initials = ''.join([s[0] for s in atoms if s]) + initials = "".join([s[0] for s in atoms if s]) if match_on & MATCH_ATOM: # is `query` one of the atoms in item? @@ -1979,16 +2017,14 @@ def _filter_item(self, value, query, match_on, fold_diacritics): # atoms, e.g. ``himym`` matches "How I Met Your Mother" # *and* "how i met your mother" (the ``capitals`` rule only # matches the former) - if (match_on & MATCH_INITIALS_STARTSWITH and - initials.startswith(query)): + if match_on & MATCH_INITIALS_STARTSWITH and initials.startswith(query): score = 100.0 - (len(initials) / len(query)) return (score, MATCH_INITIALS_STARTSWITH) # `query` is a substring of initials, e.g. ``doh`` matches # "The Dukes of Hazzard" - elif (match_on & MATCH_INITIALS_CONTAIN and - query in initials): + elif match_on & MATCH_INITIALS_CONTAIN and query in initials: score = 95.0 - (len(initials) / len(query)) return (score, MATCH_INITIALS_CONTAIN) @@ -2005,8 +2041,9 @@ def _filter_item(self, value, query, match_on, fold_diacritics): search = self._search_for_query(query) match = search(value) if match: - score = 100.0 / ((1 + match.start()) * - (match.end() - match.start() + 1)) + score = 100.0 / ( + (1 + match.start()) * (match.end() - match.start() + 1) + ) return (score, MATCH_ALLCHARS) @@ -2021,8 +2058,8 @@ def _search_for_query(self, query): pattern = [] for c in query: # pattern.append('[^{0}]*{0}'.format(re.escape(c))) - pattern.append('.*?{0}'.format(re.escape(c))) - pattern = ''.join(pattern) + pattern.append(".*?{0}".format(re.escape(c))) + pattern = "".join(pattern) search = re.compile(pattern, re.IGNORECASE).search self._search_pattern_cache[query] = search @@ -2051,16 +2088,17 @@ def run(self, func, text_errors=False): start = time.time() # Write to debugger to ensure "real" output starts on a new line - print('.', file=sys.stderr) + print(".", file=sys.stderr) # Call workflow's entry function/method within a try-except block # to catch any errors and display an error message in Alfred try: if self.version: - self.logger.debug('---------- %s (%s) ----------', - self.name, self.version) + self.logger.debug( + "---------- %s (%s) ----------", self.name, self.version + ) else: - self.logger.debug('---------- %s ----------', self.name) + self.logger.debug("---------- %s ----------", self.name) # Run update check if configured for self-updates. # This call has to go in the `run` try-except block, as it will @@ -2079,11 +2117,11 @@ def run(self, func, text_errors=False): except Exception as err: self.logger.exception(err) if self.help_url: - self.logger.info('for assistance, see: %s', self.help_url) + self.logger.info("for assistance, see: %s", self.help_url) if not sys.stdout.isatty(): # Show error in Alfred if text_errors: - print(unicode(err).encode('utf-8'), end='') + print(unicode(err).encode("utf-8"), end="") else: self._items = [] if self._name: @@ -2092,24 +2130,37 @@ def run(self, func, text_errors=False): name = self._bundleid else: # pragma: no cover name = os.path.dirname(__file__) - self.add_item("Error in workflow '%s'" % name, - unicode(err), - icon=ICON_ERROR) + self.add_item( + "Error in workflow '%s'" % name, unicode(err), icon=ICON_ERROR + ) self.send_feedback() return 1 finally: - self.logger.debug('---------- finished in %0.3fs ----------', - time.time() - start) + self.logger.debug( + "---------- finished in %0.3fs ----------", time.time() - start + ) return 0 # Alfred feedback methods ------------------------------------------ - def add_item(self, title, subtitle='', modifier_subtitles=None, arg=None, - autocomplete=None, valid=False, uid=None, icon=None, - icontype=None, type=None, largetext=None, copytext=None, - quicklookurl=None): + def add_item( + self, + title, + subtitle="", + modifier_subtitles=None, + arg=None, + autocomplete=None, + valid=False, + uid=None, + icon=None, + icontype=None, + type=None, + largetext=None, + copytext=None, + quicklookurl=None, + ): """Add an item to be output to Alfred. :param title: Title shown in Alfred @@ -2167,19 +2218,31 @@ def add_item(self, title, subtitle='', modifier_subtitles=None, arg=None, edit it or do something with it other than send it to Alfred. """ - item = self.item_class(title, subtitle, modifier_subtitles, arg, - autocomplete, valid, uid, icon, icontype, type, - largetext, copytext, quicklookurl) + item = self.item_class( + title, + subtitle, + modifier_subtitles, + arg, + autocomplete, + valid, + uid, + icon, + icontype, + type, + largetext, + copytext, + quicklookurl, + ) self._items.append(item) return item def send_feedback(self): """Print stored items to console/Alfred as XML.""" - root = ET.Element('items') + root = ET.Element("items") for item in self._items: root.append(item.elem) sys.stdout.write('\n') - sys.stdout.write(ET.tostring(root).encode('utf-8')) + sys.stdout.write(ET.tostring(root).encode("utf-8")) sys.stdout.flush() #################################################################### @@ -2196,7 +2259,7 @@ def first_run(self): """ if not self.version: - raise ValueError('No workflow version set') + raise ValueError("No workflow version set") if not self.last_version_run: return True @@ -2215,14 +2278,15 @@ def last_version_run(self): """ if self._last_version_run is UNSET: - version = self.settings.get('__workflow_last_version') + version = self.settings.get("__workflow_last_version") if version: from update import Version + version = Version(version) self._last_version_run = version - self.logger.debug('last run version: %s', self._last_version_run) + self.logger.debug("last run version: %s", self._last_version_run) return self._last_version_run @@ -2239,19 +2303,19 @@ def set_last_version(self, version=None): """ if not version: if not self.version: - self.logger.warning( - "Can't save last version: workflow has no version") + self.logger.warning("Can't save last version: workflow has no version") return False version = self.version if isinstance(version, basestring): from update import Version + version = Version(version) - self.settings['__workflow_last_version'] = str(version) + self.settings["__workflow_last_version"] = str(version) - self.logger.debug('set last run version: %s', version) + self.logger.debug("set last run version: %s", version) return True @@ -2267,16 +2331,16 @@ def update_available(self): :returns: ``True`` if an update is available, else ``False`` """ - key = '__workflow_latest_version' + key = "__workflow_latest_version" # Create a new workflow object to ensure standard serialiser # is used (update.py is called without the user's settings) status = Workflow().cached_data(key, max_age=0) # self.logger.debug('update status: %r', status) - if not status or not status.get('available'): + if not status or not status.get("available"): return False - return status['available'] + return status["available"] @property def prereleases(self): @@ -2289,10 +2353,10 @@ def prereleases(self): ``False``. """ - if self._update_settings.get('prereleases'): + if self._update_settings.get("prereleases"): return True - return self.settings.get('__workflow_prereleases') or False + return self.settings.get("__workflow_prereleases") or False def check_update(self, force=False): """Call update script if it's time to check for a new release. @@ -2309,38 +2373,36 @@ def check_update(self, force=False): :type force: ``Boolean`` """ - key = '__workflow_latest_version' - frequency = self._update_settings.get('frequency', - DEFAULT_UPDATE_FREQUENCY) + key = "__workflow_latest_version" + frequency = self._update_settings.get("frequency", DEFAULT_UPDATE_FREQUENCY) - if not force and not self.settings.get('__workflow_autoupdate', True): - self.logger.debug('Auto update turned off by user') + if not force and not self.settings.get("__workflow_autoupdate", True): + self.logger.debug("Auto update turned off by user") return # Check for new version if it's time - if (force or not self.cached_data_fresh(key, frequency * 86400)): + if force or not self.cached_data_fresh(key, frequency * 86400): - repo = self._update_settings['github_slug'] + repo = self._update_settings["github_slug"] # version = self._update_settings['version'] version = str(self.version) from background import run_in_background # update.py is adjacent to this file - update_script = os.path.join(os.path.dirname(__file__), - b'update.py') + update_script = os.path.join(os.path.dirname(__file__), b"update.py") - cmd = ['/usr/bin/python', update_script, 'check', repo, version] + cmd = ["/usr/bin/python", update_script, "check", repo, version] if self.prereleases: - cmd.append('--prereleases') + cmd.append("--prereleases") - self.logger.info('checking for update ...') + self.logger.info("checking for update ...") - run_in_background('__workflow_update_check', cmd) + run_in_background("__workflow_update_check", cmd) else: - self.logger.debug('update check not due') + self.logger.debug("update check not due") def start_update(self): """Check for update and download and install new workflow file. @@ -2356,7 +2418,7 @@ def start_update(self): """ import update - repo = self._update_settings['github_slug'] + repo = self._update_settings["github_slug"] # version = self._update_settings['version'] version = str(self.version) @@ -2366,16 +2428,15 @@ def start_update(self): from background import run_in_background # update.py is adjacent to this file - update_script = os.path.join(os.path.dirname(__file__), - b'update.py') + update_script = os.path.join(os.path.dirname(__file__), b"update.py") - cmd = ['/usr/bin/python', update_script, 'install', repo, version] + cmd = ["/usr/bin/python", update_script, "install", repo, version] if self.prereleases: - cmd.append('--prereleases') + cmd.append("--prereleases") - self.logger.debug('downloading update ...') - run_in_background('__workflow_update_install', cmd) + self.logger.debug("downloading update ...") + run_in_background("__workflow_update_install", cmd) return True @@ -2406,22 +2467,24 @@ def save_password(self, account, password, service=None): service = self.bundleid try: - self._call_security('add-generic-password', service, account, - '-w', password) - self.logger.debug('saved password : %s:%s', service, account) + self._call_security( + "add-generic-password", service, account, "-w", password + ) + self.logger.debug("saved password : %s:%s", service, account) except PasswordExists: - self.logger.debug('password exists : %s:%s', service, account) + self.logger.debug("password exists : %s:%s", service, account) current_password = self.get_password(account, service) if current_password == password: - self.logger.debug('password unchanged') + self.logger.debug("password unchanged") else: self.delete_password(account, service) - self._call_security('add-generic-password', service, - account, '-w', password) - self.logger.debug('save_password : %s:%s', service, account) + self._call_security( + "add-generic-password", service, account, "-w", password + ) + self.logger.debug("save_password : %s:%s", service, account) def get_password(self, account, service=None): """Retrieve the password saved at ``service/account``. @@ -2441,24 +2504,23 @@ def get_password(self, account, service=None): if not service: service = self.bundleid - output = self._call_security('find-generic-password', service, - account, '-g') + output = self._call_security("find-generic-password", service, account, "-g") # Parsing of `security` output is adapted from python-keyring # by Jason R. Coombs # https://pypi.python.org/pypi/keyring m = re.search( - r'password:\s*(?:0x(?P[0-9A-F]+)\s*)?(?:"(?P.*)")?', - output) + r'password:\s*(?:0x(?P[0-9A-F]+)\s*)?(?:"(?P.*)")?', output + ) if m: groups = m.groupdict() - h = groups.get('hex') - password = groups.get('pw') + h = groups.get("hex") + password = groups.get("pw") if h: - password = unicode(binascii.unhexlify(h), 'utf-8') + password = unicode(binascii.unhexlify(h), "utf-8") - self.logger.debug('got password : %s:%s', service, account) + self.logger.debug("got password : %s:%s", service, account) return password @@ -2478,9 +2540,9 @@ def delete_password(self, account, service=None): if not service: service = self.bundleid - self._call_security('delete-generic-password', service, account) + self._call_security("delete-generic-password", service, account) - self.logger.debug('deleted password : %s:%s', service, account) + self.logger.debug("deleted password : %s:%s", service, account) #################################################################### # Methods for workflow:* magic args @@ -2497,91 +2559,98 @@ def wrapper(): return wrapper - self.magic_arguments['delcache'] = callback(self.clear_cache, - 'Deleted workflow cache') - self.magic_arguments['deldata'] = callback(self.clear_data, - 'Deleted workflow data') - self.magic_arguments['delsettings'] = callback( - self.clear_settings, 'Deleted workflow settings') - self.magic_arguments['reset'] = callback(self.reset, - 'Reset workflow') - self.magic_arguments['openlog'] = callback(self.open_log, - 'Opening workflow log file') - self.magic_arguments['opencache'] = callback( - self.open_cachedir, 'Opening workflow cache directory') - self.magic_arguments['opendata'] = callback( - self.open_datadir, 'Opening workflow data directory') - self.magic_arguments['openworkflow'] = callback( - self.open_workflowdir, 'Opening workflow directory') - self.magic_arguments['openterm'] = callback( - self.open_terminal, 'Opening workflow root directory in Terminal') + self.magic_arguments["delcache"] = callback( + self.clear_cache, "Deleted workflow cache" + ) + self.magic_arguments["deldata"] = callback( + self.clear_data, "Deleted workflow data" + ) + self.magic_arguments["delsettings"] = callback( + self.clear_settings, "Deleted workflow settings" + ) + self.magic_arguments["reset"] = callback(self.reset, "Reset workflow") + self.magic_arguments["openlog"] = callback( + self.open_log, "Opening workflow log file" + ) + self.magic_arguments["opencache"] = callback( + self.open_cachedir, "Opening workflow cache directory" + ) + self.magic_arguments["opendata"] = callback( + self.open_datadir, "Opening workflow data directory" + ) + self.magic_arguments["openworkflow"] = callback( + self.open_workflowdir, "Opening workflow directory" + ) + self.magic_arguments["openterm"] = callback( + self.open_terminal, "Opening workflow root directory in Terminal" + ) # Diacritic folding def fold_on(): - self.settings['__workflow_diacritic_folding'] = True - return 'Diacritics will always be folded' + self.settings["__workflow_diacritic_folding"] = True + return "Diacritics will always be folded" def fold_off(): - self.settings['__workflow_diacritic_folding'] = False - return 'Diacritics will never be folded' + self.settings["__workflow_diacritic_folding"] = False + return "Diacritics will never be folded" def fold_default(): - if '__workflow_diacritic_folding' in self.settings: - del self.settings['__workflow_diacritic_folding'] - return 'Diacritics folding reset' + if "__workflow_diacritic_folding" in self.settings: + del self.settings["__workflow_diacritic_folding"] + return "Diacritics folding reset" - self.magic_arguments['foldingon'] = fold_on - self.magic_arguments['foldingoff'] = fold_off - self.magic_arguments['foldingdefault'] = fold_default + self.magic_arguments["foldingon"] = fold_on + self.magic_arguments["foldingoff"] = fold_off + self.magic_arguments["foldingdefault"] = fold_default # Updates def update_on(): - self.settings['__workflow_autoupdate'] = True - return 'Auto update turned on' + self.settings["__workflow_autoupdate"] = True + return "Auto update turned on" def update_off(): - self.settings['__workflow_autoupdate'] = False - return 'Auto update turned off' + self.settings["__workflow_autoupdate"] = False + return "Auto update turned off" def prereleases_on(): - self.settings['__workflow_prereleases'] = True - return 'Prerelease updates turned on' + self.settings["__workflow_prereleases"] = True + return "Prerelease updates turned on" def prereleases_off(): - self.settings['__workflow_prereleases'] = False - return 'Prerelease updates turned off' + self.settings["__workflow_prereleases"] = False + return "Prerelease updates turned off" def do_update(): if self.start_update(): - return 'Downloading and installing update ...' + return "Downloading and installing update ..." else: - return 'No update available' + return "No update available" - self.magic_arguments['autoupdate'] = update_on - self.magic_arguments['noautoupdate'] = update_off - self.magic_arguments['prereleases'] = prereleases_on - self.magic_arguments['noprereleases'] = prereleases_off - self.magic_arguments['update'] = do_update + self.magic_arguments["autoupdate"] = update_on + self.magic_arguments["noautoupdate"] = update_off + self.magic_arguments["prereleases"] = prereleases_on + self.magic_arguments["noprereleases"] = prereleases_off + self.magic_arguments["update"] = do_update # Help def do_help(): if self.help_url: self.open_help() - return 'Opening workflow help URL in browser' + return "Opening workflow help URL in browser" else: - return 'Workflow has no help URL' + return "Workflow has no help URL" def show_version(): if self.version: - return 'Version: {0}'.format(self.version) + return "Version: {0}".format(self.version) else: - return 'This workflow has no version number' + return "This workflow has no version number" def list_magic(): """Display all available magic args in Alfred.""" isatty = sys.stderr.isatty() for name in sorted(self.magic_arguments.keys()): - if name == 'magic': + if name == "magic": continue arg = self.magic_prefix + name self.logger.debug(arg) @@ -2592,9 +2661,9 @@ def list_magic(): if not isatty: self.send_feedback() - self.magic_arguments['help'] = do_help - self.magic_arguments['magic'] = list_magic - self.magic_arguments['version'] = show_version + self.magic_arguments["help"] = do_help + self.magic_arguments["magic"] = list_magic + self.magic_arguments["version"] = show_version def clear_cache(self, filter_func=lambda f: True): """Delete all files in workflow's :attr:`cachedir`. @@ -2624,7 +2693,7 @@ def clear_settings(self): """Delete workflow's :attr:`settings_path`.""" if os.path.exists(self.settings_path): os.unlink(self.settings_path) - self.logger.debug('deleted : %r', self.settings_path) + self.logger.debug("deleted : %r", self.settings_path) def reset(self): """Delete workflow settings, cache and data. @@ -2639,29 +2708,29 @@ def reset(self): def open_log(self): """Open :attr:`logfile` in default app (usually Console.app).""" - subprocess.call(['open', self.logfile]) # nosec + subprocess.call(["open", self.logfile]) # nosec def open_cachedir(self): """Open the workflow's :attr:`cachedir` in Finder.""" - subprocess.call(['open', self.cachedir]) # nosec + subprocess.call(["open", self.cachedir]) # nosec def open_datadir(self): """Open the workflow's :attr:`datadir` in Finder.""" - subprocess.call(['open', self.datadir]) # nosec + subprocess.call(["open", self.datadir]) # nosec def open_workflowdir(self): """Open the workflow's :attr:`workflowdir` in Finder.""" - subprocess.call(['open', self.workflowdir]) # nosec + subprocess.call(["open", self.workflowdir]) # nosec def open_terminal(self): """Open a Terminal window at workflow's :attr:`workflowdir`.""" - subprocess.call(['open', '-a', 'Terminal', self.workflowdir]) # nosec + subprocess.call(["open", "-a", "Terminal", self.workflowdir]) # nosec def open_help(self): """Open :attr:`help_url` in default browser.""" - subprocess.call(['open', self.help_url]) # nosec + subprocess.call(["open", self.help_url]) # nosec - return 'Opening workflow help URL in browser' + return "Opening workflow help URL in browser" #################################################################### # Helper methods @@ -2716,9 +2785,8 @@ def fold_to_ascii(self, text): """ if isascii(text): return text - text = ''.join([ASCII_REPLACEMENTS.get(c, c) for c in text]) - return unicode(unicodedata.normalize('NFKD', - text).encode('ascii', 'ignore')) + text = "".join([ASCII_REPLACEMENTS.get(c, c) for c in text]) + return unicode(unicodedata.normalize("NFKD", text).encode("ascii", "ignore")) def dumbify_punctuation(self, text): """Convert non-ASCII punctuation to closest ASCII equivalent. @@ -2738,7 +2806,7 @@ def dumbify_punctuation(self, text): if isascii(text): return text - text = ''.join([DUMB_PUNCTUATION.get(c, c) for c in text]) + text = "".join([DUMB_PUNCTUATION.get(c, c) for c in text]) return text def _delete_directory_contents(self, dirpath, filter_func): @@ -2760,12 +2828,12 @@ def _delete_directory_contents(self, dirpath, filter_func): shutil.rmtree(path) else: os.unlink(path) - self.logger.debug('deleted : %r', path) + self.logger.debug("deleted : %r", path) def _load_info_plist(self): """Load workflow info from ``info.plist``.""" # info.plist should be in the directory above this one - self._info = plistlib.readPlist(self.workflowfile('info.plist')) + self._info = plistlib.readPlist(self.workflowfile("info.plist")) self._info_loaded = True def _create(self, dirpath): @@ -2805,16 +2873,15 @@ def _call_security(self, action, service, account, *args): :rtype: `tuple` (`int`, ``unicode``) """ - cmd = ['security', action, '-s', service, '-a', account] + list(args) - p = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) + cmd = ["security", action, "-s", service, "-a", account] + list(args) + p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) stdout, _ = p.communicate() if p.returncode == 44: # password does not exist raise PasswordNotFound() elif p.returncode == 45: # password already exists raise PasswordExists() elif p.returncode > 0: - err = KeychainError('Unknown Keychain error : %s' % stdout) + err = KeychainError("Unknown Keychain error : %s" % stdout) err.retcode = p.returncode raise err - return stdout.strip().decode('utf-8') + return stdout.strip().decode("utf-8") diff --git a/workflow/workflow3.py b/workflow/workflow3.py index 23a7aae..c554a63 100644 --- a/workflow/workflow3.py +++ b/workflow/workflow3.py @@ -78,15 +78,15 @@ def obj(self): d2 = {} for k, v in self.items(): d2[k] = v - o['variables'] = d2 + o["variables"] = d2 if self.config: - o['config'] = self.config + o["config"] = self.config if self.arg is not None: - o['arg'] = self.arg + o["arg"] = self.arg - return {'alfredworkflow': o} + return {"alfredworkflow": o} def __unicode__(self): """Convert to ``alfredworkflow`` JSON object. @@ -97,7 +97,7 @@ def __unicode__(self): """ if not self and not self.config: if not self.arg: - return u'' + return "" if isinstance(self.arg, unicode): return self.arg @@ -110,7 +110,7 @@ def __str__(self): str: UTF-8 encoded ``alfredworkflow`` JSON object """ - return unicode(self).encode('utf-8') + return unicode(self).encode("utf-8") class Modifier(object): @@ -153,8 +153,9 @@ class Modifier(object): """ - def __init__(self, key, subtitle=None, arg=None, valid=None, icon=None, - icontype=None): + def __init__( + self, key, subtitle=None, arg=None, valid=None, icon=None, icontype=None + ): """Create a new :class:`Modifier`. Don't use this class directly (as it won't be associated with any @@ -216,23 +217,23 @@ def obj(self): o = {} if self.subtitle is not None: - o['subtitle'] = self.subtitle + o["subtitle"] = self.subtitle if self.arg is not None: - o['arg'] = self.arg + o["arg"] = self.arg if self.valid is not None: - o['valid'] = self.valid + o["valid"] = self.valid if self.variables: - o['variables'] = self.variables + o["variables"] = self.variables if self.config: - o['config'] = self.config + o["config"] = self.config icon = self._icon() if icon: - o['icon'] = icon + o["icon"] = icon return o @@ -245,10 +246,10 @@ def _icon(self): """ icon = {} if self.icon is not None: - icon['path'] = self.icon + icon["path"] = self.icon if self.icontype is not None: - icon['type'] = self.icontype + icon["type"] = self.icontype return icon @@ -265,9 +266,22 @@ class Item3(object): """ - def __init__(self, title, subtitle='', arg=None, autocomplete=None, - match=None, valid=False, uid=None, icon=None, icontype=None, - type=None, largetext=None, copytext=None, quicklookurl=None): + def __init__( + self, + title, + subtitle="", + arg=None, + autocomplete=None, + match=None, + valid=False, + uid=None, + icon=None, + icontype=None, + type=None, + largetext=None, + copytext=None, + quicklookurl=None, + ): """Create a new :class:`Item3` object. Use same arguments as for @@ -318,8 +332,9 @@ def getvar(self, name, default=None): """ return self.variables.get(name, default) - def add_modifier(self, key, subtitle=None, arg=None, valid=None, icon=None, - icontype=None): + def add_modifier( + self, key, subtitle=None, arg=None, valid=None, icon=None, icontype=None + ): """Add alternative values for a modifier key. Args: @@ -358,49 +373,49 @@ def obj(self): """ # Required values o = { - 'title': self.title, - 'subtitle': self.subtitle, - 'valid': self.valid, + "title": self.title, + "subtitle": self.subtitle, + "valid": self.valid, } # Optional values if self.arg is not None: - o['arg'] = self.arg + o["arg"] = self.arg if self.autocomplete is not None: - o['autocomplete'] = self.autocomplete + o["autocomplete"] = self.autocomplete if self.match is not None: - o['match'] = self.match + o["match"] = self.match if self.uid is not None: - o['uid'] = self.uid + o["uid"] = self.uid if self.type is not None: - o['type'] = self.type + o["type"] = self.type if self.quicklookurl is not None: - o['quicklookurl'] = self.quicklookurl + o["quicklookurl"] = self.quicklookurl if self.variables: - o['variables'] = self.variables + o["variables"] = self.variables if self.config: - o['config'] = self.config + o["config"] = self.config # Largetype and copytext text = self._text() if text: - o['text'] = text + o["text"] = text icon = self._icon() if icon: - o['icon'] = icon + o["icon"] = icon # Modifiers mods = self._modifiers() if mods: - o['mods'] = mods + o["mods"] = mods return o @@ -413,10 +428,10 @@ def _icon(self): """ icon = {} if self.icon is not None: - icon['path'] = self.icon + icon["path"] = self.icon if self.icontype is not None: - icon['type'] = self.icontype + icon["type"] = self.icontype return icon @@ -429,10 +444,10 @@ def _text(self): """ text = {} if self.largetext is not None: - text['largetype'] = self.largetext + text["largetype"] = self.largetext if self.copytext is not None: - text['copy'] = self.copytext + text["copy"] = self.copytext return text @@ -477,25 +492,27 @@ def __init__(self, **kwargs): self.variables = {} self._rerun = 0 # Get session ID from environment if present - self._session_id = os.getenv('_WF_SESSION_ID') or None + self._session_id = os.getenv("_WF_SESSION_ID") or None if self._session_id: - self.setvar('_WF_SESSION_ID', self._session_id) + self.setvar("_WF_SESSION_ID", self._session_id) @property def _default_cachedir(self): """Alfred 4's default cache directory.""" return os.path.join( os.path.expanduser( - '~/Library/Caches/com.runningwithcrayons.Alfred/' - 'Workflow Data/'), - self.bundleid) + "~/Library/Caches/com.runningwithcrayons.Alfred/" "Workflow Data/" + ), + self.bundleid, + ) @property def _default_datadir(self): """Alfred 4's default data directory.""" - return os.path.join(os.path.expanduser( - '~/Library/Application Support/Alfred/Workflow Data/'), - self.bundleid) + return os.path.join( + os.path.expanduser("~/Library/Application Support/Alfred/Workflow Data/"), + self.bundleid, + ) @property def rerun(self): @@ -524,8 +541,9 @@ def session_id(self): """ if not self._session_id: from uuid import uuid4 + self._session_id = uuid4().hex - self.setvar('_WF_SESSION_ID', self._session_id) + self.setvar("_WF_SESSION_ID", self._session_id) return self._session_id @@ -548,9 +566,11 @@ def setvar(self, name, value, persist=False): self.variables[name] = value if persist: from .util import set_config + set_config(name, value, self.bundleid) - self.logger.debug('saved variable %r with value %r to info.plist', - name, value) + self.logger.debug( + "saved variable %r with value %r to info.plist", name, value + ) def getvar(self, name, default=None): """Return value of workflow variable for ``name`` or ``default``. @@ -565,9 +585,22 @@ def getvar(self, name, default=None): """ return self.variables.get(name, default) - def add_item(self, title, subtitle='', arg=None, autocomplete=None, - valid=False, uid=None, icon=None, icontype=None, type=None, - largetext=None, copytext=None, quicklookurl=None, match=None): + def add_item( + self, + title, + subtitle="", + arg=None, + autocomplete=None, + valid=False, + uid=None, + icon=None, + icontype=None, + type=None, + largetext=None, + copytext=None, + quicklookurl=None, + match=None, + ): """Add an item to be output to Alfred. Args: @@ -589,9 +622,21 @@ def add_item(self, title, subtitle='', arg=None, autocomplete=None, Item3: Alfred feedback item. """ - item = self.item_class(title, subtitle, arg, autocomplete, - match, valid, uid, icon, icontype, type, - largetext, copytext, quicklookurl) + item = self.item_class( + title, + subtitle, + arg, + autocomplete, + match, + valid, + uid, + icon, + icontype, + type, + largetext, + copytext, + quicklookurl, + ) # Add variables to child item item.variables.update(self.variables) @@ -602,7 +647,7 @@ def add_item(self, title, subtitle='', arg=None, autocomplete=None, @property def _session_prefix(self): """Filename prefix for current session.""" - return '_wfsess-{0}-'.format(self.session_id) + return "_wfsess-{0}-".format(self.session_id) def _mk_session_name(self, name): """New cache name/key based on session ID.""" @@ -672,11 +717,13 @@ def clear_session_cache(self, current=False): current session. """ + def _is_session_file(filename): if current: - return filename.startswith('_wfsess-') - return filename.startswith('_wfsess-') \ - and not filename.startswith(self._session_prefix) + return filename.startswith("_wfsess-") + return filename.startswith("_wfsess-") and not filename.startswith( + self._session_prefix + ) self.clear_cache(_is_session_file) @@ -692,14 +739,14 @@ def obj(self): for item in self._items: items.append(item.obj) - o = {'items': items} + o = {"items": items} if self.variables: - o['variables'] = self.variables + o["variables"] = self.variables if self.rerun: - o['rerun'] = self.rerun + o["rerun"] = self.rerun return o - def warn_empty(self, title, subtitle=u'', icon=None): + def warn_empty(self, title, subtitle="", icon=None): """Add a warning to feedback if there are no items. .. versionadded:: 1.31 @@ -728,7 +775,7 @@ def warn_empty(self, title, subtitle=u'', icon=None): def send_feedback(self): """Print stored items to console/Alfred as JSON.""" if self.debugging: - json.dump(self.obj, sys.stdout, indent=2, separators=(',', ': ')) + json.dump(self.obj, sys.stdout, indent=2, separators=(",", ": ")) else: json.dump(self.obj, sys.stdout) sys.stdout.flush()