diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 52f7e7e8..5763065a 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -10,16 +10,21 @@ jobs: run: name: "tests & coverage" runs-on: macos-latest + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + env: COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: - python-version: 2.7 + python-version: ${{ matrix.python-version }} - name: Install test dependencies run: | diff --git a/.travis.yml b/.travis.yml index bdee27eb..ebadf2e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,9 +4,7 @@ osx_image: xcode8.3 env: matrix: - - VERSION=2.7.10 SOURCE=macpython - # Turn off 2.6 as it's no longer supported - # - VERSION=2.6.9 SOURCE=macports + - VERSION=3.8.9 SOURCE=macpython before_install: # - brew update diff --git a/README.md b/README.md index edca16ce..40d7957f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ A helper library in Python for authors of workflows for [Alfred 3 and 4][alfred] -Supports Alfred 3 and Alfred 4 on macOS 10.7+ (Python 2.7). +Supports Alfred 3 and Alfred 4 on macOS Catalina or later (with Python 3). Alfred-Workflow takes the grunt work out of writing a workflow by giving you the tools to create a fast and featureful Alfred workflow from an API, application or library in minutes. diff --git a/README_PYPI.rst b/README_PYPI.rst index 4d1ef993..c0554312 100644 --- a/README_PYPI.rst +++ b/README_PYPI.rst @@ -1,7 +1,7 @@ A helper library for writing `Alfred 2, 3 and 4`_ workflows. -Supports macOS 10.7+ and Python 2.7 (Alfred 3 is 10.9+/2.7 only). +Supports macOS Catalina and Python 3.7+. Alfred-Workflow is designed to take the grunt work out of writing a workflow. @@ -46,7 +46,7 @@ Here's how to show recent `Pinboard.in `_ posts in Alfred. Create a new workflow in Alfred's preferences. Add a **Script Filter** with -Language ``/usr/bin/python`` and paste the following into the **Script** +Language ``/usr/bin/python3`` and paste the following into the **Script** field (changing ``API_KEY``): diff --git a/bin/publish-cheeseshop.sh b/bin/publish-cheeseshop.sh index f1dea3a2..54b5fd4f 100755 --- a/bin/publish-cheeseshop.sh +++ b/bin/publish-cheeseshop.sh @@ -7,7 +7,7 @@ rootdir=$(cd $(dirname $0)/../; pwd) cd "${rootdir}" version=$( cat workflow/version ) -/usr/bin/python setup.py sdist +/usr/bin/python3 setup.py sdist twine upload dist/Alfred-Workflow-$version.tar.gz cd - diff --git a/docs/api/util.rst.inc b/docs/api/util.rst.inc index f4c33998..b2cbf40e 100644 --- a/docs/api/util.rst.inc +++ b/docs/api/util.rst.inc @@ -36,8 +36,6 @@ Text encoding and formatting. .. autofunction:: unicodify -.. autofunction:: utf8ify - .. autofunction:: applescriptify diff --git a/docs/api/web.rst.inc b/docs/api/web.rst.inc index 4b09c6e2..513b3331 100644 --- a/docs/api/web.rst.inc +++ b/docs/api/web.rst.inc @@ -12,27 +12,6 @@ modelled on the excellent `requests`_ library. The purpose of :mod:`workflow.web` is to cover trivial cases at just 0.5% of the size of `requests`_. -.. danger:: - - As :mod:`workflow.web` is based on Python 2's standard HTTP libraries, - there are two important problems with SSL connections. - - Python versions older than 2.7.9 (i.e. pre-Yosemite) **do not** - verify SSL certificates when establishing HTTPS connections. - - As a result, you **must not** use this module for sensitive - connections unless you're certain it will only run on 2.7.9/Yosemite - and later. If your workflow is Alfred 3-only, this requirement is met. - - Secondly, versions of macOS older than High Sierra (10.13) have an - extremely outdated version of OpenSSL, which is incompatible with - many servers' SSL configuration. - - Consequently, :mod:`workflow.web` cannot connect to such servers. - As this includes GitHub's SSL configuration, the - :ref:`update mechanism ` only works on High Sierra - and later. - .. autofunction:: get .. autofunction:: post diff --git a/docs/api/workflow.rst.inc b/docs/api/workflow.rst.inc index 2e12c02b..db5da04a 100644 --- a/docs/api/workflow.rst.inc +++ b/docs/api/workflow.rst.inc @@ -58,9 +58,6 @@ The default manager (which supports JSON, pickle and cPickle) is at .. autoclass:: JSONSerializer :members: -.. autoclass:: CPickleSerializer - :members: - .. autoclass:: PickleSerializer :members: diff --git a/docs/conf.py b/docs/conf.py index 43b97a36..fc2e63d9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -44,7 +44,7 @@ 'sphinxcontrib.napoleon', ] -intersphinx_mapping = {'python': ('https://docs.python.org/2.7', None)} +intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -59,8 +59,8 @@ master_doc = 'index' # General information about the project. -project = u'Alfred-Workflow' -copyright = u' 2013–{} Dean Jackson'.format(date.today().year) +project = 'Alfred-Workflow' +copyright = ' 2013–{} Dean Jackson'.format(date.today().year) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the @@ -246,8 +246,8 @@ # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ - ('index', 'Alfred-Workflow.tex', u'Alfred-Workflow Documentation', - u'Dean Jackson ', 'manual'), + ('index', 'Alfred-Workflow.tex', 'Alfred-Workflow Documentation', + 'Dean Jackson ', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of @@ -276,8 +276,8 @@ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'alfred-workflow', u'Alfred-Workflow Documentation', - [u'Dean Jackson '], 1) + ('index', 'alfred-workflow', 'Alfred-Workflow Documentation', + ['Dean Jackson '], 1) ] # If true, show URL addresses after external links. @@ -290,8 +290,8 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'Alfred-Workflow', u'Alfred-Workflow Documentation', - u'Dean Jackson ', + ('index', 'Alfred-Workflow', 'Alfred-Workflow Documentation', + 'Dean Jackson ', 'Alfred-Workflow', 'Python helper library for Alfred Workflows', 'Miscellaneous'), @@ -313,10 +313,10 @@ # -- Options for Epub output ---------------------------------------------- # Bibliographic Dublin Core info. -epub_title = u'Alfred-Workflow' -epub_author = u'Dean Jackson ' -epub_publisher = u'Dean Jackson ' -epub_copyright = u'2014, Dean Jackson ' +epub_title = 'Alfred-Workflow' +epub_author = 'Dean Jackson ' +epub_publisher = 'Dean Jackson ' +epub_copyright = '2014, Dean Jackson ' # The basename for the epub file. It defaults to the project name. # epub_basename = u'Alfred-Workflow' diff --git a/docs/guide/background.rst b/docs/guide/background.rst index 46441c31..77729371 100644 --- a/docs/guide/background.rst +++ b/docs/guide/background.rst @@ -39,7 +39,7 @@ background). What we're doing is: # Is cache over 1 hour old or non-existent? if not wf.cached_data_fresh('exchange-rates', 3600): run_in_background('update', - ['/usr/bin/python', + ['/usr/bin/python3', wf.workflowfile('update_exchange_rates.py')]) if is_running('update'): diff --git a/docs/guide/magic-arguments.rst b/docs/guide/magic-arguments.rst index 0ae855bc..c188ba65 100644 --- a/docs/guide/magic-arguments.rst +++ b/docs/guide/magic-arguments.rst @@ -95,7 +95,7 @@ The magic arguments are defined in the :attr:`Workflow.magic_arguments ` — the default serializer - for both cached and stored data, with very good support for native Python - data types; -- :class:`pickle ` — a more flexible, but - much slower alternative to ``cpickle``; and +- :class:`cpickle ` — the default serializer + for both cached and stored data, with support for native Python data types; - :class:`json ` — a very common data format, but with limited support for native Python data types. diff --git a/docs/guide/setup.rst b/docs/guide/setup.rst index d46df8bc..0aa0adfb 100644 --- a/docs/guide/setup.rst +++ b/docs/guide/setup.rst @@ -20,7 +20,7 @@ following (and only the following) **Escaping** options: The **Script** field should contain the following:: - /usr/bin/python yourscript.py "{query}" + /usr/bin/python3 yourscript.py "{query}" where ``yourscript.py`` is the name of your script [#]_. @@ -31,7 +31,7 @@ to capture any errors thrown by your scripts: .. code-block:: python :linenos: - #!/usr/bin/python + #!/usr/bin/python3 # encoding: utf-8 import sys @@ -68,6 +68,6 @@ to capture any errors thrown by your scripts: sys.exit(wf.run(main)) -.. [#] It's better to specify ``/usr/bin/python`` over just ``python``. This +.. [#] It's better to specify ``/usr/bin/python3`` over just ``python``. This ensures that the script will always be run with the system default - Python regardless of what ``PATH`` might be. \ No newline at end of file + Python regardless of what ``PATH`` might be. diff --git a/docs/guide/text-encoding.rst b/docs/guide/text-encoding.rst index 57616343..15f39d98 100644 --- a/docs/guide/text-encoding.rst +++ b/docs/guide/text-encoding.rst @@ -119,7 +119,7 @@ than a concrete implementation), while encoded strings are binary data that are encoded according to some scheme that maps characters to a specific binary representation (e.g. UTF-8 or ASCII). -In Python, these have the types ``unicode`` and ``str`` respectively. +In Python, these have the types ``str`` and ``bytes`` respectively. As noted, Unicode strings only exist within a running program. Any text stored on disk, passed into or out of a program or transmitted over a network *must* @@ -351,7 +351,7 @@ an empty environment. This tells Python (and other POSIX software) by omission that encoding is ASCII. Although this won't affect Python 2's auto-promotion of encoded strings -(``str`` objects) to Unicode (it always uses ASCII), it *does* +(``bytes`` objects) to Unicode (it always uses ASCII), it *does* affect the printing of Unicode strings, so using :func:`print` may work perfectly in your shell where the environmental encoding is UTF-8 but not in Alfred 2, where encoding is ASCII by default. diff --git a/docs/guide/variables.rst b/docs/guide/variables.rst index b4378d1e..e110cdaf 100644 --- a/docs/guide/variables.rst +++ b/docs/guide/variables.rst @@ -190,7 +190,7 @@ read: .. code-block:: bash - /usr/bin/python myscript.py pages + /usr/bin/python3 myscript.py pages The other options (``--view``, ``--edit``, ``--share``) are set via the diff --git a/docs/index.rst b/docs/index.rst index f2b064d2..eeb9be46 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -18,7 +18,7 @@ elsewhere, filter them and display results to the user. Alfred-Workflow takes care of a lot of the details for you, allowing you to concentrate your efforts on your workflow's functionality. -Alfred-Workflow supports macOS 10.7+ (Python 2.7). +Alfred-Workflow supports macOS Catalina or later. Features @@ -58,7 +58,7 @@ Quick example Here's how to show recent `Pinboard.in `_ posts in Alfred. Create a new workflow in Alfred's preferences. Add a **Script Filter** with -Language ``/usr/bin/python`` and paste the following into the **Script** +Language ``/usr/bin/python3`` and paste the following into the **Script** box (changing ``API_KEY``): .. code-block:: python diff --git a/docs/supported-versions.rst b/docs/supported-versions.rst index efcced9d..0646135c 100644 --- a/docs/supported-versions.rst +++ b/docs/supported-versions.rst @@ -5,8 +5,8 @@ Supported versions ================== -Alfred-Workflow supports all versions of Alfred 2–4. It works with -Python 2.6 and 2.7, but *not yet* Python 3. +Alfred-Workflow supports all versions of Alfred 2–4. It works with Python 3.8 +and does not support Python 2 any longer. Some features are not available on older versions of macOS. @@ -51,7 +51,7 @@ Alfred-Workflow supports the same macOS versions as Alfred, namely 10.6 (Snow Le Python versions =============== -Alfred-Workflow only officially supports the system Pythons that come with macOS (i.e. ``/usr/bin/python``), which is 2.6 on 10.6/Snow Leopard and 2.7 on later versions. +Alfred-Workflow only officially supports the system Pythons that come with macOS (i.e. ``/usr/bin/python3``). .. important:: @@ -59,40 +59,7 @@ Alfred-Workflow only officially supports the system Pythons that come with macOS This is a deliberate design choice, so please do not submit feature requests for support of, or bug reports concerning issues with any non-system Pythons. - **This includes Python 3**. - Python 3 support will be added in a new major version of the library when Catalina is more popular. - - -Here is the `full list of new features in Python 2.7`_, but the most important things if you want your workflow to run on Snow Leopard/Python 2.6 are: - -- :mod:`argparse` is not available in 2.6. Use :mod:`getopt` or - `include argparse in your workflow`_. Personally, I'm a big fan of - `docopt`_ for parsing command-line arguments, but :mod:`argparse` - is better for certain use cases. -- You must specify field numbers for :meth:`str.format`, i.e. - ``'{0}.{1}'.format(first, second)`` not just - ``'{}.{}'.format(first, second)``. -- No :class:`~collections.Counter` or - :class:`~collections.OrderedDict` in :mod:`collections`. -- No dictionary views in 2.6. -- No set literals. -- No dictionary or set comprehensions. - -Python 2.6 is still included in later versions of macOS (up to and including El Capitan), so run your Python scripts with ``/usr/bin/python2.6`` in addition to ``/usr/bin/python`` (2.7) to make sure they will run on Snow Leopard. - - -Why no Python 3 support? ------------------------- - -Alfred-Workflow is targeted at the system Python on macOS. Its goal is to enable developers to build workflows that will "just work" for users on any vanilla installation of macOS since Snow Leopard. - -As such, it :ref:`strongly discourages developers ` from requiring users of their workflows to bugger about with their OS in order to get a workflow to work. This naturally includes requiring the installation of some non-default Python. - -Version 2 of Alfred-Workflow, which will be a complete rewrite, will support Python 3 and Alfred 4+ only. - - -.. _full list of new features in Python 2.7: https://docs.python.org/3/whatsnew/2.7.html .. _include argparse in your workflow: https://pypi.python.org/pypi/argparse .. _docopt: http://docopt.org/ .. _Powerpack: https://buy.alfredapp.com/ diff --git a/docs/tutorial_1.rst b/docs/tutorial_1.rst index a0643a09..d5242cca 100644 --- a/docs/tutorial_1.rst +++ b/docs/tutorial_1.rst @@ -79,7 +79,7 @@ argument. .. note:: - You *can* choose ``/usr/bin/python`` as the **Language** and paste + You *can* choose ``/usr/bin/python3`` as the **Language** and paste your Python code into the **Script** box, but this isn't the best idea. If you do this, you can't run the script from the Terminal (which can be @@ -615,4 +615,4 @@ To learn more about coding in Python, try these resources: `TextMate `_ is an excellent and free editor. `TextWrangler `_ is - another good, free editor for macOS (supports 10.6). \ No newline at end of file + another good, free editor for macOS (supports 10.6). diff --git a/docs/tutorial_2.rst b/docs/tutorial_2.rst index 0dc47521..31996abf 100644 --- a/docs/tutorial_2.rst +++ b/docs/tutorial_2.rst @@ -892,7 +892,7 @@ update itself: # Start update script if cached data are too old (or doesn't exist) if not wf.cached_data_fresh('posts', max_age=600): - cmd = ['/usr/bin/python', wf.workflowfile('update.py')] + cmd = ['/usr/bin/python3', wf.workflowfile('update.py')] run_in_background('update', cmd) # Notify the user if the cache is being updated @@ -1062,4 +1062,4 @@ enable your workflow to update itself from GitHub. .. _GitHub releases: https://help.github.com/articles/about-releases .. [#] :mod:`argparse` isn't available in Python 2.6, so this workflow won't - run on Snow Leopard (10.6). \ No newline at end of file + run on Snow Leopard (10.6). diff --git a/extras/benchmark.py b/extras/benchmark.py index 7f1c96f0..e488c76a 100755 --- a/extras/benchmark.py +++ b/extras/benchmark.py @@ -10,7 +10,7 @@ """Benchmark the loading speed of Alfred-Workflow.""" -from __future__ import print_function, unicode_literals, absolute_import + import os import subprocess @@ -101,7 +101,7 @@ def row_to_str(self, row): str_row = [is_title] for cell in data: - if isinstance(cell, unicode): + if isinstance(cell, str): cell = cell.encode('utf-8') elif isinstance(cell, str): pass diff --git a/extras/benchmarks/00-python-interpreter-only/run.sh b/extras/benchmarks/00-python-interpreter-only/run.sh index 0172a6f5..e9e6130f 100755 --- a/extras/benchmarks/00-python-interpreter-only/run.sh +++ b/extras/benchmarks/00-python-interpreter-only/run.sh @@ -1,3 +1,3 @@ #!/bin/bash -/usr/bin/python -c '' +/usr/bin/python3 -c '' diff --git a/extras/benchmarks/01-read-info-plist/run.sh b/extras/benchmarks/01-read-info-plist/run.sh index af73361e..a7fff19d 100755 --- a/extras/benchmarks/01-read-info-plist/run.sh +++ b/extras/benchmarks/01-read-info-plist/run.sh @@ -1,3 +1,3 @@ #!/bin/bash -/usr/bin/python ./script.py +/usr/bin/python3 ./script.py diff --git a/extras/benchmarks/01-read-info-plist/script.py b/extras/benchmarks/01-read-info-plist/script.py index fc563a87..f2503bfc 100644 --- a/extras/benchmarks/01-read-info-plist/script.py +++ b/extras/benchmarks/01-read-info-plist/script.py @@ -11,7 +11,7 @@ """ """ -from __future__ import print_function, unicode_literals, absolute_import + import sys diff --git a/extras/benchmarks/02-large-info-plist/run.sh b/extras/benchmarks/02-large-info-plist/run.sh index af73361e..a7fff19d 100755 --- a/extras/benchmarks/02-large-info-plist/run.sh +++ b/extras/benchmarks/02-large-info-plist/run.sh @@ -1,3 +1,3 @@ #!/bin/bash -/usr/bin/python ./script.py +/usr/bin/python3 ./script.py diff --git a/extras/benchmarks/02-large-info-plist/script.py b/extras/benchmarks/02-large-info-plist/script.py index fc563a87..f2503bfc 100644 --- a/extras/benchmarks/02-large-info-plist/script.py +++ b/extras/benchmarks/02-large-info-plist/script.py @@ -11,7 +11,7 @@ """ """ -from __future__ import print_function, unicode_literals, absolute_import + import sys diff --git a/extras/benchmarks/03-read-envvars/run.sh b/extras/benchmarks/03-read-envvars/run.sh index 38eb63cf..6f618a3c 100755 --- a/extras/benchmarks/03-read-envvars/run.sh +++ b/extras/benchmarks/03-read-envvars/run.sh @@ -7,4 +7,4 @@ export alfred_workflow_data="$HOME/Library/Application Support/Alfred 2/Workflow # export alfred_workflow_name="Alfred Workflow" # export alfred_workflow_version="1.1.1" -/usr/bin/python ./script.py +/usr/bin/python3 ./script.py diff --git a/extras/benchmarks/03-read-envvars/script.py b/extras/benchmarks/03-read-envvars/script.py index fc563a87..f2503bfc 100644 --- a/extras/benchmarks/03-read-envvars/script.py +++ b/extras/benchmarks/03-read-envvars/script.py @@ -11,7 +11,7 @@ """ """ -from __future__ import print_function, unicode_literals, absolute_import + import sys diff --git a/extras/gen_icon_table.py b/extras/gen_icon_table.py index 688c4a68..4e8713af 100644 --- a/extras/gen_icon_table.py +++ b/extras/gen_icon_table.py @@ -14,7 +14,7 @@ """ -from __future__ import print_function, unicode_literals + import os import subprocess diff --git a/extras/generate_workflow_list.py b/extras/generate_workflow_list.py index d2bfcf41..257761f5 100755 --- a/extras/generate_workflow_list.py +++ b/extras/generate_workflow_list.py @@ -10,7 +10,7 @@ """Generate a list of workflows on Packal that use Alfred-Workflow.""" -from __future__ import print_function, unicode_literals, absolute_import + import argparse import csv @@ -212,7 +212,7 @@ def read_list(path): for workflow in reader: # Decode text # log.debug('workflow=%r', workflow) - for k, v in workflow.items(): + for k, v in list(workflow.items()): if v is not None: workflow[k] = v.decode('utf-8') diff --git a/requirements-test.txt b/requirements-test.txt index 4e5a9c6c..83f133f9 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,7 +1,6 @@ -pyobjc-framework-Cocoa==5.3 +pyobjc-framework-Cocoa==8.5 pytest==4.6.10 pytest-cov==2.8.1 -pytest-httpbin==1.0.0 pytest-localserver==0.5.0 # tox==3.15.1 # twine==1.15.0 diff --git a/setup.py b/setup.py index 9e66e4c3..236c68de 100644 --- a/setup.py +++ b/setup.py @@ -54,7 +54,9 @@ def run_tests(self): 'Operating System :: MacOS :: MacOS X', 'Intended Audience :: Developers', 'Natural Language :: English', - 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Software Development :: Libraries', 'Topic :: Software Development :: Libraries :: Application Frameworks', ] @@ -62,7 +64,6 @@ def run_tests(self): 'coverage', 'pytest', 'pytest_cov', - 'pytest_httpbin', 'pytest_localserver', ] zip_safe = False diff --git a/tests/README.md b/tests/README.md index b61562ab..0e3cb550 100644 --- a/tests/README.md +++ b/tests/README.md @@ -16,7 +16,7 @@ in the project root to run the full test suite in place with coverage. ```bash tox ``` -in the project root to build, install and test with Python 2.6 and 2.7. +in the project root to build, install and test with Python. Testing a single module with coverage diff --git a/tests/conftest.py b/tests/conftest.py index be978224..da9ac770 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,12 +10,12 @@ """Common pytest fixtures.""" -from __future__ import print_function, absolute_import from contextlib import contextmanager import os from shutil import rmtree from tempfile import mkdtemp +from unittest import mock import pytest @@ -96,21 +96,24 @@ @contextmanager def env(**kwargs): """Context manager to alter and restore system environment.""" - prev = os.environ.copy() - for k, v in kwargs.items(): - if v is None: - if k in os.environ: - del os.environ[k] - else: - if isinstance(v, unicode): - v = v.encode('utf-8') - else: - v = str(v) - os.environ[k] = v - - yield - - os.environ = prev + deleted_items = { + key: os.environ[key] + for key in kwargs + if kwargs[key] is None + and key in os.environ + } + changed_items = { + key: value + for (key, value) in kwargs.items() + if value is not None + } + with mock.patch.dict(os.environ, changed_items): + for key in deleted_items: + del os.environ[key] + try: + yield + finally: + os.environ.update(deleted_items) @pytest.fixture @@ -129,7 +132,7 @@ def setenv(*dicts): def cleanenv(): """Remove Alfred variables from ``os.environ``.""" - for k in os.environ.keys(): + for k in list(os.environ.keys()): if k.startswith('alfred_'): del os.environ[k] diff --git a/tests/test_background.py b/tests/test_background.py index 42dbbc8c..03b98f94 100644 --- a/tests/test_background.py +++ b/tests/test_background.py @@ -10,7 +10,6 @@ """Unit tests for :mod:`workflow.background`.""" -from __future__ import print_function, absolute_import import os from time import sleep @@ -27,8 +26,8 @@ def _pidfile(name): def _write_pidfile(name, pid): pidfile = _pidfile(name) - with open(pidfile, 'wb') as file: - file.write('{0}'.format(pid)) + with open(pidfile, 'w') as file: + file.write(str(int(pid))) def _delete_pidfile(name): diff --git a/tests/test_notify.py b/tests/test_notify.py index acac296f..704d612f 100644 --- a/tests/test_notify.py +++ b/tests/test_notify.py @@ -10,7 +10,6 @@ """Unit tests for notifications.""" -from __future__ import print_function import hashlib import logging @@ -24,8 +23,8 @@ from workflow import notify from workflow.workflow import Workflow -from conftest import BUNDLE_ID -from util import ( +from .conftest import BUNDLE_ID +from .util import ( FakePrograms, WorkflowMock, ) @@ -80,11 +79,15 @@ def test_install(infopl, alfred4, applet): notify.install_notifier() for p in (APP_PATH, APPLET_PATH, ICON_PATH, INFO_PATH): assert os.path.exists(p) is True, "path not found" + # Ensure applet is executable assert (os.stat(APPLET_PATH).st_mode & stat.S_IXUSR), \ "applet not executable" + # Verify bundle ID was changed - data = plistlib.readPlist(INFO_PATH) + with open(INFO_PATH, 'rb') as plist_fp: + data = plistlib.load(plist_fp) + bid = data.get('CFBundleIdentifier') assert bid != BUNDLE_ID, "bundle IDs identical" assert bid.startswith(BUNDLE_ID) is True, "bundle ID not prefix" diff --git a/tests/test_update.py b/tests/test_update.py index f20da903..3411fcfe 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -10,16 +10,16 @@ """Unit tests for update mechanism.""" -from __future__ import print_function from contextlib import contextmanager +from unittest import mock import os import re import pytest import pytest_localserver # noqa: F401 -from util import WorkflowMock +from .util import WorkflowMock from workflow import Workflow, update, web from workflow.update import Download, Version @@ -36,7 +36,7 @@ os.path.join(DATA_DIR, 'gh-releases-4plus.json')).read() # A dummy Alfred workflow DATA_WORKFLOW = open( - os.path.join(DATA_DIR, 'Dummy-6.0.alfredworkflow')).read() + os.path.join(DATA_DIR, 'Dummy-6.0.alfredworkflow'), 'rb').read() # Alfred 4 RELEASE_LATEST = '9.0' @@ -82,19 +82,18 @@ @contextmanager def fakeresponse(httpserver, content, headers=None): """Monkey patch `web.request()` to return the specified response.""" - orig = web.request - httpserver.serve_content(content, headers=headers) + original_request = web.request def _request(*args, **kwargs): """Replace request URL with `httpserver` URL""" # print('requested URL={!r}'.format(args[1])) args = (args[0], httpserver.url) + args[2:] # print('request args={!r}'.format(args)) - return orig(*args, **kwargs) + return original_request(*args, **kwargs) - web.request = _request - yield - web.request = orig + httpserver.serve_content(content, headers=headers) + with mock.patch('workflow.web.request', _request): + yield def test_parse_releases(infopl, alfred4): diff --git a/tests/test_update_versions.py b/tests/test_update_versions.py index 62deeab4..567ddabc 100644 --- a/tests/test_update_versions.py +++ b/tests/test_update_versions.py @@ -10,7 +10,6 @@ """Test `update.Version` class.""" -from __future__ import print_function import unittest @@ -114,7 +113,7 @@ def test_compare_versions(self): self.assertTrue(Version('v1.10.0-alpha') < Version('1.10.0-beta')) # Complex suffixes self.assertTrue(Version('1.0.0-alpha') < Version('1.0.0-alpha.1')) - self.assertTrue(Version('1.0.0-alpha.1') < Version('1.0.0-alpha.beta')) + self.assertTrue(Version('1.0.0-alpha.1') > Version('1.0.0-alpha.beta')) self.assertTrue(Version('1.0.0-alpha.beta') < Version('1.0.0-beta')) self.assertTrue(Version('1.0.0-beta') < Version('1.0.0-beta.2')) self.assertTrue(Version('1.0.0-beta.2') < Version('1.0.0-beta.11')) diff --git a/tests/test_util.py b/tests/test_util.py index f0cc290d..80457c4e 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -9,7 +9,6 @@ """Unit tests for workflow/util.py.""" -from __future__ import print_function, absolute_import import os import shutil @@ -35,7 +34,6 @@ set_theme, unicodify, unset_config, - utf8ify, ) from .util import MockCall @@ -59,59 +57,43 @@ def test_unicodify(): """Unicode decoding.""" data = [ # input, normalisation form, expected output - (u'Köln', None, u'Köln'), - ('Köln', None, u'Köln'), - (u'Köln', 'NFC', u'K\xf6ln'), - (u'Köln', 'NFD', u'Ko\u0308ln'), - ('UTF-8', None, u'UTF-8'), + ('Köln', None, 'Köln'), + ('Köln', None, 'Köln'), + ('Köln', 'NFC', 'K\xf6ln'), + ('Köln', 'NFD', 'Ko\u0308ln'), + ('UTF-8', None, 'UTF-8'), ] for b, n, x in data: s = unicodify(b, norm=n) assert s == x - assert isinstance(s, unicode) - - -def test_utf8ify(): - """UTF-8 encoding.""" - data = [ - # input, expected output - (u'Köln', 'Köln'), - ('UTF-8', 'UTF-8'), - (10, '10'), - ([1, 2, 3], '[1, 2, 3]'), - ] - - for s, x in data: - r = utf8ify(s) - assert x == r - assert isinstance(x, str) + assert isinstance(s, str) def test_applescript_escape(): """Escape AppleScript strings.""" data = [ # input, expected output - (u'no change', u'no change'), - (u'has "quotes" in it', u'has " & quote & "quotes" & quote & " in it'), + ('no change', 'no change'), + ('has "quotes" in it', 'has " & quote & "quotes" & quote & " in it'), ] for s, x in data: r = applescriptify(s) assert x == r - assert isinstance(x, unicode) + assert isinstance(x, str) def test_run_command(): """Run command.""" data = [ # command, expected output - ([u'echo', '-n', 1], '1'), - ([u'echo', '-n', u'Köln'], 'Köln'), + (['echo', '-n', 1], '1'), + (['echo', '-n', 'Köln'], 'Köln'), ] for cmd, x in data: - r = run_command(cmd) + r = run_command(cmd).decode('utf-8') assert r == x with pytest.raises(subprocess.CalledProcessError): @@ -122,14 +104,14 @@ def test_run_applescript(testfile): """Run AppleScript.""" # Run script passed as text out = run_applescript('return "1"') - assert out.strip() == '1' + assert out.strip() == b'1' # Run script file - with open(testfile, 'wb') as fp: + with open(testfile, 'w') as fp: fp.write('return "1"') out = run_applescript(testfile) - assert out.strip() == '1' + assert out.strip() == b'1' # Test args script = """ @@ -138,7 +120,7 @@ def test_run_applescript(testfile): end run """ out = run_applescript(script, 1) - assert out.strip() == '1' + assert out.strip() == b'1' def test_run_jxa(testfile): @@ -151,14 +133,14 @@ def test_run_jxa(testfile): # Run script passed as text out = run_jxa(script) - assert out.strip() == '1' + assert out.strip() == b'1' # Run script file - with open(testfile, 'wb') as fp: + with open(testfile, 'w') as fp: fp.write(script) out = run_jxa(testfile) - assert out.strip() == '1' + assert out.strip() == b'1' # Test args script = """ @@ -167,7 +149,7 @@ def test_run_jxa(testfile): } """ out = run_jxa(script, 1) - assert out.strip() == '1' + assert out.strip() == b'1' def test_app_name(): @@ -396,12 +378,12 @@ def test_set_theme(alfred4): def test_appinfo(): """App info for Safari.""" for name, bundleid, path in [ - (u'Safari', u'com.apple.Safari', u'/Applications/Safari.app'), - (u'Console', u'com.apple.Console', - u'/Applications/Utilities/Console.app'), + ('Safari', 'com.apple.Safari', '/Applications/Safari.app'), + ('Console', 'com.apple.Console', + '/Applications/Utilities/Console.app'), # Catalina - (u'Console', u'com.apple.Console', - u'/System/Applications/Utilities/Console.app'), + ('Console', 'com.apple.Console', + '/System/Applications/Utilities/Console.app'), ]: if not os.path.exists(path): @@ -413,7 +395,7 @@ def test_appinfo(): assert info.path == path assert info.bundleid == bundleid for s in info: - assert isinstance(s, unicode) + assert isinstance(s, str) # Non-existant app info = appinfo("Big, Hairy Man's Special Breakfast Pants") diff --git a/tests/test_util_atomic.py b/tests/test_util_atomic.py index b8669099..4a6df161 100644 --- a/tests/test_util_atomic.py +++ b/tests/test_util_atomic.py @@ -10,14 +10,13 @@ """Unit tests for :func:`~workflow.util.atomic_writer`.""" -from __future__ import print_function import json import os import pytest -from util import DEFAULT_SETTINGS +from .util import DEFAULT_SETTINGS from workflow.util import atomic_writer @@ -30,7 +29,7 @@ def _settings(tempdir): def test_write_file_succeed(tempdir): """Succeed, no temp file left""" p = _settings(tempdir) - with atomic_writer(p, 'wb') as fp: + with atomic_writer(p, 'w') as fp: json.dump(DEFAULT_SETTINGS, fp) assert len(os.listdir(tempdir)) == 1 @@ -56,7 +55,7 @@ def test_failed_after_writing(tempdir): p = _settings(tempdir) def write(): - with atomic_writer(p, 'wb') as fp: + with atomic_writer(p, 'w') as fp: json.dump(DEFAULT_SETTINGS, fp) raise Exception() @@ -72,11 +71,11 @@ def test_failed_without_overwriting(tempdir): mockSettings = {} def write(): - with atomic_writer(p, 'wb') as fp: + with atomic_writer(p, 'w') as fp: json.dump(mockSettings, fp) raise Exception() - with atomic_writer(p, 'wb') as fp: + with atomic_writer(p, 'w') as fp: json.dump(DEFAULT_SETTINGS, fp) assert len(os.listdir(tempdir)) == 1 diff --git a/tests/test_util_lockfile.py b/tests/test_util_lockfile.py index 8241bbd5..17294c67 100644 --- a/tests/test_util_lockfile.py +++ b/tests/test_util_lockfile.py @@ -10,7 +10,6 @@ """Test LockFile functionality.""" -from __future__ import print_function from collections import namedtuple from multiprocessing import Pool diff --git a/tests/test_util_uninterruptible.py b/tests/test_util_uninterruptible.py index b21a0cfd..d4a07109 100644 --- a/tests/test_util_uninterruptible.py +++ b/tests/test_util_uninterruptible.py @@ -10,7 +10,6 @@ """Unit tests for ``uninterruptible`` decorator.""" -from __future__ import print_function, absolute_import import os import signal diff --git a/tests/test_web.py b/tests/test_web.py index fc4d775c..a071fcb6 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -9,29 +9,28 @@ # """Unit tests for :mod:`workflow.web`""" -from __future__ import print_function, unicode_literals - -import os -import unittest -import urllib2 import json +import os import shutil import socket import sys import tempfile +import unittest +import urllib.error +import urllib.parse +import urllib.request from base64 import b64decode from pprint import pprint import pytest -import pytest_httpbin import pytest_localserver # noqa: F401 - from workflow import web - DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') +HTTPBIN_URL = 'https://eu.httpbin.org' + class CaseInsensitiveDictTests(unittest.TestCase): """Unit tests for CaseInsensitiveDict""" @@ -86,13 +85,13 @@ def test_set(self): self.assertEqual(d[k.lower()], v) d2 = {'Dogs': 'd', 'Elephants': 'e'} - for k, v in d2.items(): + for k, v in list(d2.items()): self.assertFalse(k in d) self.assertTrue(d.get(k) is None) d.update(d2) - for k, v in d2.items(): + for k, v in list(d2.items()): self.assertTrue(k in d) self.assertTrue(k.upper() in d) self.assertEqual(d.get(k), v) @@ -104,19 +103,18 @@ def test_iterators(self): self.assertEqual(sorted(d.keys()), sorted(self.data_dict.keys())) self.assertEqual(sorted(d.values()), sorted(self.data_dict.values())) - for k in d.iterkeys(): + for k in d.keys(): self.assertTrue(k in self.data_dict) - values = self.data_dict.values() + values = list(self.data_dict.values()) - for v in d.itervalues(): + for v in d.values(): self.assertTrue(v in values) - for t in d.iteritems(): + for t in d.items(): self.assertTrue(t in self.data_list) -@pytest_httpbin.use_class_based_httpbin class WebTests(unittest.TestCase): """Unit tests for workflow.web""" @@ -136,155 +134,155 @@ def tearDown(self): def test_404(self): """Non-existant URL raises HTTPError w/ 404""" - url = self.httpbin.url + '/status/404' + url = HTTPBIN_URL + '/status/404' r = web.get(url) - self.assertRaises(urllib2.HTTPError, r.raise_for_status) - self.assert_(r.status_code == 404) + self.assertRaises(urllib.error.HTTPError, r.raise_for_status) + self.assertTrue(r.status_code == 404) def test_follow_redirect(self): """Redirects are followed""" - url = self.httpbin.url + '/redirect-to?url=' + self.httpbin.url + url = HTTPBIN_URL + '/redirect-to?url=' + HTTPBIN_URL r = web.get(url) - self.assertEqual(r.url.rstrip('/'), self.httpbin.url.rstrip('/')) + self.assertEqual(r.url.rstrip('/'), HTTPBIN_URL.rstrip('/')) def test_no_follow_redirect(self): """Redirects are not followed""" - url = self.httpbin.url + '/redirect-to?url=' + self.httpbin.url + url = HTTPBIN_URL + '/redirect-to?url=' + HTTPBIN_URL r = web.get(url, allow_redirects=False) - self.assertNotEquals(r.url, self.httpbin.url) - self.assertRaises(urllib2.HTTPError, r.raise_for_status) + self.assertNotEqual(r.url, HTTPBIN_URL) + self.assertRaises(urllib.error.HTTPError, r.raise_for_status) self.assertEqual(r.status_code, 302) def test_post_form(self): """POST Form data""" - url = self.httpbin.url + '/post' + url = HTTPBIN_URL + '/post' r = web.post(url, data=self.data) - self.assert_(r.status_code == 200) + self.assertTrue(r.status_code == 200) r.raise_for_status() form = r.json()['form'] for key in self.data: - self.assert_(form[key] == self.data[key]) + self.assertTrue(form[key] == self.data[key]) def test_post_json(self): """POST request with JSON body""" - url = self.httpbin.url + '/post' + url = HTTPBIN_URL + '/post' headers = {'content-type': 'application/json'} r = web.post(url, headers=headers, data=json.dumps(self.data)) - self.assert_(r.status_code == 200) + self.assertTrue(r.status_code == 200) data = r.json() pprint(data) self.assertEqual(data['headers']['Content-Type'], 'application/json') for key in self.data: - self.assert_(data['json'][key] == self.data[key]) + self.assertTrue(data['json'][key] == self.data[key]) def test_post_without_data(self): """POST request without data""" - url = self.httpbin + '/post' + url = HTTPBIN_URL + '/post' r = web.post(url) - self.assert_(r.status_code == 200) + self.assertTrue(r.status_code == 200) r.raise_for_status() def test_put_form(self): """PUT Form data""" - url = self.httpbin.url + '/put' + url = HTTPBIN_URL + '/put' r = web.put(url, data=self.data) - self.assert_(r.status_code == 200) + self.assertTrue(r.status_code == 200) r.raise_for_status() form = r.json()['form'] for key in self.data: - self.assert_(form[key] == self.data[key]) + self.assertTrue(form[key] == self.data[key]) def test_put_json(self): """PUT request with JSON body""" - url = self.httpbin + '/delete' + url = HTTPBIN_URL + '/delete' headers = {'content-type': 'application/json'} r = web.delete(url, headers=headers, data=json.dumps(self.data)) - self.assert_(r.status_code == 200) + self.assertTrue(r.status_code == 200) data = r.json() pprint(data) self.assertEqual(data['headers']['Content-Type'], 'application/json') for key in self.data: - self.assert_(data['json'][key] == self.data[key]) + self.assertTrue(data['json'][key] == self.data[key]) def test_put_without_data(self): """PUT request without data""" - url = self.httpbin + '/put' + url = HTTPBIN_URL + '/put' r = web.put(url) - self.assert_(r.status_code == 200) + self.assertTrue(r.status_code == 200) r.raise_for_status() def test_delete(self): """DELETE request""" - url = self.httpbin + '/delete' + url = HTTPBIN_URL + '/delete' r = web.delete(url) pprint(r.json()) - self.assert_(r.status_code == 200) + self.assertTrue(r.status_code == 200) r.raise_for_status() def test_delete_with_json(self): """DELETE request with JSON body""" - url = self.httpbin + '/delete' + url = HTTPBIN_URL + '/delete' headers = {'content-type': 'application/json'} r = web.delete(url, headers=headers, data=json.dumps(self.data)) - self.assert_(r.status_code == 200) + self.assertTrue(r.status_code == 200) data = r.json() pprint(data) self.assertEqual(data['headers']['Content-Type'], 'application/json') for key in self.data: - self.assert_(data['json'][key] == self.data[key]) + self.assertTrue(data['json'][key] == self.data[key]) def test_timeout(self): """Request times out""" - url = self.httpbin.url + '/delay/3' + url = HTTPBIN_URL + '/delay/3' if sys.version_info < (2, 7): - self.assertRaises(urllib2.URLError, web.get, url, timeout=1) + self.assertRaises(urllib.error.URLError, web.get, url, timeout=1) else: self.assertRaises(socket.timeout, web.get, url, timeout=1) def test_encoding(self): """HTML is decoded""" - url = self.httpbin.url + '/html' + url = HTTPBIN_URL + '/html' r = web.get(url) self.assertEqual(r.encoding, 'utf-8') - self.assert_(isinstance(r.text, unicode)) + self.assertTrue(isinstance(r.text, str)) def test_no_encoding(self): """No encoding""" # Is an image - url = self.httpbin.url + '/bytes/100' + url = HTTPBIN_URL + '/bytes/100' r = web.get(url) self.assertEqual(r.encoding, None) - self.assert_(isinstance(r.text, str)) + self.assertTrue(isinstance(r.text, bytes)) def test_html_encoding(self): """HTML is decoded""" - url = self.httpbin.url + '/html' + url = HTTPBIN_URL + '/html' r = web.get(url) self.assertEqual(r.encoding, 'utf-8') - self.assert_(isinstance(r.text, unicode)) + self.assertTrue(isinstance(r.text, str)) def test_default_encoding(self): """Default encodings for mimetypes.""" - url = self.httpbin.url + '/response-headers' + url = HTTPBIN_URL + '/response-headers' r = web.get(url) r.raise_for_status() # httpbin returns JSON by default. web.py should automatically # set `encoding` to UTF-8 when mimetype = 'application/json' assert r.encoding == 'utf-8' - assert isinstance(r.text, unicode) + assert isinstance(r.text, str) def test_xml_encoding(self): """XML is decoded.""" - url = self.httpbin.url + '/response-headers' + url = HTTPBIN_URL + '/response-headers' params = {'Content-Type': 'text/xml; charset=UTF-8'} r = web.get(url, params) r.raise_for_status() assert r.encoding == 'utf-8' - assert isinstance(r.text, unicode) + assert isinstance(r.text, str) def test_get_vars(self): """GET vars""" - url = self.httpbin.url + '/get' + url = HTTPBIN_URL + '/get' r = web.get(url, params=self.data) self.assertEqual(r.status_code, 200) args = r.json()['args'] @@ -293,7 +291,7 @@ def test_get_vars(self): def test_auth_succeeds(self): """Basic AUTH succeeds""" - url = self.httpbin.url + '/basic-auth/bobsmith/password1' + url = HTTPBIN_URL + '/basic-auth/bobsmith/password1' r = web.get(url, auth=('bobsmith', 'password1')) self.assertEqual(r.status_code, 200) data = r.json() @@ -302,14 +300,14 @@ def test_auth_succeeds(self): def test_auth_fails(self): """Basic AUTH fails""" - url = self.httpbin.url + '/basic-auth/bobsmith/password1' + url = HTTPBIN_URL + '/basic-auth/bobsmith/password1' r = web.get(url, auth=('bobsmith', 'password2')) self.assertEqual(r.status_code, 401) - self.assertRaises(urllib2.HTTPError, r.raise_for_status) + self.assertRaises(urllib.error.HTTPError, r.raise_for_status) def test_file_upload(self): """File upload""" - url = self.httpbin.url + '/post' + url = HTTPBIN_URL + '/post' files = {'file': {'filename': 'cönfüsed.gif', 'content': open(self.test_file, 'rb').read(), 'mimetype': 'image/gif', @@ -323,13 +321,13 @@ def test_file_upload(self): # image bindata = data['files']['file'] preamble = 'data:image/gif;base64,' - self.assert_(bindata.startswith(preamble)) + self.assertTrue(bindata.startswith(preamble)) bindata = b64decode(bindata[len(preamble):]) self.assertEqual(bindata, open(self.test_file, 'rb').read()) def test_file_upload_without_form_data(self): """File upload w/o form data""" - url = self.httpbin.url + '/post' + url = HTTPBIN_URL + '/post' files = {'file': {'filename': 'cönfüsed.gif', 'content': open(self.test_file, 'rb').read() }} @@ -339,13 +337,26 @@ def test_file_upload_without_form_data(self): # image bindata = data['files']['file'] preamble = 'data:image/gif;base64,' - self.assert_(bindata.startswith(preamble)) + self.assertTrue(bindata.startswith(preamble)) bindata = b64decode(bindata[len(preamble):]) self.assertEqual(bindata, open(self.test_file, 'rb').read()) + def test_file_upload_with_unicode(self): + """File upload with Unicode contents is converted to bytes""" + url = HTTPBIN_URL + '/post' + content = 'Hére ïs søme ÜÑÎÇÒDÈ™' + files = {'file': {'filename': 'cönfüsed.txt', + 'content': content + }} + r = web.post(url, files=files) + self.assertEqual(r.status_code, 200) + data = r.json() + bindata = data['files']['file'] + self.assertEqual(bindata, content) + def test_json_encoding(self): """JSON decoded correctly""" - url = self.httpbin.url + '/get' + url = HTTPBIN_URL + '/get' params = {'town': 'münchen'} r = web.get(url, params) self.assertEqual(r.status_code, 200) @@ -354,7 +365,7 @@ def test_json_encoding(self): def test_gzipped_content(self): """Gzipped content decoded""" - url = self.httpbin.url + '/gzip' + url = HTTPBIN_URL + '/gzip' r = web.get(url) self.assertEqual(r.status_code, 200) data = r.json() @@ -362,7 +373,7 @@ def test_gzipped_content(self): def test_gzipped_iter_content(self): """Gzipped iter_content decoded""" - url = self.httpbin.url + '/gzip' + url = HTTPBIN_URL + '/gzip' r = web.get(url, stream=True) self.assertEqual(r.status_code, 200) data = b'' @@ -373,7 +384,7 @@ def test_gzipped_iter_content(self): def test_params_added_to_url(self): """`params` are added to existing GET args""" - url = self.httpbin.url + '/get?existing=one' + url = HTTPBIN_URL + '/get?existing=one' r = web.get(url) r.raise_for_status() args = r.json()['args'] @@ -400,22 +411,22 @@ def test_params_added_to_url(self): # dP d8888P fubar_path = os.path.join(DATA_DIR, 'fubar.txt') -fubar_bytes = open(fubar_path).read() -fubar_unicode = unicode(fubar_bytes, 'utf-8') +fubar_bytes = open(fubar_path, 'rb').read() +fubar_unicode = str(fubar_bytes, 'utf-8') utf8html_path = os.path.join(DATA_DIR, 'utf8.html') -utf8html_bytes = open(utf8html_path).read() -utf8html_unicode = unicode(utf8html_bytes, 'utf-8') +utf8html_bytes = open(utf8html_path, 'rb').read() +utf8html_unicode = str(utf8html_bytes, 'utf-8') utf8xml_path = os.path.join(DATA_DIR, 'utf8.xml') -utf8xml_bytes = open(utf8xml_path).read() -utf8xml_unicode = unicode(utf8xml_bytes, 'utf-8') +utf8xml_bytes = open(utf8xml_path, 'rb').read() +utf8xml_unicode = str(utf8xml_bytes, 'utf-8') gifpath = os.path.join(DATA_DIR, 'cönfüsed.gif') -gifbytes = open(gifpath).read() +gifbytes = open(gifpath, 'rb').read() gifpath_gzip = os.path.join(DATA_DIR, 'cönfüsed.gif.gz') -gifbytes_gzip = open(gifpath_gzip).read() +gifbytes_gzip = open(gifpath_gzip, 'rb').read() tempdir = os.path.join(tempfile.gettempdir(), 'web_py.{0}.tmp'.format(os.getpid())) @@ -430,7 +441,7 @@ def test_charset_sniffing(httpserver): r = web.get(httpserver.url) r.raise_for_status() assert r.encoding == 'utf-8' - assert isinstance(r.text, unicode) + assert isinstance(r.text, str) def test_save_to_path(httpserver): @@ -446,7 +457,7 @@ def test_save_to_path(httpserver): r.save_to_path(filepath) assert os.path.exists(filepath) - data = open(filepath).read() + data = open(filepath, 'rb').read() assert data == fubar_bytes finally: diff --git a/tests/test_web_http_encoding.py b/tests/test_web_http_encoding.py index e052f461..76b0cf12 100644 --- a/tests/test_web_http_encoding.py +++ b/tests/test_web_http_encoding.py @@ -10,7 +10,6 @@ """HTTP unit tests.""" -from __future__ import print_function import os @@ -55,7 +54,7 @@ def test_web_encoding(httpserver): print('filepath={0!r}, headers={1!r}, encoding={2!r}'.format( filepath, headers, encoding)) - content = open(filepath).read() + content = open(filepath, 'rb').read() httpserver.serve_content(content, headers=headers) r = web.get(httpserver.url) diff --git a/tests/test_workflow.py b/tests/test_workflow.py index 626fa667..d4c64753 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -10,7 +10,6 @@ """Unit tests for :mod:`workflow.Workflow`.""" -from __future__ import print_function, unicode_literals import logging import os diff --git a/tests/test_workflow3.py b/tests/test_workflow3.py index 57640454..dd604eb0 100644 --- a/tests/test_workflow3.py +++ b/tests/test_workflow3.py @@ -10,11 +10,10 @@ """Test Workflow3 feedback.""" -from __future__ import print_function, absolute_import import json import os -from StringIO import StringIO +from io import StringIO import sys import pytest @@ -106,10 +105,10 @@ def test_feedback(infopl): def test_warn_empty(infopl): """Workflow3: Warn empty.""" wf = Workflow3() - it = wf.warn_empty(u'My warning') + it = wf.warn_empty('My warning') - assert it.title == u'My warning' - assert it.subtitle == u'' + assert it.title == 'My warning' + assert it.subtitle == '' assert it.valid is False assert it.icon == ICON_WARNING @@ -120,8 +119,8 @@ def test_warn_empty(infopl): # Non-empty feedback wf = Workflow3() - wf.add_item(u'Real item') - it = wf.warn_empty(u'Warning') + wf.add_item('Real item') + it = wf.warn_empty('Warning') assert it is None @@ -491,8 +490,8 @@ def cb(wf): def test_variables_plain_arg(): """Arg-only returns string, not JSON.""" - v = Variables(arg=u'test') - assert unicode(v) == u'test' + v = Variables(arg='test') + assert str(v) == 'test' assert str(v) == 'test' @@ -503,13 +502,13 @@ def test_variables_multiple_args(infopl): v = Variables(arg=arg) assert v.obj == {'alfredworkflow': {'arg': arg}} assert str(v) == js - assert unicode(v) == js + assert str(v) == js def test_variables_empty(): """Empty Variables returns empty string.""" v = Variables() - assert unicode(v) == u'' + assert str(v) == '' assert str(v) == '' @@ -528,18 +527,18 @@ def test_variables_config(): def test_variables_unicode(): """Unicode handled correctly.""" - v = Variables(arg=u'fübar', englisch='englisch') - v[u'französisch'] = u'französisch' - v.config[u'über'] = u'über' + v = Variables(arg='fübar', englisch='englisch') + v['französisch'] = 'französisch' + v.config['über'] = 'über' d = { 'alfredworkflow': { - 'arg': u'fübar', + 'arg': 'fübar', 'variables': { - 'englisch': u'englisch', - u'französisch': u'französisch', + 'englisch': 'englisch', + 'französisch': 'französisch', }, - 'config': {u'über': u'über'} + 'config': {'über': 'über'} } } print(repr(v.obj)) @@ -547,7 +546,7 @@ def test_variables_unicode(): assert v.obj == d # Round-trip to JSON and back - d2 = json.loads(unicode(v)) + d2 = json.loads(str(v)) assert d2 == d diff --git a/tests/test_workflow_encoding.py b/tests/test_workflow_encoding.py index 3e526251..c9e85bf0 100644 --- a/tests/test_workflow_encoding.py +++ b/tests/test_workflow_encoding.py @@ -6,24 +6,23 @@ """Unit tests for serializers.""" -from __future__ import print_function, unicode_literals import pytest def test_unicode_paths(wf): """Workflow paths are Unicode""" - s = b'test.txt' - u = u'über.txt' - assert isinstance(wf.datadir, unicode) - assert isinstance(wf.datafile(s), unicode) - assert isinstance(wf.datafile(u), unicode) - assert isinstance(wf.cachedir, unicode) - assert isinstance(wf.cachefile(s), unicode) - assert isinstance(wf.cachefile(u), unicode) - assert isinstance(wf.workflowdir, unicode) - assert isinstance(wf.workflowfile(s), unicode) - assert isinstance(wf.workflowfile(u), unicode) + b = b'test.txt' + s = 'über.txt' + assert isinstance(wf.datadir, str) + assert isinstance(wf.datafile(b), str) + assert isinstance(wf.datafile(s), str) + assert isinstance(wf.cachedir, str) + assert isinstance(wf.cachefile(b), str) + assert isinstance(wf.cachefile(s), str) + assert isinstance(wf.workflowdir, str) + assert isinstance(wf.workflowfile(b), str) + assert isinstance(wf.workflowfile(s), str) if __name__ == '__main__': # pragma: no cover diff --git a/tests/test_workflow_env.py b/tests/test_workflow_env.py index 1e084386..2be8bc9a 100644 --- a/tests/test_workflow_env.py +++ b/tests/test_workflow_env.py @@ -6,7 +6,6 @@ """Unit tests for environment/info.plist.""" -from __future__ import print_function, unicode_literals import logging import os @@ -41,13 +40,13 @@ def test_env(wf): """Alfred environmental variables""" env = COMMON.copy() env.update(ENV_V4) - for k, v in env.items(): + for k, v in list(env.items()): k = k.replace('alfred_', '') if k in ('debug', 'version_build', 'theme_subtext'): assert int(v) == wf.alfred_env[k] else: - assert isinstance(wf.alfred_env[k], unicode) - assert unicode(v) == wf.alfred_env[k] + assert isinstance(wf.alfred_env[k], str) + assert str(v) == wf.alfred_env[k] assert wf.datadir == env['alfred_workflow_data'] assert wf.cachedir == env['alfred_workflow_cache'] diff --git a/tests/test_workflow_files.py b/tests/test_workflow_files.py index ca7bf1f4..a468c5d2 100644 --- a/tests/test_workflow_files.py +++ b/tests/test_workflow_files.py @@ -6,7 +6,6 @@ """Unit tests for Workflow directory & file APIs.""" -from __future__ import print_function, unicode_literals import json import os @@ -16,7 +15,7 @@ from workflow import manager, Workflow -from conftest import env, ENV_V4, ENV_V2 +from .conftest import env, ENV_V4, ENV_V2 def test_directories(alfred4): @@ -226,7 +225,7 @@ def load(self, file_obj): @classmethod def dump(self, obj, file_obj): - return json.dump(obj, file_obj, indent=2) + file_obj.write(json.dumps(obj).encode('utf8')) manager.register('spoons', MySerializer) try: @@ -305,7 +304,7 @@ def test_borked_stored_data(wf): wf.store_data('test', data) metadata, datapath = _stored_data_paths(wf, 'test', 'cpickle') - with open(metadata, 'wb') as file_obj: + with open(metadata, 'w') as file_obj: file_obj.write('bangers and mash') wf.logger.debug('Changed format to `bangers and mash`') with pytest.raises(ValueError): diff --git a/tests/test_workflow_filter.py b/tests/test_workflow_filter.py index 593cce49..848c6e62 100644 --- a/tests/test_workflow_filter.py +++ b/tests/test_workflow_filter.py @@ -6,7 +6,6 @@ """Unit tests for :meth:`workflow.Workflow.filter`.""" -from __future__ import print_function, unicode_literals import pytest diff --git a/tests/test_workflow_import.py b/tests/test_workflow_import.py index 43cc2248..db11d041 100644 --- a/tests/test_workflow_import.py +++ b/tests/test_workflow_import.py @@ -6,7 +6,6 @@ """Unit tests for sys.path manipulation.""" -from __future__ import print_function, unicode_literals import os import sys @@ -16,7 +15,7 @@ from workflow.workflow import Workflow -LIBS = [os.path.join(os.path.dirname(__file__), b'lib')] +LIBS = [os.path.join(os.path.dirname(__file__), 'lib')] def test_additional_libs(alfred4, infopl): diff --git a/tests/test_workflow_keychain.py b/tests/test_workflow_keychain.py index dae2a201..07e973ae 100644 --- a/tests/test_workflow_keychain.py +++ b/tests/test_workflow_keychain.py @@ -6,7 +6,6 @@ """Unit tests for Keychain API.""" -from __future__ import print_function, unicode_literals import pytest diff --git a/tests/test_workflow_magic.py b/tests/test_workflow_magic.py index 7870014f..b193703a 100644 --- a/tests/test_workflow_magic.py +++ b/tests/test_workflow_magic.py @@ -10,7 +10,6 @@ """Unit tests for magic arguments.""" -from __future__ import print_function import os @@ -143,7 +142,7 @@ def test_delete_data(infopl): with WorkflowMock(['script', 'workflow:deldata']): wf = Workflow() testpath = wf.datafile('file.test') - with open(testpath, 'wb') as fp: + with open(testpath, 'w') as fp: fp.write('test!') assert os.path.exists(testpath) @@ -158,7 +157,7 @@ def test_delete_cache(infopl): with WorkflowMock(['script', 'workflow:delcache']): wf = Workflow() testpath = wf.cachefile('file.test') - with open(testpath, 'wb') as fp: + with open(testpath, 'w') as fp: fp.write('test!') assert os.path.exists(testpath) @@ -178,7 +177,7 @@ def test_reset(infopl): settings_path = wf.datafile('settings.json') for p in (datatest, cachetest): - with open(p, 'wb') as file_obj: + with open(p, 'w') as file_obj: file_obj.write('test!') for p in (datatest, cachetest, settings_path): diff --git a/tests/test_workflow_magic_alfred2.py b/tests/test_workflow_magic_alfred2.py index d832a02b..211eae33 100644 --- a/tests/test_workflow_magic_alfred2.py +++ b/tests/test_workflow_magic_alfred2.py @@ -6,7 +6,6 @@ """Unit tests for Alfred 2 magic argument handling.""" -from __future__ import print_function import pytest diff --git a/tests/test_workflow_run.py b/tests/test_workflow_run.py index 130ee469..1204dc3e 100644 --- a/tests/test_workflow_run.py +++ b/tests/test_workflow_run.py @@ -6,16 +6,15 @@ """Unit tests for Workflow.run.""" -from __future__ import print_function, unicode_literals -from StringIO import StringIO +from io import StringIO import sys import pytest from workflow.workflow import Workflow -from conftest import env +from .conftest import env def test_run_fails(infopl): @@ -93,7 +92,7 @@ def cb(wf2): def test_run_fails_borked_settings(wf): """Run fails with borked settings.json""" # Create invalid settings.json file - with open(wf.settings_path, 'wb') as fp: + with open(wf.settings_path, 'w') as fp: fp.write('') def fake(wf): diff --git a/tests/test_workflow_serializers.py b/tests/test_workflow_serializers.py index d78790d1..adc65e81 100644 --- a/tests/test_workflow_serializers.py +++ b/tests/test_workflow_serializers.py @@ -10,7 +10,6 @@ """Unit tests for serializer classes.""" -from __future__ import print_function, absolute_import import os @@ -19,7 +18,6 @@ from workflow.workflow import ( SerializerManager, JSONSerializer, - CPickleSerializer, PickleSerializer, manager as default_manager, ) @@ -33,7 +31,7 @@ def manager(): """Create a `SerializerManager` with the default config.""" m = SerializerManager() - m.register('cpickle', CPickleSerializer) + m.register('cpickle', PickleSerializer) m.register('pickle', PickleSerializer) m.register('json', JSONSerializer) yield m diff --git a/tests/test_workflow_settings.py b/tests/test_workflow_settings.py index 2af8f2d1..190e0d2b 100644 --- a/tests/test_workflow_settings.py +++ b/tests/test_workflow_settings.py @@ -10,7 +10,6 @@ """Unit tests for Workflow.settings API.""" -from __future__ import print_function, unicode_literals, absolute_import import json import os @@ -31,7 +30,7 @@ def setUp(self): """Initialise unit test environment.""" self.tempdir = tempfile.mkdtemp() self.settings_file = os.path.join(self.tempdir, 'settings.json') - with open(self.settings_file, 'wb') as file_obj: + with open(self.settings_file, 'w') as file_obj: json.dump(DEFAULT_SETTINGS, file_obj) def tearDown(self): @@ -86,7 +85,7 @@ def test_settings_not_rewritten(self): mt = os.path.getmtime(self.settings_file) time.sleep(1) # wait long enough to register changes in `time.time()` now = time.time() - for k, v in DEFAULT_SETTINGS.items(): + for k, v in list(DEFAULT_SETTINGS.items()): s[k] = v self.assertTrue(os.path.getmtime(self.settings_file) == mt) s['finished_at'] = now diff --git a/tests/test_workflow_update.py b/tests/test_workflow_update.py index 72485fe3..91fba298 100644 --- a/tests/test_workflow_update.py +++ b/tests/test_workflow_update.py @@ -10,7 +10,6 @@ """Unit tests for Workflow's update API.""" -from __future__ import print_function from contextlib import contextmanager @@ -28,7 +27,7 @@ delete_info_plist, dump_env, ) -from test_update import fakeresponse, RELEASES_JSON, HTTP_HEADERS_JSON +from .test_update import fakeresponse, RELEASES_JSON, HTTP_HEADERS_JSON UPDATE_SETTINGS = { @@ -91,15 +90,19 @@ def update(wf): with fakeresponse(httpserver, RELEASES_JSON, HTTP_HEADERS_JSON): with ctx() as (wf, c): wf.run(update) - assert c.cmd[0] == '/usr/bin/python' - assert c.cmd[2] == '__workflow_update_check' + assert c.cmd == [ + '/usr/bin/python3', '-m', 'workflow.background', + '__workflow_update_check' + ] update_settings = UPDATE_SETTINGS.copy() update_settings['prereleases'] = True with ctx(update_settings=update_settings) as (wf, c): wf.run(update) - assert c.cmd[0] == '/usr/bin/python' - assert c.cmd[2] == '__workflow_update_check' + assert c.cmd == [ + '/usr/bin/python3', '-m', 'workflow.background', + '__workflow_update_check' + ] def test_install_update(httpserver, alfred4): @@ -116,8 +119,10 @@ def fake(wf): print('Magic update command : {0!r}'.format(c.cmd)) - assert c.cmd[0] == '/usr/bin/python' - assert c.cmd[2] == '__workflow_update_install' + assert c.cmd == [ + '/usr/bin/python3', '-m', 'workflow.background', + '__workflow_update_install' + ] update_settings = UPDATE_SETTINGS.copy() del update_settings['version'] @@ -164,8 +169,10 @@ def fake(wf): print('Magic update command : {!r}'.format(c.cmd)) - assert c.cmd[0] == '/usr/bin/python' - assert c.cmd[2] == '__workflow_update_install' + assert c.cmd == [ + '/usr/bin/python3', '-m', 'workflow.background', + '__workflow_update_install' + ] with env(alfred_workflow_version='v10.0-beta'): update_settings = UPDATE_SETTINGS.copy() diff --git a/tests/test_workflow_versions.py b/tests/test_workflow_versions.py index e233c12f..81f0b41e 100644 --- a/tests/test_workflow_versions.py +++ b/tests/test_workflow_versions.py @@ -6,7 +6,6 @@ """Unit tests for workflow version determination.""" -from __future__ import print_function, unicode_literals import pytest diff --git a/tests/test_workflow_xml.py b/tests/test_workflow_xml.py index 38955bf2..832bb079 100644 --- a/tests/test_workflow_xml.py +++ b/tests/test_workflow_xml.py @@ -10,10 +10,9 @@ """Unit tests for Workflow's XML feedback generation.""" -from __future__ import print_function from contextlib import contextmanager -from StringIO import StringIO +from io import StringIO import sys from xml.etree import ElementTree as ET diff --git a/tests/util.py b/tests/util.py index 71ba4dba..edd8bd0a 100644 --- a/tests/util.py +++ b/tests/util.py @@ -10,9 +10,8 @@ """Stuff used in multiple tests.""" -from __future__ import print_function, unicode_literals -from cStringIO import StringIO +from io import StringIO import sys import os import shutil @@ -26,10 +25,10 @@ 'data/info.plist.alfred3') -INFO_PLIST_PATH = os.path.join(os.path.abspath(os.getcwdu()), +INFO_PLIST_PATH = os.path.join(os.path.abspath(os.getcwd()), 'info.plist') -VERSION_PATH = os.path.join(os.path.abspath(os.getcwdu()), +VERSION_PATH = os.path.join(os.path.abspath(os.getcwd()), 'version') DEFAULT_SETTINGS = { @@ -156,7 +155,7 @@ def __init__(self, version, path=None): def __enter__(self): """Create version file.""" - with open(self.path, 'wb') as fp: + with open(self.path, 'w') as fp: fp.write(self.version) print('version {0} in {1}'.format(self.version, self.path), file=sys.stderr) @@ -182,11 +181,11 @@ def __init__(self, *names, **names2codes): def __enter__(self): """Inject program(s) into PATH.""" self.tempdir = tempfile.mkdtemp() - for name, retcode in self.programs.items(): + for name, retcode in list(self.programs.items()): path = os.path.join(self.tempdir, name) - with open(path, 'wb') as fp: + with open(path, 'w') as fp: fp.write("#!/bin/bash\n\nexit {0}\n".format(retcode)) - os.chmod(path, 0700) + os.chmod(path, 0o700) # Add new programs to front of PATH self.orig_path = os.getenv('PATH') @@ -226,7 +225,7 @@ def __exit__(self, *args): def dump_env(): """Print `os.environ` to STDOUT.""" - for k, v in os.environ.items(): + for k, v in list(os.environ.items()): if k.startswith('alfred_'): print('env: %s=%s' % (k, v)) diff --git a/tox.ini b/tox.ini index b5897f08..228267ef 100644 --- a/tox.ini +++ b/tox.ini @@ -13,31 +13,22 @@ addopts = --doctest-modules [tox] -envlist=py27 +envlist=py37,py38,py39 [testenv] usedevelop = true deps = - pytest - pytest_httpbin - pytest_cov - pytest_localserver + -r requirements-test.txt coverage +commands = + ./run-tests.sh -commands = ./run-tests.sh - -[testenv:py27] +[testenv:lint] +usedevelop = true deps = - {[testenv]deps} - pyobjc-core - pyobjc-framework-Cocoa - -; [testenv:py26] -; deps = -; {[testenv]deps} - -; [pydocstyle] -; add_ignore = D105,D203,D266,D400,D401,D413 + -r requirements-test.txt +commands = + ./run-tests.sh -l [flake8] builtins = unicode diff --git a/workflow/background.py b/workflow/background.py index c2bd7352..417bbbb4 100644 --- a/workflow/background.py +++ b/workflow/background.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # encoding: utf-8 # # Copyright (c) 2014 deanishe@deanishe.net @@ -17,15 +17,16 @@ and examples. """ -from __future__ import print_function, unicode_literals + import signal import sys import os import subprocess import pickle +from pathlib import Path -from workflow import Workflow +from .workflow import Workflow __all__ = ['is_running', 'run_in_background'] @@ -47,9 +48,9 @@ def _arg_cache(name): """Return path to pickle cache file for arguments. :param name: name of task - :type name: ``unicode`` + :type name: ``str`` :returns: Path to cache file - :rtype: ``unicode`` filepath + :rtype: ``str`` filepath """ return wf().cachefile(name + '.argcache') @@ -59,9 +60,9 @@ def _pid_file(name): """Return path to PID file for ``name``. :param name: name of task - :type name: ``unicode`` + :type name: ``str`` :returns: Path to PID file for task - :rtype: ``unicode`` filepath + :rtype: ``str`` filepath """ return wf().cachefile(name + '.pid') @@ -140,8 +141,8 @@ def _fork_and_exit_parent(errmsg, wait=False, write=False): if pid > 0: if write: # write PID of child process to `pidfile` tmp = pidfile + '.tmp' - with open(tmp, 'wb') as fp: - fp.write(str(pid)) + with open(tmp, 'w') as fp: + fp.write(str(int(pid))) os.rename(tmp, pidfile) if wait: # wait for child process to exit os.waitpid(pid, 0) @@ -162,9 +163,9 @@ def _fork_and_exit_parent(errmsg, wait=False, write=False): # Now I am a daemon! # Redirect standard file descriptors. - si = open(stdin, 'r', 0) - so = open(stdout, 'a+', 0) - se = open(stderr, 'a+', 0) + si = open(stdin, 'rb', 0) + so = open(stdout, 'ab+', 0) + se = open(stderr, 'ab+', 0) if hasattr(sys.stdin, 'fileno'): os.dup2(si.fileno(), sys.stdin.fileno()) if hasattr(sys.stdout, 'fileno'): @@ -230,12 +231,12 @@ def run_in_background(name, args, **kwargs): _log().debug('[%s] command cached: %s', name, argcache) # Call this script - cmd = ['/usr/bin/python', __file__, name] + cmd = ['/usr/bin/python3', '-m', 'workflow.background', name] _log().debug('[%s] passing job to background runner: %r', name, cmd) - retcode = subprocess.call(cmd) + retcode = subprocess.call(cmd, cwd=Path(__file__).parent.parent) if retcode: # pragma: no cover - _log().error('[%s] background runner failed with %d', name, retcode) + _log().error('[%s] background runner (%r) failed with %d', name, cmd, retcode) else: _log().debug('[%s] background job started', name) diff --git a/workflow/notify.py b/workflow/notify.py index 28ec0b98..2759eb43 100644 --- a/workflow/notify.py +++ b/workflow/notify.py @@ -23,7 +23,7 @@ icon and then calls the application to post notifications. """ -from __future__ import print_function, unicode_literals + import os import plistlib @@ -34,7 +34,7 @@ import tempfile import uuid -import workflow +from . import workflow _wf = None @@ -144,10 +144,13 @@ def install_notifier(): # 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) - data = plistlib.readPlist(ip_path) + with open(ip_path, 'rb') as plist_fp: + data = plistlib.load(plist_fp) + log().debug('changing bundle ID to %r', bundle_id) data['CFBundleIdentifier'] = bundle_id - plistlib.writePlist(data, ip_path) + with open(ip_path, 'wb') as plist_fp: + plistlib.dump(data, plist_fp) def validate_sound(sound): diff --git a/workflow/update.py b/workflow/update.py index c039f7ae..14cbc3b0 100644 --- a/workflow/update.py +++ b/workflow/update.py @@ -21,7 +21,6 @@ """ -from __future__ import print_function, unicode_literals from collections import defaultdict from functools import total_ordering @@ -31,8 +30,8 @@ import re import subprocess -import workflow -import web +from . import workflow +from . import web # __all__ = [] @@ -119,7 +118,7 @@ def from_releases(cls, js): release['prerelease'])) valid = True - for ext, n in dupes.items(): + for ext, n in list(dupes.items()): if n > 1: wf().logger.debug('ignored release "%s": multiple assets ' 'with extension "%s"', tag, ext) @@ -143,7 +142,7 @@ def __init__(self, url, filename, version, prerelease=False): pre-release. Defaults to False. """ - if isinstance(version, basestring): + if isinstance(version, str): version = Version(version) self.url = url @@ -167,12 +166,10 @@ def dict(self): 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)) - - return u.encode('utf-8') + return ('Download(url={dl.url!r}, ' + 'filename={dl.filename!r}, ' + 'version={dl.version!r}, ' + 'prerelease={dl.prerelease!r})'.format(dl=self)) def __repr__(self): """Code-like representation of `Download`.""" @@ -228,7 +225,7 @@ def __init__(self, vstr): """Create new `Version` object. Args: - vstr (basestring): Semantic version string. + vstr (``str``): Semantic version string. """ if not vstr: raise ValueError('invalid version number: {!r}'.format(vstr)) @@ -276,7 +273,7 @@ def _parse_dotted_string(self, s): parsed = [] parts = s.split('.') for p in parts: - if p.isdigit(): + if p and all(c.isdigit() for c in p): p = int(p) parsed.append(p) return parsed @@ -299,8 +296,20 @@ 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) + lft = self._parse_dotted_string(self.suffix) + rgt = self._parse_dotted_string(other.suffix) + try: + return lft < rgt + except TypeError: + # Python 3 will not allow lt/gt comparisons of int & str. + while lft and rgt and lft[0] == rgt[0]: + lft.pop(0) + rgt.pop(0) + + # Alphanumeric versions are earlier than numeric versions, + # therefore lft < rgt if the right version is numeric. + return isinstance(rgt[0], int) + # t > o return False diff --git a/workflow/util.py b/workflow/util.py index ab5e9548..154b9881 100644 --- a/workflow/util.py +++ b/workflow/util.py @@ -10,7 +10,7 @@ """A selection of helper functions useful for building workflows.""" -from __future__ import print_function, absolute_import + import atexit from collections import namedtuple @@ -88,9 +88,9 @@ def jxa_app_name(): """ if os.getenv('alfred_version', '').startswith('3'): # Alfred 3 - return u'Alfred 3' + return 'Alfred 3' # Alfred 4+ - return u'com.runningwithcrayons.Alfred' + return 'com.runningwithcrayons.Alfred' def unicodify(s, encoding='utf-8', norm=None): @@ -110,8 +110,8 @@ def unicodify(s, encoding='utf-8', norm=None): unicode: Decoded, optionally normalised, Unicode string. """ - if not isinstance(s, unicode): - s = unicode(s, encoding) + if not isinstance(s, str): + s = str(s, encoding) if norm: from unicodedata import normalize @@ -120,30 +120,6 @@ def unicodify(s, encoding='utf-8', norm=None): return s -def utf8ify(s): - """Ensure string is a bytestring. - - .. versionadded:: 1.31 - - Returns `str` objects unchanced, encodes `unicode` objects to - UTF-8, and calls :func:`str` on anything else. - - Args: - s (object): A Python object - - Returns: - str: UTF-8 string or string representation of s. - - """ - if isinstance(s, str): - return s - - if isinstance(s, unicode): - return s.encode('utf-8') - - return str(s) - - def applescriptify(s): """Escape string for insertion into an AppleScript string. @@ -162,7 +138,7 @@ def applescriptify(s): unicode: Escaped string. """ - return s.replace(u'"', u'" & quote & "') + return s.replace('"', '" & quote & "') def run_command(cmd, **kwargs): @@ -181,7 +157,7 @@ def run_command(cmd, **kwargs): str: Output returned by :func:`~subprocess.check_output`. """ - cmd = [utf8ify(s) for s in cmd] + cmd = [str(s) for s in cmd] return subprocess.check_output(cmd, **kwargs) @@ -347,7 +323,7 @@ def search_in_alfred(query=None): query (unicode, optional): Search query. """ - query = query or u'' + query = query or '' appname = jxa_app_name() script = JXA_SEARCH.format(app=json.dumps(appname), arg=json.dumps(query)) run_applescript(script, lang='JavaScript') @@ -427,7 +403,7 @@ def appinfo(name): if not output: return None - path = output.split('\n')[0] + path = output.decode('utf-8').split('\n')[0] cmd = ['mdls', '-raw', '-name', 'kMDItemCFBundleIdentifier', path] bid = run_command(cmd).strip() @@ -447,7 +423,7 @@ def atomic_writer(fpath, mode): succeeds. The data is first written to a temporary file. :param fpath: path of file to write to. - :type fpath: ``unicode`` + :type fpath: ``str`` :param mode: sames as for :func:`open` :type mode: string diff --git a/workflow/version b/workflow/version index ebc91b48..227cea21 100644 --- a/workflow/version +++ b/workflow/version @@ -1 +1 @@ -1.40.0 \ No newline at end of file +2.0.0 diff --git a/workflow/web.py b/workflow/web.py index 83212a87..494dd255 100644 --- a/workflow/web.py +++ b/workflow/web.py @@ -9,7 +9,7 @@ """Lightweight HTTP library with a requests-like interface.""" -from __future__ import absolute_import, print_function + import codecs import json @@ -20,14 +20,14 @@ import socket import string import unicodedata -import urllib -import urllib2 -import urlparse +import urllib.request, urllib.parse, urllib.error +import urllib.request, urllib.error, urllib.parse +import urllib.parse import zlib __version__ = open(os.path.join(os.path.dirname(__file__), 'version')).read() -USER_AGENT = (u'Alfred-Workflow/' + __version__ + +USER_AGENT = ('Alfred-Workflow/' + __version__ + ' (+http://www.deanishe.net/alfred-workflow)') # Valid characters for multipart form data boundaries @@ -91,16 +91,16 @@ def str_dict(dic): dic2 = CaseInsensitiveDictionary() else: dic2 = {} - for k, v in dic.items(): - if isinstance(k, unicode): + for k, v in list(dic.items()): + if isinstance(k, str): k = k.encode('utf-8') - if isinstance(v, unicode): + if isinstance(v, str): v = v.encode('utf-8') dic2[k] = v return dic2 -class NoRedirectHandler(urllib2.HTTPRedirectHandler): +class NoRedirectHandler(urllib.request.HTTPRedirectHandler): """Prevent redirections.""" def redirect_request(self, *args): @@ -124,7 +124,7 @@ class CaseInsensitiveDictionary(dict): def __init__(self, initval=None): """Create new case-insensitive dictionary.""" if isinstance(initval, dict): - for key, value in initval.iteritems(): + for key, value in initval.items(): self.__setitem__(key, value) elif isinstance(initval, list): @@ -151,44 +151,29 @@ def get(self, key, default=None): def update(self, other): """Update values from other ``dict``.""" - for k, v in other.items(): + for k, v in list(other.items()): self[k] = v 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.values(self)] def keys(self): """Return original keys.""" - return [v['key'] for v in dict.itervalues(self)] + return [v['key'] for v in dict.values(self)] def values(self): """Return all values.""" - 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'] - - def iterkeys(self): - """Iterate over original keys.""" - for v in dict.itervalues(self): - yield v['key'] - - def itervalues(self): - """Interate over values.""" - for v in dict.itervalues(self): - yield v['val'] + return [v['val'] for v in dict.values(self)] -class Request(urllib2.Request): - """Subclass of :class:`urllib2.Request` that supports custom methods.""" +class Request(urllib.request.Request): + """Subclass of :class:`urllib.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) + urllib.request.Request.__init__(self, *args, **kwargs) def get_method(self): return self._method.upper() @@ -214,7 +199,7 @@ class Response(object): """ def __init__(self, request, stream=False): - """Call `request` with :mod:`urllib2` and process results. + """Call `request` with :mod:`urllib` and process results. :param request: :class:`Request` instance :param stream: Whether to stream response or retrieve it all at once @@ -236,8 +221,8 @@ def __init__(self, request, stream=False): # Execute query try: - self.raw = urllib2.urlopen(request) - except urllib2.HTTPError as err: + self.raw = urllib.request.urlopen(request) + except urllib.error.HTTPError as err: self.error = err try: self.url = err.geturl() @@ -256,9 +241,9 @@ def __init__(self, request, stream=False): # Parse additional info if request succeeded if not self.error: headers = self.raw.info() - self.transfer_encoding = headers.getencoding() - self.mimetype = headers.gettype() - for key in headers.keys(): + self.transfer_encoding = headers.get_content_charset() + self.mimetype = headers.get_content_type() + for key in list(headers.keys()): self.headers[key.lower()] = headers.get(key) # Is content gzipped? @@ -294,7 +279,7 @@ def json(self): :rtype: list, dict or unicode """ - return json.loads(self.content, self.encoding or 'utf-8') + return json.loads(self.content) @property def encoding(self): @@ -343,8 +328,9 @@ def text(self): """ if self.encoding: - return unicodedata.normalize('NFC', unicode(self.content, - self.encoding)) + return unicodedata.normalize( + 'NFC', str(self.content, self.encoding) + ) return self.content def iter_content(self, chunk_size=4096, decode_unicode=False): @@ -423,7 +409,7 @@ def save_to_path(self, filepath): def raise_for_status(self): """Raise stored error if one occurred. - error will be instance of :class:`urllib2.HTTPError` + error will be instance of :class:`urllib.HTTPError` """ if self.error is not None: raise self.error @@ -439,30 +425,30 @@ def _get_encoding(self): headers = self.raw.info() encoding = None - if headers.getparam('charset'): - encoding = headers.getparam('charset') + if headers.get_content_charset(): + encoding = headers.get_content_charset() # HTTP Content-Type header - for param in headers.getplist(): - if param.startswith('charset='): - encoding = param[8:] + for param, value in (headers.get_params() or []): + if param.startswith('charset'): + encoding = value 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"""""", + m = re.search(br"""""", self.content) if m: - encoding = m.group(1) + encoding = m.group(1).decode('utf8') elif ((self.mimetype.startswith('application/') or self.mimetype.startswith('text/')) and 'xml' in self.mimetype): - m = re.search(r"""]*\?>""", + m = re.search(br"""]*\?>""", self.content) if m: - encoding = m.group(1) + encoding = m.group(1).decode('utf8') # Format defaults if self.mimetype == 'application/json' and not encoding: @@ -528,21 +514,21 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, socket.setdefaulttimeout(timeout) # Default handlers - openers = [urllib2.ProxyHandler(urllib2.getproxies())] + openers = [urllib.request.ProxyHandler(urllib.request.getproxies())] if not allow_redirects: openers.append(NoRedirectHandler()) if auth is not None: # Add authorisation handler username, password = auth - password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() + password_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm() password_manager.add_password(None, url, username, password) - auth_manager = urllib2.HTTPBasicAuthHandler(password_manager) + auth_manager = urllib.request.HTTPBasicAuthHandler(password_manager) openers.append(auth_manager) # Install our custom chain of openers - opener = urllib2.build_opener(*openers) - urllib2.install_opener(opener) + opener = urllib.request.build_opener(*openers) + urllib.request.install_opener(opener) if not headers: headers = CaseInsensitiveDictionary() @@ -554,7 +540,8 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, # Accept gzip-encoded content encodings = [s.strip() for s in - headers.get('accept-encoding', '').split(',')] + headers.get('accept-encoding', '').split(',') + if s.strip()] if 'gzip' not in encodings: encodings.append('gzip') @@ -566,26 +553,26 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, new_headers, data = encode_multipart_formdata(data, files) headers.update(new_headers) elif data and isinstance(data, dict): - data = urllib.urlencode(str_dict(data)) + data = urllib.parse.urlencode(str_dict(data)) # Make sure everything is encoded text headers = str_dict(headers) - if isinstance(url, unicode): - url = url.encode('utf-8') + if isinstance(data, str): + data = data.encode('utf-8') if params: # GET args (POST args are handled in encode_multipart_formdata) - scheme, netloc, path, query, fragment = urlparse.urlsplit(url) + scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url) if query: # Combine query string and `params` - url_params = urlparse.parse_qs(query) + url_params = urllib.parse.parse_qs(query) # `params` take precedence over URL query string url_params.update(params) params = url_params - query = urllib.urlencode(str_dict(params), doseq=True) - url = urlparse.urlunsplit((scheme, netloc, path, query, fragment)) + query = urllib.parse.urlencode(str_dict(params), doseq=True) + url = urllib.parse.urlunsplit((scheme, netloc, path, query, fragment)) req = Request(url, data, headers, method=method) return Response(req, stream) @@ -673,48 +660,50 @@ def get_content_type(filename): """ 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 = b'-----' + b''.join( + random.choice(BOUNDARY_CHARS).encode('ascii') for i in range(30) + ) + CRLF = b'\r\n' output = [] # Normal form fields - for (name, value) in fields.items(): - if isinstance(name, unicode): + for (name, value) in list(fields.items()): + if isinstance(name, str): name = name.encode('utf-8') - if isinstance(value, unicode): + if isinstance(value, str): value = value.encode('utf-8') - output.append('--' + boundary) - output.append('Content-Disposition: form-data; name="%s"' % name) - output.append('') + output.append(b'--' + boundary) + output.append(b'Content-Disposition: form-data; name="%b"' % name) + output.append(b'') 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'] + for name, d in list(files.items()): + filename = d['filename'] + content = d['content'] + if 'mimetype' in d: + mimetype = d['mimetype'] else: mimetype = get_content_type(filename) - if isinstance(name, unicode): + if isinstance(name, str): name = name.encode('utf-8') - if isinstance(filename, unicode): + if isinstance(filename, str): filename = filename.encode('utf-8') - if isinstance(mimetype, unicode): + if isinstance(mimetype, str): 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('') + if isinstance(content, str): + content = content.encode('utf-8') + output.append(b'--' + boundary) + output.append(b'Content-Disposition: form-data; ' + b'name="%b"; filename="%b"' % (name, filename)) + output.append(b'Content-Type: %b' % mimetype) + output.append(b'') output.append(content) - output.append('--' + boundary + '--') - output.append('') + output.append(b'--' + boundary + b'--') + output.append(b'') 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.decode('ascii'), } return (headers, body) diff --git a/workflow/workflow.py b/workflow/workflow.py index 39352279..61587623 100644 --- a/workflow/workflow.py +++ b/workflow/workflow.py @@ -19,10 +19,10 @@ """ -from __future__ import print_function, unicode_literals + import binascii -import cPickle +import pickle from copy import deepcopy import json import logging @@ -44,8 +44,8 @@ import xml.etree.ElementTree as ET # imported to maintain API -from util import AcquisitionError # noqa: F401 -from util import ( +from .util import AcquisitionError # noqa: F401 +from .util import ( atomic_writer, LockFile, uninterruptible, @@ -92,7 +92,7 @@ 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_WARNING = os.path.join(ICON_ROOT, 'AlertCautionBadgeIcon.icns') ICON_WEB = os.path.join(ICON_ROOT, 'BookmarkIcon.icns') #################################################################### @@ -487,7 +487,7 @@ def isascii(text): """Test if ``text`` contains only ASCII characters. :param text: text to test for ASCII-ness - :type text: ``unicode`` + :type text: ``str`` :returns: ``True`` if ``text`` contains only ASCII characters :rtype: ``Boolean`` @@ -534,7 +534,7 @@ def register(self, name, serializer): ``name`` will be used as the file extension of the saved files. :param name: Name to register ``serializer`` under - :type name: ``unicode`` or ``str`` + :type name: ``bytes`` :param serializer: object with ``load()`` and ``dump()`` methods @@ -549,7 +549,7 @@ def serializer(self, name): """Return serializer object for ``name``. :param name: Name of serializer to return - :type name: ``unicode`` or ``str`` + :type name: ``str`` or ``bytes`` :returns: serializer object or ``None`` if no such serializer is registered. @@ -563,7 +563,7 @@ def unregister(self, name): serializer. :param name: Name of serializer to remove - :type name: ``unicode`` or ``str`` + :type name: ``str`` or ``bytes`` :returns: serializer object """ @@ -588,7 +588,7 @@ class JSONSerializer(object): .. versionadded:: 1.8 Use this serializer if you need readable data files. JSON doesn't - support Python objects as well as ``cPickle``/``pickle``, so be + support Python objects as well as ``pickle``, so be careful which data you try to serialize as JSON. """ @@ -619,46 +619,8 @@ def dump(cls, obj, file_obj): :type file_obj: ``file`` object """ - return json.dump(obj, file_obj, indent=2, encoding='utf-8') - - -class CPickleSerializer(object): - """Wrapper around :mod:`cPickle`. Sets ``protocol``. - - .. versionadded:: 1.8 - - This is the default serializer and the best combination of speed and - flexibility. - - """ - - @classmethod - def load(cls, file_obj): - """Load serialized object from open pickle file. - - .. versionadded:: 1.8 - - :param file_obj: file handle - :type file_obj: ``file`` object - :returns: object loaded from pickle file - :rtype: object - - """ - return cPickle.load(file_obj) - - @classmethod - def dump(cls, obj, file_obj): - """Serialize object ``obj`` to open pickle file. - - .. versionadded:: 1.8 - - :param obj: Python object to serialize - :type obj: Python object - :param file_obj: file handle - :type file_obj: ``file`` object - - """ - return cPickle.dump(obj, file_obj, protocol=-1) + encoded = json.dumps(obj).encode('utf8') + file_obj.write(encoded) class PickleSerializer(object): @@ -701,7 +663,7 @@ def dump(cls, obj, file_obj): # Set up default manager and register built-in serializers manager = SerializerManager() -manager.register('cpickle', CPickleSerializer) +manager.register('cpickle', PickleSerializer) manager.register('pickle', PickleSerializer) manager.register('json', JSONSerializer) @@ -807,7 +769,7 @@ class Settings(dict): (and settings file) will be initialised with ``defaults``. :param filepath: where to save the settings - :type filepath: :class:`unicode` + :type filepath: :class:`str` :param defaults: dict of default settings :type defaults: :class:`dict` @@ -826,7 +788,7 @@ def __init__(self, filepath, defaults=None): if os.path.exists(self._filepath): self._load() elif defaults: - for key, val in defaults.items(): + for key, val in list(defaults.items()): self[key] = val self.save() # save default settings @@ -858,9 +820,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, 'w') as fp: + json.dump(data, fp, sort_keys=True, indent=2) # dict methods def __setitem__(self, key, value): @@ -912,10 +873,10 @@ class Workflow(object): :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` + :type input_encoding: :class:`str` :param normalization: normalisation to apply to CLI args. See :meth:`Workflow.decode` for more details. - :type normalization: :class:`unicode` + :type normalization: :class:`str` :param capture_args: Capture and act on ``workflow:*`` arguments. See :ref:`Magic arguments ` for details. :type capture_args: :class:`Boolean` @@ -928,7 +889,7 @@ class Workflow(object): this URL will be displayed in the log and Alfred's debugger. It can also be opened directly in a web browser with the ``workflow:help`` :ref:`magic argument `. - :type help_url: :class:`unicode` or :class:`str` + :type help_url: :class:`str` or :class:`str` """ @@ -976,7 +937,7 @@ def __init__(self, default_settings=None, update_settings=None, #: what the user should enter (prefixed with :attr:`magic_prefix`) #: and the value is a callable that will be called when the argument #: is entered. If you would like to display a message in Alfred, the - #: function should return a ``unicode`` string. + #: function should return a ``str`` string. #: #: By default, the magic arguments documented #: :ref:`here ` are registered. @@ -996,7 +957,7 @@ def __init__(self, default_settings=None, update_settings=None, @property def alfred_version(self): """Alfred version as :class:`~workflow.update.Version` object.""" - from update import Version + from .update import Version return Version(self.alfred_env.get('version')) @property @@ -1093,14 +1054,14 @@ def bundleid(self): """Workflow bundle ID from environmental vars or ``info.plist``. :returns: bundle ID - :rtype: ``unicode`` + :rtype: ``str`` """ if not self._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 = self.info['bundleid'] return self._bundleid @@ -1119,7 +1080,7 @@ def name(self): """Workflow name from Alfred's environmental vars or ``info.plist``. :returns: workflow name - :rtype: ``unicode`` + :rtype: ``str`` """ if not self._name: @@ -1163,7 +1124,7 @@ def version(self): filepath = self.workflowfile('version') if os.path.exists(filepath): - with open(filepath, 'rb') as fileobj: + with open(filepath, 'r') as fileobj: version = fileobj.read() # info.plist @@ -1171,7 +1132,7 @@ def version(self): version = self.info.get('version') if version: - from update import Version + from .update import Version version = Version(version) self._version = version @@ -1299,7 +1260,7 @@ def workflowdir(self): # the library is in. CWD will be the workflow root if # a workflow is being run in Alfred candidates = [ - os.path.abspath(os.getcwdu()), + os.path.abspath(os.getcwd()), os.path.dirname(os.path.abspath(os.path.dirname(__file__)))] # climb the directory tree until we find `info.plist` @@ -1336,11 +1297,13 @@ def cachefile(self, filename): :attr:`cache directory `. :param filename: basename of file - :type filename: ``unicode`` + :type filename: ``str`` :returns: full path to file within cache directory - :rtype: ``unicode`` + :rtype: ``str`` """ + if isinstance(filename, bytes): + filename = filename.decode('utf8') return os.path.join(self.cachedir, filename) def datafile(self, filename): @@ -1350,22 +1313,26 @@ def datafile(self, filename): :attr:`data directory `. :param filename: basename of file - :type filename: ``unicode`` + :type filename: ``str`` :returns: full path to file within data directory - :rtype: ``unicode`` + :rtype: ``str`` """ + if isinstance(filename, bytes): + filename = filename.decode('utf8') return os.path.join(self.datadir, filename) def workflowfile(self, filename): """Return full path to ``filename`` in workflow's root directory. :param filename: basename of file - :type filename: ``unicode`` + :type filename: ``str`` :returns: full path to file within data directory - :rtype: ``unicode`` + :rtype: ``str`` """ + if isinstance(filename, bytes): + filename = filename.decode('utf8') return os.path.join(self.workflowdir, filename) @property @@ -1373,7 +1340,7 @@ def logfile(self): """Path to logfile. :returns: path to logfile within workflow's cache directory - :rtype: ``unicode`` + :rtype: ``str`` """ return self.cachefile('%s.log' % self.bundleid) @@ -1441,7 +1408,7 @@ def settings_path(self): """Path to settings file within workflow's data directory. :returns: path to ``settings.json`` file - :rtype: ``unicode`` + :rtype: ``str`` """ if not self._settings_path: @@ -1482,7 +1449,7 @@ def cache_serializer(self): See :class:`SerializerManager` for details. :returns: serializer name - :rtype: ``unicode`` + :rtype: ``str`` """ return self._cache_serializer @@ -1525,7 +1492,7 @@ def data_serializer(self): See :class:`SerializerManager` for details. :returns: serializer name - :rtype: ``unicode`` + :rtype: ``str`` """ return self._data_serializer @@ -1571,7 +1538,7 @@ def stored_data(self, name): self.logger.debug('no data stored for `%s`', name) return None - with open(metadata_path, 'rb') as file_obj: + with open(metadata_path) as file_obj: serializer_name = file_obj.read().strip() serializer = manager.serializer(serializer_name) @@ -1658,7 +1625,7 @@ def delete_paths(paths): @uninterruptible def _store(): # Save file extension - with atomic_writer(metadata_path, 'wb') as file_obj: + with atomic_writer(metadata_path, 'w') as file_obj: file_obj.write(serializer_name) with atomic_writer(data_path, 'wb') as file_obj: @@ -1750,7 +1717,7 @@ def cached_data_age(self, name): """Return age in seconds of cache `name` or 0 if cache doesn't exist. :param name: name of datastore - :type name: ``unicode`` + :type name: ``str`` :returns: age of datastore in seconds :rtype: ``int`` @@ -1774,11 +1741,11 @@ def filter(self, query, items, key=lambda x: x, ascending=False, all items will match. :param query: query to test items against - :type query: ``unicode`` + :type query: ``str`` :param items: iterable of items to test :type items: ``list`` or ``tuple`` :param key: function to get comparison key from ``items``. - Must return a ``unicode`` string. The default simply returns + Must return a ``str`` string. The default simply returns the item. :type key: ``callable`` :param ascending: set to ``True`` to get worst matches first @@ -2083,7 +2050,7 @@ def run(self, func, text_errors=False): if not sys.stdout.isatty(): # Show error in Alfred if text_errors: - print(unicode(err).encode('utf-8'), end='') + print(str(err), end='') else: self._items = [] if self._name: @@ -2093,7 +2060,7 @@ def run(self, func, text_errors=False): else: # pragma: no cover name = os.path.dirname(__file__) self.add_item("Error in workflow '%s'" % name, - unicode(err), + str(err), icon=ICON_ERROR) self.send_feedback() return 1 @@ -2113,24 +2080,24 @@ def add_item(self, title, subtitle='', modifier_subtitles=None, arg=None, """Add an item to be output to Alfred. :param title: Title shown in Alfred - :type title: ``unicode`` + :type title: ``str`` :param subtitle: Subtitle shown in Alfred - :type subtitle: ``unicode`` + :type subtitle: ``str`` :param modifier_subtitles: Subtitles shown when modifier (CMD, OPT etc.) is pressed. Use a ``dict`` with the lowercase keys ``cmd``, ``ctrl``, ``shift``, ``alt`` and ``fn`` :type modifier_subtitles: ``dict`` :param arg: Argument passed by Alfred as ``{query}`` when item is actioned - :type arg: ``unicode`` + :type arg: ``str`` :param autocomplete: Text expanded in Alfred when item is TABbed - :type autocomplete: ``unicode`` + :type autocomplete: ``str`` :param valid: Whether or not item can be actioned :type valid: ``Boolean`` :param uid: Used by Alfred to remember/sort items - :type uid: ``unicode`` + :type uid: ``str`` :param icon: Filename of icon to use - :type icon: ``unicode`` + :type icon: ``str`` :param icontype: Type of icon. Must be one of ``None`` , ``'filetype'`` or ``'fileicon'``. Use ``'filetype'`` when ``icon`` is a filetype such as ``'public.folder'``. Use ``'fileicon'`` when you wish to @@ -2138,20 +2105,20 @@ def add_item(self, title, subtitle='', modifier_subtitles=None, arg=None, ``icon='/Applications/Safari.app', icontype='fileicon'``. Leave as `None` if ``icon`` points to an actual icon file. - :type icontype: ``unicode`` + :type icontype: ``str`` :param type: Result type. Currently only ``'file'`` is supported (by Alfred). This will tell Alfred to enable file actions for this item. - :type type: ``unicode`` + :type type: ``str`` :param largetext: Text to be displayed in Alfred's large text box if user presses CMD+L on item. - :type largetext: ``unicode`` + :type largetext: ``str`` :param copytext: Text to be copied to pasteboard if user presses CMD+C on item. - :type copytext: ``unicode`` + :type copytext: ``str`` :param quicklookurl: URL to be displayed using Alfred's Quick Look feature (tapping ``SHIFT`` or ``⌘+Y`` on a result). - :type quicklookurl: ``unicode`` + :type quicklookurl: ``str`` :returns: :class:`Item` instance See :ref:`icons` for a list of the supported system icons. @@ -2178,9 +2145,8 @@ def send_feedback(self): 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.flush() + print('', file=sys.stdout) + print(ET.tostring(root, encoding='unicode'), file=sys.stdout) #################################################################### # Updating methods @@ -2217,7 +2183,7 @@ def last_version_run(self): version = self.settings.get('__workflow_last_version') if version: - from update import Version + from .update import Version version = Version(version) self._last_version_run = version @@ -2233,7 +2199,7 @@ def set_last_version(self, version=None): :param version: version to store (default is current version) :type version: :class:`~workflow.update.Version` instance - or ``unicode`` + or ``str`` :returns: ``True`` if version is saved, else ``False`` """ @@ -2245,8 +2211,8 @@ def set_last_version(self, version=None): version = self.version - if isinstance(version, basestring): - from update import Version + if isinstance(version, str): + from .update import Version version = Version(version) self.settings['__workflow_last_version'] = str(version) @@ -2324,13 +2290,12 @@ def check_update(self, force=False): # version = self._update_settings['version'] version = str(self.version) - from background import run_in_background + 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__), 'update.py') - cmd = ['/usr/bin/python', update_script, 'check', repo, version] + cmd = ['/usr/bin/python3', update_script, 'check', repo, version] if self.prereleases: cmd.append('--prereleases') @@ -2354,7 +2319,7 @@ def start_update(self): installed, else ``False`` """ - import update + from . import update repo = self._update_settings['github_slug'] # version = self._update_settings['version'] @@ -2363,13 +2328,12 @@ def start_update(self): if not update.check_update(repo, version, self.prereleases): return False - from background import run_in_background + 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__), 'update.py') - cmd = ['/usr/bin/python', update_script, 'install', repo, version] + cmd = ['/usr/bin/python3', update_script, 'install', repo, version] if self.prereleases: cmd.append('--prereleases') @@ -2394,12 +2358,12 @@ def save_password(self, account, password, service=None): :param account: name of the account the password is for, e.g. "Pinboard" - :type account: ``unicode`` + :type account: ``str`` :param password: the password to secure - :type password: ``unicode`` + :type password: ``str`` :param service: Name of the service. By default, this is the workflow's bundle ID - :type service: ``unicode`` + :type service: ``str`` """ if not service: @@ -2430,12 +2394,12 @@ def get_password(self, account, service=None): :param account: name of the account the password is for, e.g. "Pinboard" - :type account: ``unicode`` + :type account: ``str`` :param service: Name of the service. By default, this is the workflow's bundle ID - :type service: ``unicode`` + :type service: ``str`` :returns: account password - :rtype: ``unicode`` + :rtype: ``str`` """ if not service: @@ -2456,7 +2420,7 @@ def get_password(self, account, service=None): h = groups.get('hex') password = groups.get('pw') if h: - password = unicode(binascii.unhexlify(h), 'utf-8') + password = str(binascii.unhexlify(h), 'utf-8') self.logger.debug('got password : %s:%s', service, account) @@ -2469,10 +2433,10 @@ def delete_password(self, account, service=None): :param account: name of the account the password is for, e.g. "Pinboard" - :type account: ``unicode`` + :type account: ``str`` :param service: Name of the service. By default, this is the workflow's bundle ID - :type service: ``unicode`` + :type service: ``str`` """ if not service: @@ -2679,10 +2643,10 @@ def decode(self, text, encoding=None, normalization=None): Unicode string, it will only be normalised. :param encoding: The text encoding to use to decode ``text`` to Unicode. - :type encoding: ``unicode`` or ``None`` + :type encoding: ``str`` or ``None`` :param normalization: The nomalisation form to apply to ``text``. - :type normalization: ``unicode`` or ``None`` - :returns: decoded and normalised ``unicode`` + :type normalization: ``str`` or ``None`` + :returns: decoded and normalised ``str`` :class:`Workflow` uses "NFC" normalisation by default. This is the standard for Python and will work well with data from the web (via @@ -2697,8 +2661,8 @@ def decode(self, text, encoding=None, normalization=None): """ encoding = encoding or self._input_encoding normalization = normalization or self._normalizsation - if not isinstance(text, unicode): - text = unicode(text, encoding) + if not isinstance(text, str): + text = str(text, encoding) return unicodedata.normalize(normalization, text) def fold_to_ascii(self, text): @@ -2709,16 +2673,17 @@ def fold_to_ascii(self, text): .. note:: This only works for a subset of European languages. :param text: text to convert - :type text: ``unicode`` + :type text: ``str`` :returns: text containing only ASCII characters - :rtype: ``unicode`` + :rtype: ``str`` + >>> fold_to_ascii('Fußpilz') + 'Fusspilz' """ 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')) + return unicodedata.normalize('NFKD', text) def dumbify_punctuation(self, text): """Convert non-ASCII punctuation to closest ASCII equivalent. @@ -2730,9 +2695,9 @@ def dumbify_punctuation(self, text): .. versionadded: 1.9.7 :param text: text to convert - :type text: ``unicode`` + :type text: ``str`` :returns: text with only ASCII punctuation - :rtype: ``unicode`` + :rtype: ``str`` """ if isascii(text): @@ -2745,7 +2710,7 @@ def _delete_directory_contents(self, dirpath, filter_func): """Delete all files in a directory. :param dirpath: path to directory to clear - :type dirpath: ``unicode`` or ``str`` + :type dirpath: ``str`` or ``bytes`` :param filter_func function to determine whether a file shall be deleted or not. :type filter_func ``callable`` @@ -2765,16 +2730,18 @@ def _delete_directory_contents(self, dirpath, filter_func): 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')) + with open(self.workflowfile('info.plist'), 'rb') as plist_fp: + self._info = plistlib.load(plist_fp) + self._info_loaded = True def _create(self, dirpath): """Create directory `dirpath` if it doesn't exist. :param dirpath: path to directory - :type dirpath: ``unicode`` + :type dirpath: ``str`` :returns: ``dirpath`` argument - :rtype: ``unicode`` + :rtype: ``str`` """ if not os.path.exists(dirpath): @@ -2789,20 +2756,20 @@ def _call_security(self, action, service, account, *args): :param action: The ``security`` action to call, e.g. ``add-generic-password`` - :type action: ``unicode`` + :type action: ``str`` :param service: Name of the service. - :type service: ``unicode`` + :type service: ``str`` :param account: name of the account the password is for, e.g. "Pinboard" - :type account: ``unicode`` + :type account: ``str`` :param password: the password to secure - :type password: ``unicode`` + :type password: ``str`` :param *args: list of command line arguments to be passed to ``security`` :type *args: `list` or `tuple` :returns: ``(retcode, output)``. ``retcode`` is an `int`, ``output`` a - ``unicode`` string. - :rtype: `tuple` (`int`, ``unicode``) + ``str`` string. + :rtype: `tuple` (`int`, ``str``) """ cmd = ['security', action, '-s', service, '-a', account] + list(args) diff --git a/workflow/workflow3.py b/workflow/workflow3.py index 23a7aae1..7cde690a 100644 --- a/workflow/workflow3.py +++ b/workflow/workflow3.py @@ -23,7 +23,7 @@ """ -from __future__ import print_function, unicode_literals, absolute_import + import json import os @@ -76,7 +76,7 @@ def obj(self): o = {} if self: d2 = {} - for k, v in self.items(): + for k, v in list(self.items()): d2[k] = v o['variables'] = d2 @@ -88,7 +88,7 @@ def obj(self): return {'alfredworkflow': o} - def __unicode__(self): + def __str__(self): """Convert to ``alfredworkflow`` JSON object. Returns: @@ -97,21 +97,12 @@ def __unicode__(self): """ if not self and not self.config: if not self.arg: - return u'' - if isinstance(self.arg, unicode): + return '' + if isinstance(self.arg, str): return self.arg return json.dumps(self.obj) - def __str__(self): - """Convert to ``alfredworkflow`` JSON object. - - Returns: - str: UTF-8 encoded ``alfredworkflow`` JSON object - - """ - return unicode(self).encode('utf-8') - class Modifier(object): """Modify :class:`Item3` arg/icon/variables when modifier key is pressed. @@ -445,7 +436,7 @@ def _modifiers(self): """ if self.modifiers: mods = {} - for k, mod in self.modifiers.items(): + for k, mod in list(self.modifiers.items()): mods[k] = mod.obj return mods @@ -699,7 +690,7 @@ def obj(self): 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