diff --git a/Fixum-0.3.alfredworkflow b/Fixum-0.3.alfredworkflow
deleted file mode 100644
index 6f24dd1..0000000
Binary files a/Fixum-0.3.alfredworkflow and /dev/null differ
diff --git a/Fixum-0.4.alfredworkflow b/Fixum-0.4.alfredworkflow
new file mode 100644
index 0000000..228fabd
Binary files /dev/null and b/Fixum-0.4.alfredworkflow differ
diff --git a/src/info.plist b/src/info.plist
index ffafe7c..3d3f07a 100644
--- a/src/info.plist
+++ b/src/info.plist
@@ -48,29 +48,52 @@
config
- lastpathcomponent
-
- onlyshowifquerypopulated
-
- removeextension
+ concurrently
- text
- {query}
- title
- Fixum
+ escaping
+ 102
+ script
+ mode=$1
+datadir="$alfred_workflow_data"
+cachedir="$alfred_workflow_cache"
+blacklist="${datadir}/blacklist.txt"
+logfile="${cachedir}/net.deanishe.alfred.fixum.log"
+
+# create data & cache directories, logfile and blacklist
+test -d "$cachedir" || mkdir -p "$cachedir"
+test -f "$logfile" || touch "$logfile"
+
+test -d "$datadir" || mkdir -p "$datadir"
+test -f "$blacklist" || cp blacklist.default.txt "$blacklist"
+
+# script actions
+[[ "$mode" = dryrun ]] && /usr/bin/python fixum.py --nothing
+[[ "$mode" = fix ]] && /usr/bin/python fixum.py
+[[ "$mode" = blacklist ]] && open "$blacklist"
+[[ "$mode" = log ]] && open -a Console "$logfile"
+
+exit 0
+ scriptargtype
+ 1
+ scriptfile
+
+ type
+ 5
type
- alfred.workflow.output.notification
+ alfred.workflow.action.script
uid
- 90302262-60E4-4C1C-AAEA-2A5C3F4C025A
+ 97033D94-9B6F-446C-94E5-AB677B5ABB4F
version
- 1
+ 2
config
alfredfiltersresults
+ argumenttrimmode
+ 0
argumenttype
1
escaping
@@ -112,44 +135,23 @@
config
- concurrently
+ lastpathcomponent
- escaping
- 102
- script
- mode=$1
-datadir="$alfred_workflow_data"
-cachedir="$alfred_workflow_cache"
-blacklist="${datadir}/blacklist.txt"
-logfile="${cachedir}/net.deanishe.alfred.fixum.log"
-
-# create data & cache directories, logfile and blacklist
-test -d "$cachedir" || mkdir -p "$cachedir"
-test -f "$logfile" || touch "$logfile"
-
-test -d "$datadir" || mkdir -p "$datadir"
-test -f "$blacklist" || cp blacklist.default.txt "$blacklist"
-
-# script actions
-[[ "$mode" = dryrun ]] && /usr/bin/python fixum.py --nothing
-[[ "$mode" = fix ]] && /usr/bin/python fixum.py
-[[ "$mode" = blacklist ]] && open "$blacklist"
-[[ "$mode" = log ]] && open -a Console "$logfile"
-
-exit 0
- scriptargtype
- 1
- scriptfile
-
- type
- 5
+ onlyshowifquerypopulated
+
+ removeextension
+
+ text
+ {query}
+ title
+ Fixum
type
- alfred.workflow.action.script
+ alfred.workflow.output.notification
uid
- 97033D94-9B6F-446C-94E5-AB677B5ABB4F
+ 90302262-60E4-4C1C-AAEA-2A5C3F4C025A
version
- 2
+ 1
readme
@@ -181,7 +183,7 @@ It is primarily a workaround to fix bugs that are preventing the workflows from
version
- 0.3
+ 0.4
webaddress
diff --git a/src/workflow/.alfredversionchecked b/src/workflow/.alfredversionchecked
new file mode 100644
index 0000000..e69de29
diff --git a/src/workflow/__init__.py b/src/workflow/__init__.py
index 3069e51..2c4f8c0 100644
--- a/src/workflow/__init__.py
+++ b/src/workflow/__init__.py
@@ -64,7 +64,7 @@
__version__ = open(os.path.join(os.path.dirname(__file__), 'version')).read()
__author__ = 'Dean Jackson'
__licence__ = 'MIT'
-__copyright__ = 'Copyright 2014 Dean Jackson'
+__copyright__ = 'Copyright 2014-2017 Dean Jackson'
__all__ = [
'Variables',
diff --git a/src/workflow/background.py b/src/workflow/background.py
index 7bda3f5..a382000 100644
--- a/src/workflow/background.py
+++ b/src/workflow/background.py
@@ -8,7 +8,14 @@
# Created on 2014-04-06
#
-"""Run background tasks."""
+"""
+This module provides an API to run commands in background processes.
+Combine with the :ref:`caching API ` to work from cached data
+while you fetch fresh data in the background.
+
+See :ref:`the User Manual ` for more information
+and examples.
+"""
from __future__ import print_function, unicode_literals
@@ -31,6 +38,10 @@ def wf():
return _wf
+def _log():
+ return wf().logger
+
+
def _arg_cache(name):
"""Return path to pickle cache file for arguments.
@@ -40,7 +51,7 @@ def _arg_cache(name):
:rtype: ``unicode`` filepath
"""
- return wf().cachefile('{0}.argcache'.format(name))
+ return wf().cachefile(name + '.argcache')
def _pid_file(name):
@@ -52,7 +63,7 @@ def _pid_file(name):
:rtype: ``unicode`` filepath
"""
- return wf().cachefile('{0}.pid'.format(name))
+ return wf().cachefile(name + '.pid')
def _process_exists(pid):
@@ -72,12 +83,12 @@ def _process_exists(pid):
def is_running(name):
- """Test whether task is running under ``name``.
+ """Test whether task ``name`` is currently running.
:param name: name of task
- :type name: ``unicode``
+ :type name: unicode
:returns: ``True`` if task with name ``name`` is running, else ``False``
- :rtype: ``Boolean``
+ :rtype: bool
"""
pidfile = _pid_file(name)
@@ -114,8 +125,7 @@ def _fork_and_exit_parent(errmsg):
if pid > 0:
os._exit(0)
except OSError as err:
- wf().logger.critical('%s: (%d) %s', errmsg, err.errno,
- err.strerror)
+ _log().critical('%s: (%d) %s', errmsg, err.errno, err.strerror)
raise err
# Do first fork.
@@ -145,11 +155,11 @@ def run_in_background(name, args, **kwargs):
r"""Cache arguments then call this script again via :func:`subprocess.call`.
:param name: name of task
- :type name: ``unicode``
+ :type name: unicode
:param args: arguments passed as first argument to :func:`subprocess.call`
:param \**kwargs: keyword arguments to :func:`subprocess.call`
:returns: exit code of sub-process
- :rtype: ``int``
+ :rtype: int
When you call this function, it caches its arguments and then calls
``background.py`` in a subprocess. The Python subprocess will load the
@@ -167,7 +177,7 @@ def run_in_background(name, args, **kwargs):
"""
if is_running(name):
- wf().logger.info('Task `{0}` is already running'.format(name))
+ _log().info('[%s] job already running', name)
return
argcache = _arg_cache(name)
@@ -175,16 +185,16 @@ def run_in_background(name, args, **kwargs):
# Cache arguments
with open(argcache, 'wb') as file_obj:
pickle.dump({'args': args, 'kwargs': kwargs}, file_obj)
- wf().logger.debug('Command arguments cached to `{0}`'.format(argcache))
+ _log().debug('[%s] command cached: %s', name, argcache)
# Call this script
cmd = ['/usr/bin/python', __file__, name]
- wf().logger.debug('Calling {0!r} ...'.format(cmd))
+ _log().debug('[%s] passing job to background runner: %r', name, cmd)
retcode = subprocess.call(cmd)
if retcode: # pragma: no cover
- wf().logger.error('Failed to call task in background')
+ _log().error('[%s] background runner failed with %d', retcode)
else:
- wf().logger.debug('Executing task `{0}` in background...'.format(name))
+ _log().debug('[%s] background job started', name)
return retcode
@@ -195,10 +205,11 @@ def main(wf): # pragma: no cover
:meth:`subprocess.call` with cached arguments.
"""
+ log = wf.logger
name = wf.args[0]
argcache = _arg_cache(name)
if not os.path.exists(argcache):
- wf.logger.critical('No arg cache found : {0!r}'.format(argcache))
+ log.critical('[%s] command cache not found: %r', name, argcache)
return 1
# Load cached arguments
@@ -219,23 +230,21 @@ def main(wf): # pragma: no cover
# Write PID to file
with open(pidfile, 'wb') as file_obj:
- file_obj.write('{0}'.format(os.getpid()))
+ file_obj.write(str(os.getpid()))
# Run the command
try:
- wf.logger.debug('Task `{0}` running'.format(name))
- wf.logger.debug('cmd : {0!r}'.format(args))
+ log.debug('[%s] running command: %r', name, args)
retcode = subprocess.call(args, **kwargs)
if retcode:
- wf.logger.error('Command failed with [{0}] : {1!r}'.format(
- retcode, args))
+ log.error('[%s] command failed with status %d', name, retcode)
finally:
if os.path.exists(pidfile):
os.unlink(pidfile)
- wf.logger.debug('Task `{0}` finished'.format(name))
+ log.debug('[%s] job complete', name)
if __name__ == '__main__': # pragma: no cover
diff --git a/src/workflow/notify.py b/src/workflow/notify.py
index 3ed1e5e..4542c78 100644
--- a/src/workflow/notify.py
+++ b/src/workflow/notify.py
@@ -11,7 +11,7 @@
# TODO: Exclude this module from test and code coverage in py2.6
"""
-Post notifications via the OS X Notification Center. This feature
+Post notifications via the macOS Notification Center. This feature
is only available on Mountain Lion (10.8) and later. It will
silently fail on older systems.
@@ -60,10 +60,10 @@
def wf():
- """Return `Workflow` object for this module.
+ """Return Workflow object for this module.
Returns:
- workflow.Workflow: `Workflow` object for current workflow.
+ workflow.Workflow: Workflow object for current workflow.
"""
global _wf
if _wf is None:
@@ -87,7 +87,7 @@ def notifier_program():
"""Return path to notifier applet executable.
Returns:
- unicode: Path to Notify.app `applet` executable.
+ unicode: Path to Notify.app ``applet`` executable.
"""
return wf().datafile('Notify.app/Contents/MacOS/applet')
@@ -96,13 +96,13 @@ def notifier_icon_path():
"""Return path to icon file in installed Notify.app.
Returns:
- unicode: Path to `applet.icns` within the app bundle.
+ unicode: Path to ``applet.icns`` within the app bundle.
"""
return wf().datafile('Notify.app/Contents/Resources/applet.icns')
def install_notifier():
- """Extract `Notify.app` from the workflow to data directory.
+ """Extract ``Notify.app`` from the workflow to data directory.
Changes the bundle ID of the installed app and gives it the
workflow's icon.
@@ -111,13 +111,13 @@ def install_notifier():
destdir = wf().datadir
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.extractall(destdir)
- assert os.path.exists(n), (
- "Notify.app could not be installed in {0!r}.".format(destdir))
+ assert os.path.exists(n), \
+ 'Notify.app could not be installed in %s' % destdir
# Replace applet icon
icon = notifier_icon_path()
@@ -144,29 +144,29 @@ def install_notifier():
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 {0!r}'.format(bundle_id))
+ log().debug('changing bundle ID to %r', bundle_id)
data['CFBundleIdentifier'] = bundle_id
plistlib.writePlist(data, ip_path)
def validate_sound(sound):
- """Coerce `sound` to valid sound name.
+ """Coerce ``sound`` to valid sound name.
- Returns `None` for invalid sounds. Sound names can be found
- in `System Preferences > Sound > Sound Effects`.
+ Returns ``None`` for invalid sounds. Sound names can be found
+ in ``System Preferences > Sound > Sound Effects``.
Args:
sound (str): Name of system sound.
Returns:
- str: Proper name of sound or `None`.
+ str: Proper name of sound or ``None``.
"""
if not sound:
return None
# Case-insensitive comparison of `sound`
if sound.lower() in [s.lower() for s in SOUNDS]:
- # Title-case is correct for all system sounds as of OS X 10.11
+ # Title-case is correct for all system sounds as of macOS 10.11
return sound.title()
return None
@@ -180,10 +180,10 @@ def notify(title='', text='', sound=None):
sound (str, optional): Name of sound to play.
Raises:
- ValueError: Raised if both `title` and `text` are empty.
+ ValueError: Raised if both ``title`` and ``text`` are empty.
Returns:
- bool: `True` if notification was posted, else `False`.
+ bool: ``True`` if notification was posted, else ``False``.
"""
if title == text == '':
raise ValueError('Empty notification')
@@ -210,7 +210,7 @@ def notify(title='', text='', sound=None):
def convert_image(inpath, outpath, size):
- """Convert an image file using `sips`.
+ """Convert an image file using ``sips``.
Args:
inpath (str): Path of source file.
@@ -218,11 +218,11 @@ def convert_image(inpath, outpath, size):
size (int): Width and height of destination image in pixels.
Raises:
- RuntimeError: Raised if `sips` exits with non-zero status.
+ RuntimeError: Raised if ``sips`` exits with non-zero status.
"""
cmd = [
b'sips',
- b'-z', b'{0}'.format(size), b'{0}'.format(size),
+ b'-z', str(size), str(size),
inpath,
b'--out', outpath]
# log().debug(cmd)
@@ -230,14 +230,14 @@ def convert_image(inpath, outpath, size):
retcode = subprocess.call(cmd, stdout=pipe, stderr=subprocess.STDOUT)
if retcode != 0:
- raise RuntimeError('sips exited with {0}'.format(retcode))
+ raise RuntimeError('sips exited with %d' % retcode)
def png_to_icns(png_path, icns_path):
- """Convert PNG file to ICNS using `iconutil`.
+ """Convert PNG file to ICNS using ``iconutil``.
Create an iconset from the source PNG file. Generate PNG files
- in each size required by OS X, then call `iconutil` to turn
+ in each size required by macOS, then call ``iconutil`` to turn
them into a single ICNS file.
Args:
@@ -245,15 +245,15 @@ def png_to_icns(png_path, icns_path):
icns_path (str): Path to destination ICNS file.
Raises:
- RuntimeError: Raised if `iconutil` or `sips` fail.
+ RuntimeError: Raised if ``iconutil`` or ``sips`` fail.
"""
tempdir = tempfile.mkdtemp(prefix='aw-', dir=wf().datadir)
try:
iconset = os.path.join(tempdir, 'Icon.iconset')
- assert not os.path.exists(iconset), (
- "Iconset path already exists : {0!r}".format(iconset))
+ assert not os.path.exists(iconset), \
+ 'iconset already exists: ' + iconset
os.makedirs(iconset)
# Copy source icon to icon set and generate all the other
@@ -261,7 +261,7 @@ def png_to_icns(png_path, icns_path):
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}@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'))
@@ -280,10 +280,10 @@ def png_to_icns(png_path, icns_path):
retcode = subprocess.call(cmd)
if retcode != 0:
- raise RuntimeError("iconset exited with {0}".format(retcode))
+ raise RuntimeError('iconset exited with %d' % retcode)
- assert os.path.exists(icns_path), (
- "Generated ICNS file not found : {0!r}".format(icns_path))
+ assert os.path.exists(icns_path), \
+ 'generated ICNS file not found: ' + repr(icns_path)
finally:
try:
shutil.rmtree(tempdir)
@@ -291,36 +291,6 @@ def png_to_icns(png_path, icns_path):
pass
-# def notify_native(title='', text='', sound=''):
-# """Post notification via the native API (via pyobjc).
-
-# At least one of `title` or `text` must be specified.
-
-# This method will *always* show the Python launcher icon (i.e. the
-# rocket with the snakes on it).
-
-# Args:
-# title (str, optional): Notification title.
-# text (str, optional): Notification body text.
-# sound (str, optional): Name of sound to play.
-
-# """
-
-# if title == text == '':
-# raise ValueError('Empty notification')
-
-# import Foundation
-
-# sound = sound or Foundation.NSUserNotificationDefaultSoundName
-
-# n = Foundation.NSUserNotification.alloc().init()
-# n.setTitle_(title)
-# n.setInformativeText_(text)
-# n.setSoundName_(sound)
-# nc = Foundation.NSUserNotificationCenter.defaultUserNotificationCenter()
-# nc.deliverNotification_(n)
-
-
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
@@ -329,21 +299,20 @@ def png_to_icns(png_path, icns_path):
from unicodedata import normalize
- def uni(s):
+ def ustr(s):
"""Coerce `s` to normalised Unicode."""
- ustr = s.decode('utf-8')
- return normalize('NFD', ustr)
+ 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=uni,
+ help="Notification title.", type=ustr,
default='')
- p.add_argument('-s', '--sound', type=uni,
+ p.add_argument('-s', '--sound', type=ustr,
help="Optional notification sound.", default='')
- p.add_argument('text', type=uni,
+ p.add_argument('text', type=ustr,
help="Notification body text.", default='', nargs='?')
o = p.parse_args()
@@ -357,21 +326,20 @@ def uni(s):
if o.png:
icns = os.path.join(
os.path.dirname(o.png),
- b'{0}{1}'.format(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),
+ print('converting {0!r} to {1!r} ...'.format(o.png, icns),
file=sys.stderr)
- assert not os.path.exists(icns), (
- "Destination file already exists : {0}".format(icns))
+ assert not os.path.exists(icns), \
+ '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)
+ print('ERROR: empty notification.', file=sys.stderr)
sys.exit(1)
else:
notify(o.title, o.text, o.sound)
diff --git a/src/workflow/update.py b/src/workflow/update.py
index bb8e9da..37569bb 100644
--- a/src/workflow/update.py
+++ b/src/workflow/update.py
@@ -94,7 +94,7 @@ def _parse(self, vstr):
else:
m = self.match_version(vstr)
if not m:
- raise ValueError('Invalid version number: {0}'.format(vstr))
+ raise ValueError('invalid version number: {0}'.format(vstr))
version, suffix = m.groups()
parts = self._parse_dotted_string(version)
@@ -104,7 +104,7 @@ def _parse(self, vstr):
if len(parts):
self.patch = parts.pop(0)
if not len(parts) == 0:
- raise ValueError('Invalid version (too long) : {0}'.format(vstr))
+ raise ValueError('invalid version (too long) : {0}'.format(vstr))
if suffix:
# Build info
@@ -115,8 +115,7 @@ def _parse(self, vstr):
if suffix:
if not suffix.startswith('-'):
raise ValueError(
- 'Invalid suffix : `{0}`. Must start with `-`'.format(
- suffix))
+ 'suffix must start with - : {0}'.format(suffix))
self.suffix = suffix[1:]
# wf().logger.debug('version str `{}` -> {}'.format(vstr, repr(self)))
@@ -139,7 +138,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:
@@ -157,7 +156,7 @@ def __lt__(self, other):
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):
@@ -167,13 +166,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):
@@ -184,9 +183,9 @@ def __str__(self):
"""Return semantic version string."""
vstr = '{0}.{1}.{2}'.format(self.major, self.minor, self.patch)
if self.suffix:
- vstr += '-{0}'.format(self.suffix)
+ vstr = '{0}-{1}'.format(vstr, self.suffix)
if self.build:
- vstr += '+{0}'.format(self.build)
+ vstr = '{0}+{1}'.format(vstr, self.build)
return vstr
def __repr__(self):
@@ -201,16 +200,16 @@ def download_workflow(url):
:returns: path to downloaded file
"""
- filename = url.split("/")[-1]
+ filename = url.split('/')[-1]
if (not filename.endswith('.alfredworkflow') and
not filename.endswith('.alfred3workflow')):
- raise ValueError('Attachment `{0}` not a workflow'.format(filename))
+ raise ValueError('attachment not a workflow: {0}'.format(filename))
local_path = os.path.join(tempfile.gettempdir(), filename)
wf().logger.debug(
- 'Downloading updated workflow from `%s` to `%s` ...', url, local_path)
+ 'downloading updated workflow from `%s` to `%s` ...', url, local_path)
response = web.get(url)
@@ -228,7 +227,7 @@ def build_api_url(slug):
"""
if len(slug.split('/')) != 2:
- raise ValueError('Invalid GitHub slug : {0}'.format(slug))
+ raise ValueError('invalid GitHub slug: {0}'.format(slug))
return RELEASES_BASE.format(slug)
@@ -261,13 +260,13 @@ def _validate_release(release):
if dl_count == 0:
wf().logger.warning(
- 'Invalid release %s : No workflow file', version)
+ 'invalid release (no workflow file): %s', version)
return None
for k in downloads:
if len(downloads[k]) > 1:
wf().logger.warning(
- 'Invalid release %s : multiple %s files', version, k)
+ 'invalid release (multiple %s files): %s', k, version)
return None
# Prefer .alfred3workflow file if there is one and Alfred 3 is
@@ -278,7 +277,7 @@ def _validate_release(release):
else:
download_url = downloads['.alfredworkflow'][0]
- wf().logger.debug('Release `%s` : %s', version, download_url)
+ wf().logger.debug('release %s: %s', version, download_url)
return {
'version': version,
@@ -306,28 +305,27 @@ def get_valid_releases(github_slug, prereleases=False):
api_url = build_api_url(github_slug)
releases = []
- wf().logger.debug('Retrieving releases list from `%s` ...', api_url)
+ wf().logger.debug('retrieving releases list: %s', api_url)
def retrieve_releases():
wf().logger.info(
- 'Retrieving releases for `%s` ...', github_slug)
+ 'retrieving releases: %s', github_slug)
return web.get(api_url).json()
slug = github_slug.replace('/', '-')
- for release in wf().cached_data('gh-releases-{0}'.format(slug),
- retrieve_releases):
-
- wf().logger.debug('Release : %r', release)
+ for release in wf().cached_data('gh-releases-' + slug, retrieve_releases):
release = _validate_release(release)
if release is None:
- wf().logger.debug('Invalid release')
+ wf().logger.debug('invalid release: %r', release)
continue
elif release['prerelease'] and not prereleases:
- wf().logger.debug('Ignoring prerelease : %s', release['version'])
+ wf().logger.debug('ignoring prerelease: %s', release['version'])
continue
+ wf().logger.debug('release: %r', release)
+
releases.append(release)
return releases
@@ -349,10 +347,10 @@ def check_update(github_slug, current_version, prereleases=False):
"""
releases = get_valid_releases(github_slug, prereleases)
- wf().logger.info('%d releases for %s', len(releases), github_slug)
-
if not len(releases):
- raise ValueError('No valid releases for %s', github_slug)
+ raise ValueError('no valid releases for %s', github_slug)
+
+ wf().logger.info('%d releases for %s', len(releases), github_slug)
# GitHub returns releases newest-first
latest_release = releases[0]
@@ -360,7 +358,7 @@ def check_update(github_slug, current_version, prereleases=False):
# (latest_version, download_url) = get_latest_release(releases)
vr = Version(latest_release['version'])
vl = Version(current_version)
- wf().logger.debug('Latest : %r Installed : %r', vr, vl)
+ wf().logger.debug('latest=%r, installed=%r', vr, vl)
if vr > vl:
wf().cache_data('__workflow_update_status', {
@@ -371,9 +369,7 @@ def check_update(github_slug, current_version, prereleases=False):
return True
- wf().cache_data('__workflow_update_status', {
- 'available': False
- })
+ wf().cache_data('__workflow_update_status', {'available': False})
return False
@@ -386,12 +382,12 @@ def install_update():
update_data = wf().cached_data('__workflow_update_status', max_age=0)
if not update_data or not update_data.get('available'):
- wf().logger.info('No update available')
+ wf().logger.info('no update available')
return False
local_file = download_workflow(update_data['download_url'])
- wf().logger.info('Installing updated workflow ...')
+ wf().logger.info('installing updated workflow ...')
subprocess.call(['open', local_file])
update_data['available'] = False
@@ -402,27 +398,29 @@ def install_update():
if __name__ == '__main__': # pragma: nocover
import sys
- def show_help():
+ def show_help(status=0):
"""Print help message."""
- print('Usage : update.py (check|install) github_slug version '
- '[--prereleases]')
- sys.exit(1)
+ print('Usage : update.py (check|install) '
+ '[--prereleases] ')
+ sys.exit(status)
argv = sys.argv[:]
+ if '-h' in argv or '--help' in argv:
+ show_help()
+
prereleases = '--prereleases' in argv
if prereleases:
argv.remove('--prereleases')
if len(argv) != 4:
- show_help()
+ show_help(1)
action, github_slug, version = argv[1:]
- if action not in ('check', 'install'):
- show_help()
-
if action == 'check':
check_update(github_slug, version, prereleases)
elif action == 'install':
install_update()
+ else:
+ show_help(1)
diff --git a/src/workflow/version b/src/workflow/version
index c8d3893..ec8f6a3 100644
--- a/src/workflow/version
+++ b/src/workflow/version
@@ -1 +1 @@
-1.26
\ No newline at end of file
+1.27
\ No newline at end of file
diff --git a/src/workflow/web.py b/src/workflow/web.py
index 748b199..d64bb6f 100644
--- a/src/workflow/web.py
+++ b/src/workflow/web.py
@@ -77,8 +77,10 @@
def str_dict(dic):
"""Convert keys and values in ``dic`` into UTF-8-encoded :class:`str`.
- :param dic: :class:`dict` of Unicode strings
- :returns: :class:`dict`
+ :param dic: Mapping of Unicode strings
+ :type dic: dict
+ :returns: Dictionary containing only UTF-8 strings
+ :rtype: dict
"""
if isinstance(dic, CaseInsensitiveDictionary):
@@ -191,7 +193,7 @@ def __init__(self, request, stream=False):
:param request: :class:`urllib2.Request` instance
:param stream: Whether to stream response or retrieve it all at once
- :type stream: ``bool``
+ :type stream: bool
"""
self.request = request
@@ -263,7 +265,7 @@ def json(self):
"""Decode response contents as JSON.
:returns: object decoded from JSON
- :rtype: :class:`list` / :class:`dict`
+ :rtype: list, dict or unicode
"""
return json.loads(self.content, self.encoding or 'utf-8')
@@ -272,7 +274,8 @@ def json(self):
def encoding(self):
"""Text encoding of document or ``None``.
- :returns: :class:`str` or ``None``
+ :returns: Text encoding if found.
+ :rtype: str or ``None``
"""
if not self._encoding:
@@ -285,7 +288,7 @@ def content(self):
"""Raw content of response (i.e. bytes).
:returns: Body of HTTP response
- :rtype: :class:`str`
+ :rtype: str
"""
if not self._content:
@@ -310,7 +313,7 @@ def text(self):
itself, the encoded response body will be returned instead.
:returns: Body of HTTP response
- :rtype: :class:`unicode` or :class:`str`
+ :rtype: unicode or str
"""
if self.encoding:
@@ -324,9 +327,9 @@ def iter_content(self, chunk_size=4096, decode_unicode=False):
.. versionadded:: 1.6
:param chunk_size: Number of bytes to read into memory
- :type chunk_size: ``int``
+ :type chunk_size: int
:param decode_unicode: Decode to Unicode using detected encoding
- :type decode_unicode: ``Boolean``
+ :type decode_unicode: bool
:returns: iterator
"""
@@ -406,7 +409,7 @@ def _get_encoding(self):
"""Get encoding from HTTP headers or content.
:returns: encoding or `None`
- :rtype: ``unicode`` or ``None``
+ :rtype: unicode or ``None``
"""
headers = self.raw.info()
@@ -458,29 +461,30 @@ def request(method, url, params=None, data=None, headers=None, cookies=None,
"""Initiate an HTTP(S) request. Returns :class:`Response` object.
:param method: 'GET' or 'POST'
- :type method: ``unicode``
+ :type method: unicode
:param url: URL to open
- :type url: ``unicode``
+ :type url: unicode
:param params: mapping of URL parameters
- :type params: :class:`dict`
+ :type params: dict
:param data: mapping of form data ``{'field_name': 'value'}`` or
:class:`str`
- :type data: :class:`dict` or :class:`str`
+ :type data: dict or str
:param headers: HTTP headers
- :type headers: :class:`dict`
+ :type headers: dict
:param cookies: cookies to send to server
- :type cookies: :class:`dict`
+ :type cookies: dict
:param files: files to upload (see below).
- :type files: :class:`dict`
+ :type files: dict
:param auth: username, password
- :type auth: ``tuple``
+ :type auth: tuple
:param timeout: connection timeout limit in seconds
- :type timeout: ``int``
+ :type timeout: int
:param allow_redirects: follow redirections
- :type allow_redirects: ``Boolean``
+ :type allow_redirects: bool
:param stream: Stream content instead of fetching it all at once.
- :type stream: ``bool``
- :returns: :class:`Response` object
+ :type stream: bool
+ :returns: Response object
+ :rtype: :class:`Response`
The ``files`` argument is a dictionary::
@@ -594,11 +598,12 @@ def encode_multipart_formdata(fields, files):
"""Encode form data (``fields``) and ``files`` for POST request.
:param fields: mapping of ``{name : value}`` pairs for normal form fields.
- :type fields: :class:`dict`
+ :type fields: dict
:param files: dictionary of fieldnames/files elements for file data.
See below for details.
- :type files: :class:`dict` of :class:`dicts`
- :returns: ``(headers, body)`` ``headers`` is a :class:`dict` of HTTP headers
+ :type files: dict of :class:`dict`
+ :returns: ``(headers, body)`` ``headers`` is a
+ :class:`dict` of HTTP headers
:rtype: 2-tuple ``(dict, str)``
The ``files`` argument is a dictionary::
@@ -609,16 +614,18 @@ def encode_multipart_formdata(fields, files):
}
- ``fieldname`` is the name of the field in the HTML form.
- - ``mimetype`` is optional. If not provided, :mod:`mimetypes` will be used to guess the mimetype, or ``application/octet-stream`` will be used.
+ - ``mimetype`` is optional. If not provided, :mod:`mimetypes` will
+ be used to guess the mimetype, or ``application/octet-stream``
+ will be used.
"""
def get_content_type(filename):
"""Return or guess mimetype of ``filename``.
:param filename: filename of file
- :type filename: unicode/string
+ :type filename: unicode/str
:returns: mime-type, e.g. ``text/html``
- :rtype: :class::class:`str`
+ :rtype: str
"""
diff --git a/src/workflow/workflow.py b/src/workflow/workflow.py
index 4fd8db4..0d2dc4e 100644
--- a/src/workflow/workflow.py
+++ b/src/workflow/workflow.py
@@ -10,7 +10,7 @@
"""The :class:`Workflow` object is the main interface to this library.
:class:`Workflow` is targeted at Alfred 2. Use
-:class:`~workflow.workflow3.Workflow3` if you want to use Alfred 3's new
+:class:`~workflow.Workflow3` if you want to use Alfred 3's new
features, such as :ref:`workflow variables ` or
more powerful modifiers.
@@ -56,7 +56,7 @@
# Standard system icons
####################################################################
-# These icons are default OS X icons. They are super-high quality, and
+# These icons are default macOS icons. They are super-high quality, and
# will be familiar to users.
# This library uses `ICON_ERROR` when a workflow dies in flames, so
# in my own workflows, I use `ICON_WARNING` for less fatal errors
@@ -456,6 +456,7 @@ class KeychainError(Exception):
Raised by methods :meth:`Workflow.save_password`,
:meth:`Workflow.get_password` and :meth:`Workflow.delete_password`
when ``security`` CLI app returns an unknown error code.
+
"""
@@ -464,6 +465,7 @@ class PasswordNotFound(KeychainError):
Raised by method :meth:`Workflow.get_password` when ``account``
is unknown to the Keychain.
+
"""
@@ -473,6 +475,7 @@ class PasswordExists(KeychainError):
You should never receive this error: it is used internally
by the :meth:`Workflow.save_password` method to know if it needs
to delete the old password first (a Keychain implementation detail).
+
"""
@@ -506,13 +509,13 @@ class SerializerManager(object):
.. versionadded:: 1.8
A configured instance of this class is available at
- ``workflow.manager``.
+ :attr:`workflow.manager`.
Use :meth:`register()` to register new (or replace
existing) serializers, which you can specify by name when calling
- :class:`Workflow` data storage methods.
+ :class:`~workflow.Workflow` data storage methods.
- See :ref:`manual-serialization` and :ref:`manual-persistent-data`
+ See :ref:`guide-serialization` and :ref:`guide-persistent-data`
for further information.
"""
@@ -797,7 +800,27 @@ def elem(self):
class LockFile(object):
- """Context manager to create lock files."""
+ """Context manager to protect filepaths with lockfiles.
+
+ .. versionadded:: 1.13
+
+ Creates a lockfile alongside ``protected_path``. Other ``LockFile``
+ instances will refuse to lock the same path.
+
+ >>> path = '/path/to/file'
+ >>> with LockFile(path):
+ >>> with open(path, 'wb') as fp:
+ >>> fp.write(data)
+
+ Args:
+ protected_path (unicode): File to protect with a lockfile
+ timeout (int, optional): Raises an :class:`AcquisitionError`
+ if lock cannot be acquired within this number of seconds.
+ If ``timeout`` is 0 (the default), wait forever.
+ delay (float, optional): How often to check (in seconds) if
+ lock has been released.
+
+ """
def __init__(self, protected_path, timeout=0, delay=0.05):
"""Create new :class:`LockFile` object."""
@@ -830,14 +853,14 @@ def acquire(self, blocking=True):
try:
fd = os.open(self.lockfile, os.O_CREAT | os.O_EXCL | os.O_RDWR)
with os.fdopen(fd, 'w') as fd:
- fd.write('{0}'.format(os.getpid()))
+ fd.write(str(os.getpid()))
break
except OSError as err:
if err.errno != errno.EEXIST: # pragma: no cover
raise
if self.timeout and (time.time() - start) >= self.timeout:
- raise AcquisitionError('Lock acquisition timed out.')
+ raise AcquisitionError('lock acquisition timed out')
if not blocking:
return False
time.sleep(self.delay)
@@ -895,16 +918,16 @@ def __del__(self):
def atomic_writer(file_path, mode):
"""Atomic file writer.
- :param file_path: path of file to write to.
- :type file_path: ``unicode``
- :param mode: sames as for `func:open`
- :type mode: string
-
.. versionadded:: 1.12
Context manager that ensures the file is only written if the write
succeeds. The data is first written to a temporary file.
+ :param file_path: path of file to write to.
+ :type file_path: ``unicode``
+ :param mode: sames as for :func:`open`
+ :type mode: string
+
"""
temp_suffix = '.aw.temp'
temp_file_path = file_path + temp_suffix
@@ -920,11 +943,13 @@ def atomic_writer(file_path, mode):
class uninterruptible(object):
- """Decorator that postpones SIGTERM until wrapped function is complete.
+ """Decorator that postpones SIGTERM until wrapped function returns.
.. versionadded:: 1.12
- Since version 2.7, Alfred allows Script Filters to be killed. If
+ .. important:: This decorator is NOT thread-safe.
+
+ As of version 2.7, Alfred allows Script Filters to be killed. If
your workflow is killed in the middle of critical code (e.g.
writing data to disk), this may corrupt your workflow's data.
@@ -936,10 +961,6 @@ class uninterruptible(object):
Alfred-Workflow uses this internally to ensure its settings, data
and cache writes complete.
- .. important::
-
- This decorator is NOT thread-safe.
-
"""
def __init__(self, func, class_name=''):
@@ -1063,27 +1084,36 @@ def setdefault(self, key, value=None):
class Workflow(object):
- """Create new :class:`Workflow` instance.
+ """The ``Workflow`` object is the main interface to Alfred-Workflow.
+
+ It provides APIs for accessing the Alfred/workflow environment,
+ storing & caching data, using Keychain, and generating Script
+ Filter feedback.
+
+ ``Workflow`` is compatible with both Alfred 2 and 3. The
+ :class:`~workflow.Workflow3` subclass provides additional,
+ Alfred 3-only features, such as workflow variables.
:param default_settings: default workflow settings. If no settings file
exists, :class:`Workflow.settings` will be pre-populated with
``default_settings``.
:type default_settings: :class:`dict`
- :param update_settings: settings for updating your workflow from GitHub.
- This must be a :class:`dict` that contains ``github_slug`` and
- ``version`` keys. ``github_slug`` is of the form ``username/repo``
- and ``version`` **must** correspond to the tag of a release. The
- boolean ``prereleases`` key is optional and if ``True`` will
- override the :ref:`magic argument ` preference.
- This is only recommended when the installed workflow is a pre-release.
- See :ref:`updates` for more information.
+ :param update_settings: settings for updating your workflow from
+ GitHub releases. The only required key is ``github_slug``,
+ whose value must take the form of ``username/repo``.
+ If specified, ``Workflow`` will check the repo's releases
+ for updates. Your workflow must also have a semantic version
+ number. Please see the :ref:`User Manual ` and
+ `update API docs ` for more information.
:type update_settings: :class:`dict`
- :param input_encoding: encoding of command line arguments
+ :param input_encoding: encoding of command line arguments. You
+ should probably leave this as the default (``utf-8``), which
+ is the encoding Alfred uses.
:type input_encoding: :class:`unicode`
:param normalization: normalisation to apply to CLI args.
See :meth:`Workflow.decode` for more details.
:type normalization: :class:`unicode`
- :param capture_args: capture and act on ``workflow:*`` arguments. See
+ :param capture_args: Capture and act on ``workflow:*`` arguments. See
:ref:`Magic arguments ` for details.
:type capture_args: :class:`Boolean`
:param libraries: sequence of paths to directories containing
@@ -1176,32 +1206,32 @@ def alfred_env(self):
============================ =========================================
Variable Description
============================ =========================================
- alfred_debug Set to ``1`` if Alfred's debugger is
+ debug Set to ``1`` if Alfred's debugger is
open, otherwise unset.
- alfred_preferences Path to Alfred.alfredpreferences
+ preferences Path to Alfred.alfredpreferences
(where your workflows and settings are
stored).
- alfred_preferences_localhash Machine-specific preferences are stored
+ preferences_localhash Machine-specific preferences are stored
in ``Alfred.alfredpreferences/preferences/local/``
- (see ``alfred_preferences`` above for
+ (see ``preferences`` above for
the path to ``Alfred.alfredpreferences``)
- alfred_theme ID of selected theme
- alfred_theme_background Background colour of selected theme in
+ theme ID of selected theme
+ theme_background Background colour of selected theme in
format ``rgba(r,g,b,a)``
- alfred_theme_subtext Show result subtext.
+ theme_subtext Show result subtext.
``0`` = Always,
``1`` = Alternative actions only,
``2`` = Selected result only,
``3`` = Never
- alfred_version Alfred version number, e.g. ``'2.4'``
- alfred_version_build Alfred build number, e.g. ``277``
- alfred_workflow_bundleid Bundle ID, e.g.
+ version Alfred version number, e.g. ``'2.4'``
+ version_build Alfred build number, e.g. ``277``
+ workflow_bundleid Bundle ID, e.g.
``net.deanishe.alfred-mailto``
- alfred_workflow_cache Path to workflow's cache directory
- alfred_workflow_data Path to workflow's data directory
- alfred_workflow_name Name of current workflow
- alfred_workflow_uid UID of workflow
- alfred_workflow_version The version number specified in the
+ workflow_cache Path to workflow's cache directory
+ workflow_data Path to workflow's data directory
+ workflow_name Name of current workflow
+ workflow_uid UID of workflow
+ workflow_version The version number specified in the
workflow configuration sheet/info.plist
============================ =========================================
@@ -1611,7 +1641,7 @@ def settings_path(self):
def settings(self):
"""Return a dictionary subclass that saves itself when changed.
- See :ref:`manual-settings` in the :ref:`user-manual` for more
+ See :ref:`guide-settings` in the :ref:`user-manual` for more
information on how to use :attr:`settings` and **important
limitations** on what it can do.
@@ -1624,8 +1654,7 @@ def settings(self):
"""
if not self._settings:
- self.logger.debug('Reading settings from `{0}` ...'.format(
- self.settings_path))
+ self.logger.debug('reading settings from %s', self.settings_path)
self._settings = Settings(self.settings_path,
self._default_settings)
return self._settings
@@ -1669,8 +1698,7 @@ def cache_serializer(self, serializer_name):
'Unknown serializer : `{0}`. Register your serializer '
'with `manager` first.'.format(serializer_name))
- self.logger.debug(
- 'default cache serializer set to `{0}`'.format(serializer_name))
+ self.logger.debug('default cache serializer: %s', serializer_name)
self._cache_serializer = serializer_name
@@ -1712,8 +1740,7 @@ def data_serializer(self, serializer_name):
'Unknown serializer : `{0}`. Register your serializer '
'with `manager` first.'.format(serializer_name))
- self.logger.debug(
- 'default data serializer set to `{0}`'.format(serializer_name))
+ self.logger.debug('default data serializer: %s', serializer_name)
self._data_serializer = serializer_name
@@ -1730,7 +1757,7 @@ def stored_data(self, name):
metadata_path = self.datafile('.{0}.alfred-workflow'.format(name))
if not os.path.exists(metadata_path):
- self.logger.debug('No data stored for `{0}`'.format(name))
+ self.logger.debug('no data stored for `%s`', name)
return None
with open(metadata_path, 'rb') as file_obj:
@@ -1744,14 +1771,13 @@ def stored_data(self, name):
'serializer with `manager.register()` '
'to load this data.'.format(serializer_name))
- self.logger.debug('Data `{0}` stored in `{1}` format'.format(
- name, serializer_name))
+ self.logger.debug('data `%s` stored as `%s`', 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 for `{0}`'.format(name))
+ self.logger.debug('no data stored: %s', name)
if os.path.exists(metadata_path):
os.unlink(metadata_path)
@@ -1760,7 +1786,7 @@ def stored_data(self, name):
with open(data_path, 'rb') as file_obj:
data = serializer.load(file_obj)
- self.logger.debug('Stored data loaded from : {0}'.format(data_path))
+ self.logger.debug('stored data loaded: %s', data_path)
return data
@@ -1789,7 +1815,7 @@ def delete_paths(paths):
for path in paths:
if os.path.exists(path):
os.unlink(path)
- self.logger.debug('Deleted data file : {0}'.format(path))
+ self.logger.debug('deleted data file: %s', path)
serializer_name = serializer or self.data_serializer
@@ -1829,7 +1855,7 @@ def _store():
_store()
- self.logger.debug('Stored data saved at : {0}'.format(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.
@@ -1855,8 +1881,7 @@ def cached_data(self, name, data_func=None, max_age=60):
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 from : %s',
- cache_path)
+ self.logger.debug('loading cached data: %s', cache_path)
return serializer.load(file_obj)
if not data_func:
@@ -1885,13 +1910,13 @@ def cache_data(self, name, data):
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:
serializer.dump(data, file_obj)
- self.logger.debug('Cached data saved at : %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.
@@ -2221,8 +2246,7 @@ def run(self, func, text_errors=False):
try:
if self.version:
- self.logger.debug(
- 'Workflow version : {0}'.format(self.version))
+ self.logger.debug('workflow version: %s', self.version)
# Run update check if configured for self-updates.
# This call has to go in the `run` try-except block, as it will
@@ -2242,8 +2266,7 @@ 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: {0}'.format(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:
@@ -2263,8 +2286,8 @@ def run(self, func, text_errors=False):
return 1
finally:
- self.logger.debug('Workflow finished in {0:0.3f} seconds.'.format(
- time.time() - start))
+ self.logger.debug('workflow finished in %0.3f seconds',
+ time.time() - start)
return 0
@@ -2318,10 +2341,6 @@ def add_item(self, title, subtitle='', modifier_subtitles=None, arg=None,
:type quicklookurl: ``unicode``
:returns: :class:`Item` instance
- See the :ref:`script-filter-results` section of the documentation
- for a detailed description of what the various parameters do and how
- they interact with one another.
-
See :ref:`icons` for a list of the supported system icons.
.. note::
@@ -2390,8 +2409,7 @@ def last_version_run(self):
self._last_version_run = version
- self.logger.debug('Last run version : {0}'.format(
- self._last_version_run))
+ self.logger.debug('last run version: %s', self._last_version_run)
return self._last_version_run
@@ -2420,7 +2438,7 @@ def set_last_version(self, version=None):
self.settings['__workflow_last_version'] = str(version)
- self.logger.debug('Set last run version : {0}'.format(version))
+ self.logger.debug('set last run version: %s', version)
return True
@@ -2430,7 +2448,7 @@ def update_available(self):
.. versionadded:: 1.9
- See :ref:`manual-updates` in the :ref:`user-manual` for detailed
+ See :ref:`guide-updates` in the :ref:`user-manual` for detailed
information on how to enable your workflow to update itself.
:returns: ``True`` if an update is available, else ``False``
@@ -2441,7 +2459,7 @@ def update_available(self):
update_data = Workflow().cached_data('__workflow_update_status',
max_age=0)
- self.logger.debug('update_data : {0}'.format(update_data))
+ self.logger.debug('update_data: %r', update_data)
if not update_data or not update_data.get('available'):
return False
@@ -2472,7 +2490,7 @@ def check_update(self, force=False):
The update script will be run in the background, so it won't
interfere in the execution of your workflow.
- See :ref:`manual-updates` in the :ref:`user-manual` for detailed
+ See :ref:`guide-updates` in the :ref:`user-manual` for detailed
information on how to enable your workflow to update itself.
:param force: Force update check
@@ -2518,7 +2536,7 @@ def start_update(self):
.. versionadded:: 1.9
- See :ref:`manual-updates` in the :ref:`user-manual` for detailed
+ See :ref:`guide-updates` in the :ref:`user-manual` for detailed
information on how to enable your workflow to update itself.
:returns: ``True`` if an update is available and will be
@@ -2861,7 +2879,7 @@ def decode(self, text, encoding=None, normalization=None):
standard for Python and will work well with data from the web (via
:mod:`~workflow.web` or :mod:`json`).
- OS X, on the other hand, uses "NFD" normalisation (nearly), so data
+ macOS, on the other hand, uses "NFD" normalisation (nearly), so data
coming from the system (e.g. via :mod:`subprocess` or
:func:`os.listdir`/:mod:`os.path`) may not match. You should either
normalise this data, too, or change the default normalisation used by
diff --git a/src/workflow/workflow3.py b/src/workflow/workflow3.py
index beee0ef..cfd580f 100644
--- a/src/workflow/workflow3.py
+++ b/src/workflow/workflow3.py
@@ -7,21 +7,20 @@
# Created on 2016-06-25
#
-"""
-:class:`Workflow3` supports Alfred 3's new features.
-
-It is an Alfred 3-only version of :class:`~workflow.workflow.Workflow`.
+"""An Alfred 3-only version of :class:`~workflow.Workflow`.
-It supports setting :ref:`workflow-variables` and
+:class:`~workflow.Workflow3` supports Alfred 3's new features, such as
+setting :ref:`workflow-variables` and
:class:`the more advanced modifiers ` supported by Alfred 3.
In order for the feedback mechanism to work correctly, it's important
to create :class:`Item3` and :class:`Modifier` objects via the
:meth:`Workflow3.add_item()` and :meth:`Item3.add_modifier()` methods
respectively. If you instantiate :class:`Item3` or :class:`Modifier`
-objects directly, the current :class:`~workflow.workflow3.Workflow3`
-object won't be aware of them, and they won't be sent to Alfred when
-you call :meth:`~workflow.workflow3.Workflow3.send_feedback()`.
+objects directly, the current :class:`Workflow3` object won't be aware
+of them, and they won't be sent to Alfred when you call
+:meth:`Workflow3.send_feedback()`.
+
"""
from __future__ import print_function, unicode_literals, absolute_import
@@ -41,12 +40,20 @@ class Variables(dict):
This class allows you to set workflow variables from
Run Script actions.
- It is a subclass of `dict`.
+ It is a subclass of :class:`dict`.
>>> v = Variables(username='deanishe', password='hunter2')
>>> v.arg = u'output value'
>>> print(v)
+ See :ref:`variables-run-script` in the User Guide for more
+ information.
+
+ Args:
+ arg (unicode, optional): Main output/``{query}``.
+ **variables: Workflow variables to set.
+
+
Attributes:
arg (unicode): Output value (``{query}``).
config (dict): Configuration for downstream workflow element.
@@ -54,13 +61,7 @@ class Variables(dict):
"""
def __init__(self, arg=None, **variables):
- """Create a new `Variables` object.
-
- Args:
- arg (unicode, optional): Main output/``{query}``.
- **variables: Workflow variables to set.
-
- """
+ """Create a new `Variables` object."""
self.arg = arg
self.config = {}
super(Variables, self).__init__(**variables)
@@ -88,6 +89,7 @@ def __unicode__(self):
Returns:
unicode: ``alfredworkflow`` JSON object
+
"""
if not self and not self.config:
if self.arg:
@@ -102,45 +104,76 @@ def __str__(self):
Returns:
str: UTF-8 encoded ``alfredworkflow`` JSON object
+
"""
return unicode(self).encode('utf-8')
class Modifier(object):
- """Modify ``Item3`` values for when specified modifier keys are pressed.
-
- Valid modifiers (i.e. values for ``key``) are:
-
- * cmd
- * alt
- * shift
- * ctrl
- * fn
+ """Modify :class:`Item3` arg/icon/variables when modifier key is pressed.
+
+ Don't use this class directly (as it won't be associated with any
+ :class:`Item3`), but rather use :meth:`Item3.add_modifier()`
+ to add modifiers to results.
+
+ >>> it = wf.add_item('Title', 'Subtitle', valid=True)
+ >>> it.setvar('name', 'default')
+ >>> m = it.add_modifier('cmd')
+ >>> m.setvar('name', 'alternate')
+
+ See :ref:`workflow-variables` in the User Guide for more information
+ and :ref:`example usage `.
+
+ Args:
+ key (unicode): Modifier key, e.g. ``"cmd"``, ``"alt"`` etc.
+ subtitle (unicode, optional): Override default subtitle.
+ arg (unicode, optional): Argument to pass for this modifier.
+ valid (bool, optional): Override item's validity.
+ icon (unicode, optional): Filepath/UTI of icon to use
+ icontype (unicode, optional): Type of icon. See
+ :meth:`Workflow.add_item() `
+ for valid values.
Attributes:
arg (unicode): Arg to pass to following action.
+ config (dict): Configuration for a downstream element, such as
+ a File Filter.
+ icon (unicode): Filepath/UTI of icon.
+ icontype (unicode): Type of icon. See
+ :meth:`Workflow.add_item() `
+ for valid values.
key (unicode): Modifier key (see above).
subtitle (unicode): Override item subtitle.
valid (bool): Override item validity.
variables (dict): Workflow variables set by this modifier.
+
"""
- def __init__(self, key, subtitle=None, arg=None, valid=None):
+ def __init__(self, key, subtitle=None, arg=None, valid=None, icon=None,
+ icontype=None):
"""Create a new :class:`Modifier`.
- You probably don't want to use this class directly, but rather
- use :meth:`Item3.add_modifier()` to add modifiers to results.
+ Don't use this class directly (as it won't be associated with any
+ :class:`Item3`), but rather use :meth:`Item3.add_modifier()`
+ to add modifiers to results.
Args:
key (unicode): Modifier key, e.g. ``"cmd"``, ``"alt"`` etc.
subtitle (unicode, optional): Override default subtitle.
arg (unicode, optional): Argument to pass for this modifier.
valid (bool, optional): Override item's validity.
+ icon (unicode, optional): Filepath/UTI of icon to use
+ icontype (unicode, optional): Type of icon. See
+ :meth:`Workflow.add_item() `
+ for valid values.
+
"""
self.key = key
self.subtitle = subtitle
self.arg = arg
self.valid = valid
+ self.icon = icon
+ self.icontype = icontype
self.config = {}
self.variables = {}
@@ -151,6 +184,7 @@ def setvar(self, name, value):
Args:
name (unicode): Name of variable.
value (unicode): Value of variable.
+
"""
self.variables[name] = value
@@ -163,6 +197,7 @@ def getvar(self, name, default=None):
Returns:
unicode or ``default``: Value of variable if set or ``default``.
+
"""
return self.variables.get(name, default)
@@ -172,6 +207,7 @@ def obj(self):
Returns:
dict: Modifier for serializing to JSON.
+
"""
o = {}
@@ -184,39 +220,57 @@ def obj(self):
if self.valid is not None:
o['valid'] = self.valid
- # Variables and config
- if self.variables or self.config:
- d = {}
- if self.variables:
- d['variables'] = self.variables
-
- if self.config:
- d['config'] = self.config
+ if self.variables:
+ o['variables'] = self.variables
- if self.arg is not None:
- d['arg'] = self.arg
+ if self.config:
+ o['config'] = self.config
- o['arg'] = json.dumps({'alfredworkflow': d})
+ icon = self._icon()
+ if icon:
+ o['icon'] = icon
return o
+ def _icon(self):
+ """Return `icon` object for item.
+
+ Returns:
+ dict: Mapping for item `icon` (may be empty).
+
+ """
+ icon = {}
+ if self.icon is not None:
+ icon['path'] = self.icon
+
+ if self.icontype is not None:
+ icon['type'] = self.icontype
+
+ return icon
+
class Item3(object):
"""Represents a feedback item for Alfred 3.
Generates Alfred-compliant JSON for a single item.
- You probably shouldn't use this class directly, but via
- :meth:`Workflow3.add_item`. See :meth:`~Workflow3.add_item`
- for details of arguments.
+ Don't use this class directly (as it then won't be associated with
+ any :class:`Workflow3` object), but rather use
+ :meth:`Workflow3.add_item() `.
+ See :meth:`~workflow.Workflow3.add_item` for details of arguments.
+
"""
def __init__(self, title, subtitle='', arg=None, autocomplete=None,
valid=False, uid=None, icon=None, icontype=None,
type=None, largetext=None, copytext=None, quicklookurl=None):
- """Use same arguments as for :meth:`Workflow.add_item`.
+ """Create a new :class:`Item3` object.
+
+ Use same arguments as for
+ :class:`Workflow.Item `.
Argument ``subtitle_modifiers`` is not supported.
+
"""
self.title = title
self.subtitle = subtitle
@@ -255,10 +309,12 @@ def getvar(self, name, default=None):
Returns:
unicode or ``default``: Value of variable if set or ``default``.
+
"""
return self.variables.get(name, default)
- def add_modifier(self, key, subtitle=None, arg=None, valid=None):
+ def add_modifier(self, key, subtitle=None, arg=None, valid=None, icon=None,
+ icontype=None):
"""Add alternative values for a modifier key.
Args:
@@ -266,11 +322,16 @@ def add_modifier(self, key, subtitle=None, arg=None, valid=None):
subtitle (unicode, optional): Override item subtitle.
arg (unicode, optional): Input for following action.
valid (bool, optional): Override item validity.
+ icon (unicode, optional): Filepath/UTI of icon.
+ icontype (unicode, optional): Type of icon. See
+ :meth:`Workflow.add_item() `
+ for valid values.
Returns:
Modifier: Configured :class:`Modifier`.
+
"""
- mod = Modifier(key, subtitle, arg, valid)
+ mod = Modifier(key, subtitle, arg, valid, icon, icontype)
for k in self.variables:
mod.setvar(k, self.variables[k])
@@ -285,22 +346,18 @@ def obj(self):
Returns:
dict: Data suitable for Alfred 3 feedback.
+
"""
# Required values
- o = {'title': self.title,
- 'subtitle': self.subtitle,
- 'valid': self.valid}
-
- icon = {}
+ o = {
+ 'title': self.title,
+ 'subtitle': self.subtitle,
+ 'valid': self.valid,
+ }
# Optional values
-
- # arg & variables
- v = Variables(self.arg, **self.variables)
- v.config = self.config
- arg = unicode(v)
- if arg:
- o['arg'] = arg
+ if self.arg is not None:
+ o['arg'] = self.arg
if self.autocomplete is not None:
o['autocomplete'] = self.autocomplete
@@ -314,6 +371,12 @@ def obj(self):
if self.quicklookurl is not None:
o['quicklookurl'] = self.quicklookurl
+ if self.variables:
+ o['variables'] = self.variables
+
+ if self.config:
+ o['config'] = self.config
+
# Largetype and copytext
text = self._text()
if text:
@@ -335,6 +398,7 @@ def _icon(self):
Returns:
dict: Mapping for item `icon` (may be empty).
+
"""
icon = {}
if self.icon is not None:
@@ -350,6 +414,7 @@ def _text(self):
Returns:
dict: `text` mapping (may be empty)
+
"""
text = {}
if self.largetext is not None:
@@ -365,6 +430,7 @@ def _modifiers(self):
Returns:
dict: Modifier mapping or `None`.
+
"""
if self.modifiers:
mods = {}
@@ -379,9 +445,13 @@ def _modifiers(self):
class Workflow3(Workflow):
"""Workflow class that generates Alfred 3 feedback.
+ ``Workflow3`` is a subclass of :class:`~workflow.Workflow` and
+ most of its methods are documented there.
+
Attributes:
item_class (class): Class used to generate feedback items.
variables (dict): Top level workflow variables.
+
"""
item_class = Item3
@@ -389,7 +459,8 @@ class Workflow3(Workflow):
def __init__(self, **kwargs):
"""Create a new :class:`Workflow3` object.
- See :class:`~workflow.workflow.Workflow` for documentation.
+ See :class:`~workflow.Workflow` for documentation.
+
"""
Workflow.__init__(self, **kwargs)
self.variables = {}
@@ -459,6 +530,7 @@ def setvar(self, name, value):
Args:
name (unicode): Name of variable.
value (unicode): Value of variable.
+
"""
self.variables[name] = value
@@ -471,6 +543,7 @@ def getvar(self, name, default=None):
Returns:
unicode or ``default``: Value of variable if set or ``default``.
+
"""
return self.variables.get(name, default)
@@ -479,8 +552,8 @@ def add_item(self, title, subtitle='', arg=None, autocomplete=None,
type=None, largetext=None, copytext=None, quicklookurl=None):
"""Add an item to be output to Alfred.
- See :meth:`~workflow.workflow.Workflow.add_item` for the main
- documentation.
+ See :meth:`Workflow.add_item() ` for the
+ main documentation.
The key difference is that this method does not support the
``modifier_subtitles`` argument. Use the :meth:`~Item3.add_modifier()`
@@ -488,6 +561,7 @@ def add_item(self, title, subtitle='', arg=None, autocomplete=None,
Returns:
Item3: Alfred feedback item.
+
"""
item = self.item_class(title, subtitle, arg,
autocomplete, valid, uid, icon, icontype, type,
@@ -496,9 +570,14 @@ def add_item(self, title, subtitle='', arg=None, autocomplete=None,
self._items.append(item)
return item
+ @property
+ def _session_prefix(self):
+ """Filename prefix for current session."""
+ return '_wfsess-{0}-'.format(self.session_id)
+
def _mk_session_name(self, name):
"""New cache name/key based on session ID."""
- return '_wfsess-{0}-{1}'.format(self.session_id, name)
+ return '{0}{1}'.format(self._session_prefix, name)
def cache_data(self, name, data, session=False):
"""Cache API with session-scoped expiry.
@@ -511,11 +590,11 @@ def cache_data(self, name, data, session=False):
session (bool, optional): Whether to scope the cache
to the current session.
- ``name`` and ``data`` are as for the
- :meth:`~workflow.workflow.Workflow.cache_data` on
- :class:`~workflow.workflow.Workflow`.
+ ``name`` and ``data`` are the same as for the
+ :meth:`~workflow.Workflow.cache_data` method on
+ :class:`~workflow.Workflow`.
- If ``session`` is ``True``, the ``name`` variable is prefixed
+ If ``session`` is ``True``, then ``name`` is prefixed
with :attr:`session_id`.
"""
@@ -537,11 +616,11 @@ def cached_data(self, name, data_func=None, max_age=60, session=False):
session (bool, optional): Whether to scope the cache
to the current session.
- ``name``, ``data_func`` and ``max_age`` are as for the
- :meth:`~workflow.workflow.Workflow.cached_data` on
- :class:`~workflow.workflow.Workflow`.
+ ``name``, ``data_func`` and ``max_age`` are the same as for the
+ :meth:`~workflow.Workflow.cached_data` method on
+ :class:`~workflow.Workflow`.
- If ``session`` is ``True``, the ``name`` variable is prefixed
+ If ``session`` is ``True``, then ``name`` is prefixed
with :attr:`session_id`.
"""
@@ -550,13 +629,25 @@ def cached_data(self, name, data_func=None, max_age=60, session=False):
return super(Workflow3, self).cached_data(name, data_func, max_age)
- def clear_session_cache(self):
- """Remove *all* session data from the cache.
+ def clear_session_cache(self, current=False):
+ """Remove session data from the cache.
.. versionadded:: 1.25
+ .. versionchanged:: 1.27
+
+ By default, data belonging to the current session won't be
+ deleted. Set ``current=True`` to also clear current session.
+
+ Args:
+ current (bool, optional): If ``True``, also remove data for
+ current session.
+
"""
def _is_session_file(filename):
- return filename.startswith('_wfsess-')
+ if current:
+ return filename.startswith('_wfsess-')
+ return filename.startswith('_wfsess-') \
+ and not filename.startswith(self._session_prefix)
self.clear_cache(_is_session_file)
@@ -566,6 +657,7 @@ def obj(self):
Returns:
dict: Data suitable for Alfred 3 feedback.
+
"""
items = []
for item in self._items: