diff --git a/bin/hooks/pre-commit b/.githooks/pre-commit similarity index 100% rename from bin/hooks/pre-commit rename to .githooks/pre-commit diff --git a/Convert-3.7.0.alfredworkflow b/Convert-3.7.1.alfredworkflow similarity index 88% rename from Convert-3.7.0.alfredworkflow rename to Convert-3.7.1.alfredworkflow index b21b1ef..a3497e9 100644 Binary files a/Convert-3.7.0.alfredworkflow and b/Convert-3.7.1.alfredworkflow differ diff --git a/src/info.plist b/src/info.plist index c88e6c0..40e45ce 100644 --- a/src/info.plist +++ b/src/info.plist @@ -231,7 +231,11 @@ runningsubtext Convertifying… script - /usr/bin/python convert.py "$1" + echo . >&2 +echo "http_proxy=$http_proxy">&2 +echo "https_proxy=$https_proxy">&2 + +/usr/bin/python convert.py "$1" scriptargtype 1 scriptfile @@ -476,6 +480,8 @@ variables={allvars} argument {var:query} + passthroughargument + variables @@ -653,7 +659,7 @@ UPDATE_INTERVAL is the number of minutes between exchange rate updates. APP_KEY version - 3.7.0 + 3.7.1 webaddress diff --git a/src/workflow/background.py b/src/workflow/background.py index ba5c52a..c2bd735 100644 --- a/src/workflow/background.py +++ b/src/workflow/background.py @@ -102,10 +102,7 @@ def _job_pid(name): if _process_exists(pid): return pid - try: - os.unlink(pidfile) - except Exception: # pragma: no cover - pass + os.unlink(pidfile) def is_running(name): diff --git a/src/workflow/notify.py b/src/workflow/notify.py index a4b7f40..28ec0b9 100644 --- a/src/workflow/notify.py +++ b/src/workflow/notify.py @@ -117,8 +117,8 @@ def install_notifier(): # z.extractall(destdir) tgz = tarfile.open(archive, 'r:gz') tgz.extractall(destdir) - assert os.path.exists(n), \ - 'Notify.app could not be installed in %s' % destdir + if not os.path.exists(n): # pragma: nocover + raise RuntimeError('Notify.app could not be installed in ' + destdir) # Replace applet icon icon = notifier_icon_path() @@ -253,8 +253,9 @@ def png_to_icns(png_path, icns_path): try: iconset = os.path.join(tempdir, 'Icon.iconset') - assert not os.path.exists(iconset), \ - 'iconset already exists: ' + iconset + if os.path.exists(iconset): # pragma: nocover + raise RuntimeError('iconset already exists: ' + iconset) + os.makedirs(iconset) # Copy source icon to icon set and generate all the other @@ -283,8 +284,9 @@ def png_to_icns(png_path, icns_path): if retcode != 0: raise RuntimeError('iconset exited with %d' % retcode) - assert os.path.exists(icns_path), \ - 'generated ICNS file not found: ' + repr(icns_path) + if not os.path.exists(icns_path): # pragma: nocover + raise ValueError( + 'generated ICNS file not found: ' + repr(icns_path)) finally: try: shutil.rmtree(tempdir) @@ -332,8 +334,8 @@ def ustr(s): print('converting {0!r} to {1!r} ...'.format(o.png, icns), file=sys.stderr) - assert not os.path.exists(icns), \ - 'destination file already exists: ' + icns + if os.path.exists(icns): + raise ValueError('destination file already exists: ' + icns) png_to_icns(o.png, icns) sys.exit(0) diff --git a/src/workflow/update.py b/src/workflow/update.py index 6affc94..c039f7a 100644 --- a/src/workflow/update.py +++ b/src/workflow/update.py @@ -519,7 +519,7 @@ def install_update(): path = retrieve_download(Download.from_dict(dl)) wf().logger.info('installing updated workflow ...') - subprocess.call(['open', path]) + subprocess.call(['open', path]) # nosec wf().cache_data(key, no_update) return True diff --git a/src/workflow/util.py b/src/workflow/util.py index 27209d8..ab5e954 100644 --- a/src/workflow/util.py +++ b/src/workflow/util.py @@ -31,19 +31,21 @@ # "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});' class AcquisitionError(Exception): @@ -148,17 +150,16 @@ def applescriptify(s): .. versionadded:: 1.31 Replaces ``"`` with `"& quote &"`. Use this function if you want - to insert a string into an AppleScript script: - >>> query = 'g "python" test' - >>> applescriptify(query) + + >>> applescriptify('g "python" test') 'g " & quote & "python" & quote & "test' Args: s (unicode): Unicode string to escape. Returns: - unicode: Escaped string + unicode: Escaped string. """ return s.replace(u'"', u'" & quote & "') @@ -173,11 +174,11 @@ def run_command(cmd, **kwargs): all arguments are encoded to UTF-8 first. Args: - cmd (list): Command arguments to pass to ``check_output``. - **kwargs: Keyword arguments to pass to ``check_output``. + cmd (list): Command arguments to pass to :func:`~subprocess.check_output`. + **kwargs: Keyword arguments to pass to :func:`~subprocess.check_output`. Returns: - str: Output returned by ``check_output``. + str: Output returned by :func:`~subprocess.check_output`. """ cmd = [utf8ify(s) for s in cmd] @@ -197,6 +198,7 @@ def run_applescript(script, *args, **kwargs): script (str, optional): Filepath of script or code to run. *args: Optional command-line arguments to pass to the script. **kwargs: Pass ``lang`` to run a language other than AppleScript. + Any other keyword arguments are passed to :func:`run_command`. Returns: str: Output of run command. @@ -242,8 +244,8 @@ def run_trigger(name, bundleid=None, arg=None): .. versionadded:: 1.31 - If ``bundleid`` is not specified, reads the bundle ID of the current - workflow from Alfred's environment variables. + If ``bundleid`` is not specified, the bundle ID of the calling + workflow is used. Args: name (str): Name of External Trigger to call. @@ -264,11 +266,29 @@ def run_trigger(name, bundleid=None, arg=None): run_applescript(script, lang='JavaScript') +def set_theme(theme_name): + """Change Alfred's theme. + + .. versionadded:: 1.39.0 + + Args: + theme_name (unicode): Name of theme Alfred should use. + + """ + appname = jxa_app_name() + 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): """Set a workflow variable in ``info.plist``. .. versionadded:: 1.33 + If ``bundleid`` is not specified, the bundle ID of the calling + workflow is used. + Args: name (str): Name of variable to set. value (str): Value to set variable to. @@ -297,6 +317,9 @@ def unset_config(name, bundleid=None): .. versionadded:: 1.33 + If ``bundleid`` is not specified, the bundle ID of the calling + workflow is used. + Args: name (str): Name of variable to delete. bundleid (str, optional): Bundle ID of workflow variable belongs to. @@ -313,6 +336,71 @@ def unset_config(name, bundleid=None): run_applescript(script, lang='JavaScript') +def search_in_alfred(query=None): + """Open Alfred with given search query. + + .. versionadded:: 1.39.0 + + Omit ``query`` to simply open Alfred's main window. + + Args: + query (unicode, optional): Search query. + + """ + 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') + + +def browse_in_alfred(path): + """Open Alfred's filesystem navigation mode at ``path``. + + .. versionadded:: 1.39.0 + + Args: + path (unicode): File or directory path. + + """ + appname = jxa_app_name() + script = JXA_BROWSE.format(app=json.dumps(appname), arg=json.dumps(path)) + run_applescript(script, lang='JavaScript') + + +def action_in_alfred(paths): + """Action the give filepaths in Alfred. + + .. versionadded:: 1.39.0 + + Args: + paths (list): Unicode paths to files/directories to action. + + """ + appname = jxa_app_name() + script = JXA_ACTION.format(app=json.dumps(appname), arg=json.dumps(paths)) + run_applescript(script, lang='JavaScript') + + +def reload_workflow(bundleid=None): + """Tell Alfred to reload a workflow from disk. + + .. versionadded:: 1.39.0 + + If ``bundleid`` is not specified, the bundle ID of the calling + workflow is used. + + Args: + bundleid (unicode, optional): Bundle ID of workflow to reload. + + """ + 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)) + + run_applescript(script, lang='JavaScript') + + def appinfo(name): """Get information about an installed application. @@ -325,11 +413,15 @@ def appinfo(name): AppInfo: :class:`AppInfo` tuple or ``None`` if app isn't found. """ - cmd = ['mdfind', '-onlyin', '/Applications', - '-onlyin', os.path.expanduser('~/Applications'), - '(kMDItemContentTypeTree == com.apple.application &&' - '(kMDItemDisplayName == "{0}" || kMDItemFSName == "{0}.app"))' - .format(name)] + cmd = [ + '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: diff --git a/src/workflow/version b/src/workflow/version index 673b6a6..ebc91b4 100644 --- a/src/workflow/version +++ b/src/workflow/version @@ -1 +1 @@ -1.37.2 \ No newline at end of file +1.40.0 \ No newline at end of file diff --git a/src/workflow/web.py b/src/workflow/web.py index 0781911..83212a8 100644 --- a/src/workflow/web.py +++ b/src/workflow/web.py @@ -9,6 +9,8 @@ """Lightweight HTTP library with a requests-like interface.""" +from __future__ import absolute_import, print_function + import codecs import json import mimetypes @@ -23,8 +25,10 @@ import urlparse import zlib +__version__ = open(os.path.join(os.path.dirname(__file__), 'version')).read() -USER_AGENT = u'Alfred-Workflow/1.36 (+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 @@ -178,6 +182,18 @@ def itervalues(self): yield v['val'] +class Request(urllib2.Request): + """Subclass of :class:`urllib2.Request` that supports custom methods.""" + + def __init__(self, *args, **kwargs): + """Create a new :class:`Request`.""" + self._method = kwargs.pop('method', None) + urllib2.Request.__init__(self, *args, **kwargs) + + def get_method(self): + return self._method.upper() + + class Response(object): """ Returned by :func:`request` / :func:`get` / :func:`post` functions. @@ -200,7 +216,7 @@ class Response(object): def __init__(self, request, stream=False): """Call `request` with :mod:`urllib2` and process results. - :param request: :class:`urllib2.Request` instance + :param request: :class:`Request` instance :param stream: Whether to stream response or retrieve it all at once :type stream: bool @@ -512,7 +528,7 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, socket.setdefaulttimeout(timeout) # Default handlers - openers = [] + openers = [urllib2.ProxyHandler(urllib2.getproxies())] if not allow_redirects: openers.append(NoRedirectHandler()) @@ -544,10 +560,6 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, headers['accept-encoding'] = ', '.join(encodings) - # Force POST by providing an empty data string - if method == 'POST' and not data: - data = '' - if files: if not data: data = {} @@ -575,7 +587,7 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, query = urllib.urlencode(str_dict(params), doseq=True) url = urlparse.urlunsplit((scheme, netloc, path, query, fragment)) - req = urllib2.Request(url, data, headers) + req = Request(url, data, headers, method=method) return Response(req, stream) @@ -591,6 +603,18 @@ def get(url, params=None, headers=None, cookies=None, auth=None, 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): """Initiate a POST request. Arguments as for :func:`request`. @@ -602,6 +626,17 @@ def post(url, params=None, data=None, headers=None, cookies=None, files=None, 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) + + def encode_multipart_formdata(fields, files): """Encode form data (``fields``) and ``files`` for POST request. diff --git a/src/workflow/workflow.py b/src/workflow/workflow.py index 2a057b0..3935227 100644 --- a/src/workflow/workflow.py +++ b/src/workflow/workflow.py @@ -2639,28 +2639,27 @@ def reset(self): def open_log(self): """Open :attr:`logfile` in default app (usually Console.app).""" - subprocess.call(['open', self.logfile]) + subprocess.call(['open', self.logfile]) # nosec def open_cachedir(self): """Open the workflow's :attr:`cachedir` in Finder.""" - subprocess.call(['open', self.cachedir]) + subprocess.call(['open', self.cachedir]) # nosec def open_datadir(self): """Open the workflow's :attr:`datadir` in Finder.""" - subprocess.call(['open', self.datadir]) + subprocess.call(['open', self.datadir]) # nosec def open_workflowdir(self): """Open the workflow's :attr:`workflowdir` in Finder.""" - subprocess.call(['open', self.workflowdir]) + 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]) + 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]) + subprocess.call(['open', self.help_url]) # nosec return 'Opening workflow help URL in browser' diff --git a/src/workflow/workflow3.py b/src/workflow/workflow3.py index b92c4be..23a7aae 100644 --- a/src/workflow/workflow3.py +++ b/src/workflow/workflow3.py @@ -50,12 +50,16 @@ class Variables(dict): information. Args: - arg (unicode, optional): Main output/``{query}``. + arg (unicode or list, optional): Main output/``{query}``. **variables: Workflow variables to set. + In Alfred 4.1+ and Alfred-Workflow 1.40+, ``arg`` may also be a + :class:`list` or :class:`tuple`. Attributes: - arg (unicode): Output value (``{query}``). + arg (unicode or list): Output value (``{query}``). + In Alfred 4.1+ and Alfred-Workflow 1.40+, ``arg`` may also be a + :class:`list` or :class:`tuple`. config (dict): Configuration for downstream workflow element. """ @@ -68,7 +72,7 @@ def __init__(self, arg=None, **variables): @property def obj(self): - """Return ``alfredworkflow`` `dict`.""" + """``alfredworkflow`` :class:`dict`.""" o = {} if self: d2 = {} @@ -92,10 +96,10 @@ def __unicode__(self): """ if not self and not self.config: - if self.arg: - return self.arg - else: + if not self.arg: return u'' + if isinstance(self.arg, unicode): + return self.arg return json.dumps(self.obj) @@ -328,6 +332,9 @@ def add_modifier(self, key, subtitle=None, arg=None, valid=None, icon=None, :meth:`Workflow.add_item() ` for valid values. + In Alfred 4.1+ and Alfred-Workflow 1.40+, ``arg`` may also be a + :class:`list` or :class:`tuple`. + Returns: Modifier: Configured :class:`Modifier`. @@ -568,6 +575,9 @@ def add_item(self, title, subtitle='', arg=None, autocomplete=None, turned on for your Script Filter, Alfred (version 3.5 and above) will filter against this field, not ``title``. + In Alfred 4.1+ and Alfred-Workflow 1.40+, ``arg`` may also be a + :class:`list` or :class:`tuple`. + See :meth:`Workflow.add_item() ` for the main documentation and other parameters. @@ -717,5 +727,8 @@ def warn_empty(self, title, subtitle=u'', icon=None): def send_feedback(self): """Print stored items to console/Alfred as JSON.""" - json.dump(self.obj, sys.stdout) + if self.debugging: + json.dump(self.obj, sys.stdout, indent=2, separators=(',', ': ')) + else: + json.dump(self.obj, sys.stdout) sys.stdout.flush()