From d4932a959819141d7bfc7fbee5eb6630420bf22d Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Tue, 19 Apr 2022 17:21:35 -0700 Subject: [PATCH 01/31] 2to3-3.8 -wn setup.py docs/conf.py tests/ workflow/ extras/ --- docs/conf.py | 24 +++--- extras/benchmark.py | 4 +- .../benchmarks/01-read-info-plist/script.py | 2 +- .../benchmarks/02-large-info-plist/script.py | 2 +- extras/benchmarks/03-read-envvars/script.py | 2 +- extras/gen_icon_table.py | 2 +- extras/generate_workflow_list.py | 4 +- tests/conftest.py | 8 +- tests/test_background.py | 2 +- tests/test_notify.py | 6 +- tests/test_update.py | 4 +- tests/test_update_versions.py | 2 +- tests/test_util.py | 38 ++++----- tests/test_util_atomic.py | 4 +- tests/test_util_lockfile.py | 2 +- tests/test_util_uninterruptible.py | 2 +- tests/test_web.py | 76 +++++++++--------- tests/test_web_http_encoding.py | 2 +- tests/test_workflow.py | 2 +- tests/test_workflow3.py | 38 ++++----- tests/test_workflow_encoding.py | 22 +++--- tests/test_workflow_env.py | 8 +- tests/test_workflow_files.py | 4 +- tests/test_workflow_filter.py | 2 +- tests/test_workflow_import.py | 2 +- tests/test_workflow_keychain.py | 2 +- tests/test_workflow_magic.py | 2 +- tests/test_workflow_magic_alfred2.py | 2 +- tests/test_workflow_run.py | 6 +- tests/test_workflow_serializers.py | 2 +- tests/test_workflow_settings.py | 4 +- tests/test_workflow_update.py | 4 +- tests/test_workflow_versions.py | 2 +- tests/test_workflow_xml.py | 4 +- tests/util.py | 14 ++-- workflow/background.py | 4 +- workflow/notify.py | 4 +- workflow/update.py | 10 +-- workflow/util.py | 16 ++-- workflow/web.py | 78 +++++++++---------- workflow/workflow.py | 46 +++++------ workflow/workflow3.py | 14 ++-- 42 files changed, 239 insertions(+), 239 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 43b97a36..cab491b7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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/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/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/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/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/tests/conftest.py b/tests/conftest.py index be978224..552eb770 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ """Common pytest fixtures.""" -from __future__ import print_function, absolute_import + from contextlib import contextmanager import os @@ -97,12 +97,12 @@ def env(**kwargs): """Context manager to alter and restore system environment.""" prev = os.environ.copy() - for k, v in kwargs.items(): + for k, v in list(kwargs.items()): if v is None: if k in os.environ: del os.environ[k] else: - if isinstance(v, unicode): + if isinstance(v, str): v = v.encode('utf-8') else: v = str(v) @@ -129,7 +129,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..6d93f82e 100644 --- a/tests/test_background.py +++ b/tests/test_background.py @@ -10,7 +10,7 @@ """Unit tests for :mod:`workflow.background`.""" -from __future__ import print_function, absolute_import + import os from time import sleep diff --git a/tests/test_notify.py b/tests/test_notify.py index acac296f..9168bfb7 100644 --- a/tests/test_notify.py +++ b/tests/test_notify.py @@ -10,7 +10,7 @@ """Unit tests for notifications.""" -from __future__ import print_function + import hashlib import logging @@ -24,8 +24,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, ) diff --git a/tests/test_update.py b/tests/test_update.py index f20da903..1b16d626 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -10,7 +10,7 @@ """Unit tests for update mechanism.""" -from __future__ import print_function + from contextlib import contextmanager import os @@ -19,7 +19,7 @@ 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 diff --git a/tests/test_update_versions.py b/tests/test_update_versions.py index 62deeab4..408e6b10 100644 --- a/tests/test_update_versions.py +++ b/tests/test_update_versions.py @@ -10,7 +10,7 @@ """Test `update.Version` class.""" -from __future__ import print_function + import unittest diff --git a/tests/test_util.py b/tests/test_util.py index f0cc290d..d6347939 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -9,7 +9,7 @@ """Unit tests for workflow/util.py.""" -from __future__ import print_function, absolute_import + import os import shutil @@ -59,24 +59,24 @@ 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) + assert isinstance(s, str) def test_utf8ify(): """UTF-8 encoding.""" data = [ # input, expected output - (u'Köln', 'Köln'), + ('Köln', 'Köln'), ('UTF-8', 'UTF-8'), (10, '10'), ([1, 2, 3], '[1, 2, 3]'), @@ -92,22 +92,22 @@ 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: @@ -396,12 +396,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 +413,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..bb46c04a 100644 --- a/tests/test_util_atomic.py +++ b/tests/test_util_atomic.py @@ -10,14 +10,14 @@ """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 diff --git a/tests/test_util_lockfile.py b/tests/test_util_lockfile.py index 8241bbd5..b02aa28b 100644 --- a/tests/test_util_lockfile.py +++ b/tests/test_util_lockfile.py @@ -10,7 +10,7 @@ """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..56b8a1a5 100644 --- a/tests/test_util_uninterruptible.py +++ b/tests/test_util_uninterruptible.py @@ -10,7 +10,7 @@ """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..599ff9e9 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -9,12 +9,12 @@ # """Unit tests for :mod:`workflow.web`""" -from __future__ import print_function, unicode_literals + import os import unittest -import urllib2 +import urllib.request, urllib.error, urllib.parse import json import shutil import socket @@ -86,13 +86,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,15 +104,15 @@ 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) @@ -138,8 +138,8 @@ def test_404(self): """Non-existant URL raises HTTPError w/ 404""" url = self.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""" @@ -151,66 +151,66 @@ def test_no_follow_redirect(self): """Redirects are not followed""" url = self.httpbin.url + '/redirect-to?url=' + self.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, self.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' 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' 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' 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' 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' 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' r = web.put(url) - self.assert_(r.status_code == 200) + self.assertTrue(r.status_code == 200) r.raise_for_status() def test_delete(self): @@ -218,7 +218,7 @@ def test_delete(self): url = self.httpbin + '/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): @@ -226,18 +226,18 @@ def test_delete_with_json(self): url = self.httpbin + '/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' 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) @@ -246,7 +246,7 @@ def test_encoding(self): url = self.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""" @@ -254,14 +254,14 @@ def test_no_encoding(self): url = self.httpbin.url + '/bytes/100' r = web.get(url) self.assertEqual(r.encoding, None) - self.assert_(isinstance(r.text, str)) + self.assertTrue(isinstance(r.text, str)) def test_html_encoding(self): """HTML is decoded""" url = self.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.""" @@ -271,7 +271,7 @@ def test_default_encoding(self): # 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.""" @@ -280,7 +280,7 @@ def test_xml_encoding(self): 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""" @@ -305,7 +305,7 @@ def test_auth_fails(self): url = self.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""" @@ -323,7 +323,7 @@ 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()) @@ -339,7 +339,7 @@ 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()) @@ -401,15 +401,15 @@ def test_params_added_to_url(self): fubar_path = os.path.join(DATA_DIR, 'fubar.txt') fubar_bytes = open(fubar_path).read() -fubar_unicode = unicode(fubar_bytes, 'utf-8') +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_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_unicode = str(utf8xml_bytes, 'utf-8') gifpath = os.path.join(DATA_DIR, 'cönfüsed.gif') gifbytes = open(gifpath).read() @@ -430,7 +430,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): diff --git a/tests/test_web_http_encoding.py b/tests/test_web_http_encoding.py index e052f461..66e2d9ea 100644 --- a/tests/test_web_http_encoding.py +++ b/tests/test_web_http_encoding.py @@ -10,7 +10,7 @@ """HTTP unit tests.""" -from __future__ import print_function + import os diff --git a/tests/test_workflow.py b/tests/test_workflow.py index 626fa667..8afe0b9f 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -10,7 +10,7 @@ """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..9ccf88b3 100644 --- a/tests/test_workflow3.py +++ b/tests/test_workflow3.py @@ -10,11 +10,11 @@ """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 +106,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 +120,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 +491,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 +503,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 +528,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 +547,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..a2c3de9c 100644 --- a/tests/test_workflow_encoding.py +++ b/tests/test_workflow_encoding.py @@ -6,7 +6,7 @@ """Unit tests for serializers.""" -from __future__ import print_function, unicode_literals + import pytest @@ -14,16 +14,16 @@ 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) + u = 'über.txt' + assert isinstance(wf.datadir, str) + assert isinstance(wf.datafile(s), str) + assert isinstance(wf.datafile(u), str) + assert isinstance(wf.cachedir, str) + assert isinstance(wf.cachefile(s), str) + assert isinstance(wf.cachefile(u), str) + assert isinstance(wf.workflowdir, str) + assert isinstance(wf.workflowfile(s), str) + assert isinstance(wf.workflowfile(u), str) if __name__ == '__main__': # pragma: no cover diff --git a/tests/test_workflow_env.py b/tests/test_workflow_env.py index 1e084386..bc51066f 100644 --- a/tests/test_workflow_env.py +++ b/tests/test_workflow_env.py @@ -6,7 +6,7 @@ """Unit tests for environment/info.plist.""" -from __future__ import print_function, unicode_literals + import logging import os @@ -41,13 +41,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..fb036a3c 100644 --- a/tests/test_workflow_files.py +++ b/tests/test_workflow_files.py @@ -6,7 +6,7 @@ """Unit tests for Workflow directory & file APIs.""" -from __future__ import print_function, unicode_literals + import json import os @@ -16,7 +16,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): diff --git a/tests/test_workflow_filter.py b/tests/test_workflow_filter.py index 593cce49..1dc64da7 100644 --- a/tests/test_workflow_filter.py +++ b/tests/test_workflow_filter.py @@ -6,7 +6,7 @@ """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..e0ef6901 100644 --- a/tests/test_workflow_import.py +++ b/tests/test_workflow_import.py @@ -6,7 +6,7 @@ """Unit tests for sys.path manipulation.""" -from __future__ import print_function, unicode_literals + import os import sys diff --git a/tests/test_workflow_keychain.py b/tests/test_workflow_keychain.py index dae2a201..f24d7097 100644 --- a/tests/test_workflow_keychain.py +++ b/tests/test_workflow_keychain.py @@ -6,7 +6,7 @@ """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..b53ad5c0 100644 --- a/tests/test_workflow_magic.py +++ b/tests/test_workflow_magic.py @@ -10,7 +10,7 @@ """Unit tests for magic arguments.""" -from __future__ import print_function + import os diff --git a/tests/test_workflow_magic_alfred2.py b/tests/test_workflow_magic_alfred2.py index d832a02b..28698538 100644 --- a/tests/test_workflow_magic_alfred2.py +++ b/tests/test_workflow_magic_alfred2.py @@ -6,7 +6,7 @@ """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..27cff40c 100644 --- a/tests/test_workflow_run.py +++ b/tests/test_workflow_run.py @@ -6,16 +6,16 @@ """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): diff --git a/tests/test_workflow_serializers.py b/tests/test_workflow_serializers.py index d78790d1..061f4b6e 100644 --- a/tests/test_workflow_serializers.py +++ b/tests/test_workflow_serializers.py @@ -10,7 +10,7 @@ """Unit tests for serializer classes.""" -from __future__ import print_function, absolute_import + import os diff --git a/tests/test_workflow_settings.py b/tests/test_workflow_settings.py index 2af8f2d1..c3d41f9a 100644 --- a/tests/test_workflow_settings.py +++ b/tests/test_workflow_settings.py @@ -10,7 +10,7 @@ """Unit tests for Workflow.settings API.""" -from __future__ import print_function, unicode_literals, absolute_import + import json import os @@ -86,7 +86,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..839cbadf 100644 --- a/tests/test_workflow_update.py +++ b/tests/test_workflow_update.py @@ -10,7 +10,7 @@ """Unit tests for Workflow's update API.""" -from __future__ import print_function + from contextlib import contextmanager @@ -28,7 +28,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 = { diff --git a/tests/test_workflow_versions.py b/tests/test_workflow_versions.py index e233c12f..dc551255 100644 --- a/tests/test_workflow_versions.py +++ b/tests/test_workflow_versions.py @@ -6,7 +6,7 @@ """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..245c534c 100644 --- a/tests/test_workflow_xml.py +++ b/tests/test_workflow_xml.py @@ -10,10 +10,10 @@ """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..8af49719 100644 --- a/tests/util.py +++ b/tests/util.py @@ -10,9 +10,9 @@ """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 +26,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 = { @@ -182,11 +182,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: 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 +226,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/workflow/background.py b/workflow/background.py index c2bd7352..b3afafae 100644 --- a/workflow/background.py +++ b/workflow/background.py @@ -17,7 +17,7 @@ and examples. """ -from __future__ import print_function, unicode_literals + import signal import sys @@ -25,7 +25,7 @@ import subprocess import pickle -from workflow import Workflow +from .workflow import Workflow __all__ = ['is_running', 'run_in_background'] diff --git a/workflow/notify.py b/workflow/notify.py index 28ec0b98..65362204 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 diff --git a/workflow/update.py b/workflow/update.py index c039f7ae..7392d8ab 100644 --- a/workflow/update.py +++ b/workflow/update.py @@ -21,7 +21,7 @@ """ -from __future__ import print_function, unicode_literals + from collections import defaultdict from functools import total_ordering @@ -31,8 +31,8 @@ import re import subprocess -import workflow -import web +from . import workflow +from . import web # __all__ = [] @@ -119,7 +119,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 +143,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 diff --git a/workflow/util.py b/workflow/util.py index ab5e9548..79a42378 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 @@ -138,7 +138,7 @@ def utf8ify(s): if isinstance(s, str): return s - if isinstance(s, unicode): + if isinstance(s, str): return s.encode('utf-8') return str(s) @@ -162,7 +162,7 @@ def applescriptify(s): unicode: Escaped string. """ - return s.replace(u'"', u'" & quote & "') + return s.replace('"', '" & quote & "') def run_command(cmd, **kwargs): @@ -347,7 +347,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') diff --git a/workflow/web.py b/workflow/web.py index 83212a87..5d9684c2 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,7 +151,7 @@ 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): @@ -182,13 +182,13 @@ def itervalues(self): yield v['val'] -class Request(urllib2.Request): +class Request(urllib.request.Request): """Subclass of :class:`urllib2.Request` that supports custom methods.""" def __init__(self, *args, **kwargs): """Create a new :class:`Request`.""" self._method = kwargs.pop('method', None) - urllib2.Request.__init__(self, *args, **kwargs) + urllib.request.Request.__init__(self, *args, **kwargs) def get_method(self): return self._method.upper() @@ -236,8 +236,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() @@ -258,7 +258,7 @@ def __init__(self, request, stream=False): headers = self.raw.info() self.transfer_encoding = headers.getencoding() self.mimetype = headers.gettype() - for key in headers.keys(): + for key in list(headers.keys()): self.headers[key.lower()] = headers.get(key) # Is content gzipped? @@ -343,7 +343,7 @@ def text(self): """ if self.encoding: - return unicodedata.normalize('NFC', unicode(self.content, + return unicodedata.normalize('NFC', str(self.content, self.encoding)) return self.content @@ -528,21 +528,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(urllib2.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() @@ -566,26 +566,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): + if isinstance(url, str): url = url.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) @@ -679,10 +679,10 @@ def get_content_type(filename): 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) @@ -690,18 +690,18 @@ def get_content_type(filename): 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; ' diff --git a/workflow/workflow.py b/workflow/workflow.py index 39352279..cb99c7fc 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, @@ -644,7 +644,7 @@ def load(cls, file_obj): :rtype: object """ - return cPickle.load(file_obj) + return pickle.load(file_obj) @classmethod def dump(cls, obj, file_obj): @@ -658,7 +658,7 @@ def dump(cls, obj, file_obj): :type file_obj: ``file`` object """ - return cPickle.dump(obj, file_obj, protocol=-1) + return pickle.dump(obj, file_obj, protocol=-1) class PickleSerializer(object): @@ -826,7 +826,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 @@ -996,7 +996,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 @@ -1100,7 +1100,7 @@ def bundleid(self): 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 = str(self.info['bundleid'], 'utf-8') return self._bundleid @@ -1171,7 +1171,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 +1299,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` @@ -2083,7 +2083,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).encode('utf-8'), end='') else: self._items = [] if self._name: @@ -2093,7 +2093,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 @@ -2217,7 +2217,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 @@ -2245,8 +2245,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,7 +2324,7 @@ 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__), @@ -2354,7 +2354,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,7 +2363,7 @@ 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__), @@ -2456,7 +2456,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) @@ -2697,8 +2697,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): @@ -2717,7 +2717,7 @@ def fold_to_ascii(self, text): if isascii(text): return text text = ''.join([ASCII_REPLACEMENTS.get(c, c) for c in text]) - return unicode(unicodedata.normalize('NFKD', + return str(unicodedata.normalize('NFKD', text).encode('ascii', 'ignore')) def dumbify_punctuation(self, text): diff --git a/workflow/workflow3.py b/workflow/workflow3.py index 23a7aae1..736fef35 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 @@ -97,8 +97,8 @@ 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) @@ -110,7 +110,7 @@ def __str__(self): str: UTF-8 encoded ``alfredworkflow`` JSON object """ - return unicode(self).encode('utf-8') + return str(self).encode('utf-8') class Modifier(object): @@ -445,7 +445,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 +699,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 From f31040da96a92d45696a83c2875b300653ddbed9 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Tue, 19 Apr 2022 17:47:42 -0700 Subject: [PATCH 02/31] Test on python3.8 instead of python2.7 --- requirements-test.txt | 2 +- tox.ini | 20 ++------------------ 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 4e5a9c6c..eca84221 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,4 +1,4 @@ -pyobjc-framework-Cocoa==5.3 +pyobjc-framework-Cocoa==8.5 pytest==4.6.10 pytest-cov==2.8.1 pytest-httpbin==1.0.0 diff --git a/tox.ini b/tox.ini index b5897f08..72d96348 100644 --- a/tox.ini +++ b/tox.ini @@ -13,32 +13,16 @@ addopts = --doctest-modules [tox] -envlist=py27 +envlist=py38 [testenv] usedevelop = true deps = - pytest - pytest_httpbin - pytest_cov - pytest_localserver + -r requirements-test.txt coverage commands = ./run-tests.sh -[testenv:py27] -deps = - {[testenv]deps} - pyobjc-core - pyobjc-framework-Cocoa - -; [testenv:py26] -; deps = -; {[testenv]deps} - -; [pydocstyle] -; add_ignore = D105,D203,D266,D400,D401,D413 - [flake8] builtins = unicode ignore = From da1a32f7ba57c7e5a8ef82f759d60fea14f6f879 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Tue, 19 Apr 2022 17:53:35 -0700 Subject: [PATCH 03/31] Remove dependency on pytest_httpbin --- requirements-test.txt | 1 - setup.py | 1 - tests/test_update.py | 1 - tests/test_web.py | 62 +++++++++++++++++++++---------------------- 4 files changed, 30 insertions(+), 35 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index eca84221..83f133f9 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -1,7 +1,6 @@ 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..a20fbce2 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,6 @@ def run_tests(self): 'coverage', 'pytest', 'pytest_cov', - 'pytest_httpbin', 'pytest_localserver', ] zip_safe = False diff --git a/tests/test_update.py b/tests/test_update.py index 1b16d626..d1de7e64 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -11,7 +11,6 @@ """Unit tests for update mechanism.""" - from contextlib import contextmanager import os import re diff --git a/tests/test_web.py b/tests/test_web.py index 599ff9e9..b41e5417 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -10,8 +10,6 @@ """Unit tests for :mod:`workflow.web`""" - - import os import unittest import urllib.request, urllib.error, urllib.parse @@ -24,7 +22,6 @@ from pprint import pprint import pytest -import pytest_httpbin import pytest_localserver # noqa: F401 from workflow import web @@ -32,6 +29,8 @@ DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') +HTTPBIN_URL = 'https://eu.httpbin.org' + class CaseInsensitiveDictTests(unittest.TestCase): """Unit tests for CaseInsensitiveDict""" @@ -116,7 +115,6 @@ def test_iterators(self): self.assertTrue(t in self.data_list) -@pytest_httpbin.use_class_based_httpbin class WebTests(unittest.TestCase): """Unit tests for workflow.web""" @@ -136,28 +134,28 @@ 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(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.assertNotEqual(r.url, self.httpbin.url) + 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.assertTrue(r.status_code == 200) r.raise_for_status() @@ -167,7 +165,7 @@ def test_post_form(self): 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.assertTrue(r.status_code == 200) @@ -179,14 +177,14 @@ def test_post_json(self): def test_post_without_data(self): """POST request without data""" - url = self.httpbin + '/post' + url = HTTPBIN_URL + '/post' r = web.post(url) 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.assertTrue(r.status_code == 200) r.raise_for_status() @@ -196,7 +194,7 @@ def test_put_form(self): 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.assertTrue(r.status_code == 200) @@ -208,14 +206,14 @@ def test_put_json(self): def test_put_without_data(self): """PUT request without data""" - url = self.httpbin + '/put' + url = HTTPBIN_URL + '/put' r = web.put(url) 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.assertTrue(r.status_code == 200) @@ -223,7 +221,7 @@ def test_delete(self): 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.assertTrue(r.status_code == 200) @@ -235,7 +233,7 @@ def test_delete_with_json(self): 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(urllib.error.URLError, web.get, url, timeout=1) else: @@ -243,7 +241,7 @@ def test_timeout(self): 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.assertTrue(isinstance(r.text, str)) @@ -251,21 +249,21 @@ def test_encoding(self): 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.assertTrue(isinstance(r.text, str)) 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.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 @@ -275,7 +273,7 @@ def test_default_encoding(self): 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() @@ -284,7 +282,7 @@ def test_xml_encoding(self): 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(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', @@ -329,7 +327,7 @@ def test_file_upload(self): 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() }} @@ -345,7 +343,7 @@ def test_file_upload_without_form_data(self): 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 +352,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 +360,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 +371,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'] From 85b4ecddf120224348d4f0884612b13e86bb2c58 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Tue, 19 Apr 2022 17:53:59 -0700 Subject: [PATCH 04/31] Tests fail, but at least they run --- tests/test_update.py | 2 +- tests/test_web.py | 10 +++++----- tests/test_workflow_import.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_update.py b/tests/test_update.py index d1de7e64..89c461b7 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -35,7 +35,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' diff --git a/tests/test_web.py b/tests/test_web.py index b41e5417..a30039bc 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -398,22 +398,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_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_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_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())) diff --git a/tests/test_workflow_import.py b/tests/test_workflow_import.py index e0ef6901..cc749e80 100644 --- a/tests/test_workflow_import.py +++ b/tests/test_workflow_import.py @@ -16,7 +16,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): From 1d0f70baea9f3dcedb87c1791f53bcada6252d00 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Tue, 19 Apr 2022 18:17:12 -0700 Subject: [PATCH 05/31] Fix test_background --- tests/test_background.py | 4 ++-- workflow/background.py | 19 ++++++++++--------- workflow/workflow.py | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/tests/test_background.py b/tests/test_background.py index 6d93f82e..7040cb45 100644 --- a/tests/test_background.py +++ b/tests/test_background.py @@ -27,8 +27,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/workflow/background.py b/workflow/background.py index b3afafae..5651d65e 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 @@ -24,6 +24,7 @@ import os import subprocess import pickle +from pathlib import Path from .workflow import Workflow @@ -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/workflow.py b/workflow/workflow.py index cb99c7fc..0848fc32 100644 --- a/workflow/workflow.py +++ b/workflow/workflow.py @@ -1100,7 +1100,7 @@ def bundleid(self): if self.alfred_env.get('workflow_bundleid'): self._bundleid = self.alfred_env.get('workflow_bundleid') else: - self._bundleid = str(self.info['bundleid'], 'utf-8') + self._bundleid = self.info['bundleid'] return self._bundleid From 3ca167c4672a89c80b3259996763ac1f409ddba1 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Tue, 19 Apr 2022 20:31:48 -0700 Subject: [PATCH 06/31] Fix test_notify --- tests/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/util.py b/tests/util.py index 8af49719..de639424 100644 --- a/tests/util.py +++ b/tests/util.py @@ -184,7 +184,7 @@ def __enter__(self): self.tempdir = tempfile.mkdtemp() 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, 0o700) From d9f9429cf0c16d69c4968cc7195bb9ff20b8ef60 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Tue, 19 Apr 2022 20:57:31 -0700 Subject: [PATCH 07/31] Fix test_update --- workflow/update.py | 10 ++++------ workflow/web.py | 28 ++++++++++++++-------------- workflow/workflow.py | 14 ++++++-------- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/workflow/update.py b/workflow/update.py index 7392d8ab..fea8e723 100644 --- a/workflow/update.py +++ b/workflow/update.py @@ -167,12 +167,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`.""" diff --git a/workflow/web.py b/workflow/web.py index 5d9684c2..7e0b8302 100644 --- a/workflow/web.py +++ b/workflow/web.py @@ -156,34 +156,34 @@ def update(self, other): def items(self): """Return ``(key, value)`` pairs.""" - return [(v['key'], v['val']) for v in dict.itervalues(self)] + return [(v['key'], v['val']) for v in dict.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)] + return [v['val'] for v in dict.values(self)] def iteritems(self): """Iterate over ``(key, value)`` pairs.""" - for v in dict.itervalues(self): + for v in dict.values(self): yield v['key'], v['val'] def iterkeys(self): """Iterate over original keys.""" - for v in dict.itervalues(self): + for v in dict.values(self): yield v['key'] def itervalues(self): """Interate over values.""" - for v in dict.itervalues(self): + for v in dict.values(self): yield v['val'] class Request(urllib.request.Request): - """Subclass of :class:`urllib2.Request` that supports custom methods.""" + """Subclass of :class:`urllib.Request` that supports custom methods.""" def __init__(self, *args, **kwargs): """Create a new :class:`Request`.""" @@ -214,7 +214,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 @@ -256,8 +256,8 @@ 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() + 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) @@ -423,7 +423,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 @@ -528,7 +528,7 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, socket.setdefaulttimeout(timeout) # Default handlers - openers = [urllib.request.ProxyHandler(urllib2.getproxies())] + openers = [urllib.request.ProxyHandler(urllib.request.getproxies())] if not allow_redirects: openers.append(NoRedirectHandler()) @@ -571,8 +571,8 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, # Make sure everything is encoded text headers = str_dict(headers) - if isinstance(url, str): - url = url.encode('utf-8') + if isinstance(url, bytes): + url = url.decode('utf-8') if params: # GET args (POST args are handled in encode_multipart_formdata) diff --git a/workflow/workflow.py b/workflow/workflow.py index 0848fc32..89c84840 100644 --- a/workflow/workflow.py +++ b/workflow/workflow.py @@ -619,7 +619,7 @@ def dump(cls, obj, file_obj): :type file_obj: ``file`` object """ - return json.dump(obj, file_obj, indent=2, encoding='utf-8') + return json.dump(obj, file_obj, indent=2) class CPickleSerializer(object): @@ -858,9 +858,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): @@ -2083,7 +2082,7 @@ def run(self, func, text_errors=False): if not sys.stdout.isatty(): # Show error in Alfred if text_errors: - print(str(err).encode('utf-8'), end='') + print(str(err), end='') else: self._items = [] if self._name: @@ -2178,9 +2177,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), file=sys.stdout) #################################################################### # Updating methods From bacb37b34a0a4cf38cb5b0b42c05340119ff2b09 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Tue, 19 Apr 2022 21:20:41 -0700 Subject: [PATCH 08/31] Fix test_update_versions --- tests/test_update_versions.py | 3 +-- workflow/update.py | 24 ++++++++++++++++++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/tests/test_update_versions.py b/tests/test_update_versions.py index 408e6b10..567ddabc 100644 --- a/tests/test_update_versions.py +++ b/tests/test_update_versions.py @@ -11,7 +11,6 @@ """Test `update.Version` class.""" - import unittest import pytest @@ -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/workflow/update.py b/workflow/update.py index fea8e723..63986e33 100644 --- a/workflow/update.py +++ b/workflow/update.py @@ -22,7 +22,6 @@ """ - from collections import defaultdict from functools import total_ordering import json @@ -274,7 +273,7 @@ def _parse_dotted_string(self, s): parsed = [] parts = s.split('.') for p in parts: - if p.isdigit(): + if all(c.isdigit() for c in p): p = int(p) parsed.append(p) return parsed @@ -297,8 +296,25 @@ 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) + + if lft and not rgt: + return False + elif rgt and not lft: + return True + else: + # 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 From 2b08d781892ec999ca6a8afdd452c07ad29d9058 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Tue, 19 Apr 2022 22:07:17 -0700 Subject: [PATCH 09/31] Fix test_util* --- tests/conftest.py | 34 +++++++++++++++++++--------------- tests/test_util.py | 19 +++++++++---------- tests/test_util_atomic.py | 8 ++++---- workflow/util.py | 2 +- 4 files changed, 33 insertions(+), 30 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 552eb770..e482ccfe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,7 @@ import os from shutil import rmtree from tempfile import mkdtemp +from unittest import mock import pytest @@ -96,21 +97,24 @@ @contextmanager def env(**kwargs): """Context manager to alter and restore system environment.""" - prev = os.environ.copy() - for k, v in list(kwargs.items()): - if v is None: - if k in os.environ: - del os.environ[k] - else: - if isinstance(v, str): - 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 diff --git a/tests/test_util.py b/tests/test_util.py index d6347939..c898c7b3 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -10,7 +10,6 @@ """Unit tests for workflow/util.py.""" - import os import shutil import subprocess @@ -111,7 +110,7 @@ def test_run_command(): ] 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 +121,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 +137,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 +150,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 +166,7 @@ def test_run_jxa(testfile): } """ out = run_jxa(script, 1) - assert out.strip() == '1' + assert out.strip() == b'1' def test_app_name(): diff --git a/tests/test_util_atomic.py b/tests/test_util_atomic.py index bb46c04a..f3f46d5d 100644 --- a/tests/test_util_atomic.py +++ b/tests/test_util_atomic.py @@ -30,7 +30,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 +56,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 +72,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/workflow/util.py b/workflow/util.py index 79a42378..b56da4d5 100644 --- a/workflow/util.py +++ b/workflow/util.py @@ -427,7 +427,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() From 3ed460e8e1f695082784250371b7f03703d9a833 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 00:17:10 -0700 Subject: [PATCH 10/31] Fix test_web* --- tests/test_web.py | 4 +-- tests/test_web_http_encoding.py | 2 +- workflow/web.py | 63 ++++++++++++++++++--------------- 3 files changed, 38 insertions(+), 31 deletions(-) diff --git a/tests/test_web.py b/tests/test_web.py index a30039bc..b95c53b3 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -252,7 +252,7 @@ def test_no_encoding(self): url = HTTPBIN_URL + '/bytes/100' r = web.get(url) self.assertEqual(r.encoding, None) - self.assertTrue(isinstance(r.text, str)) + self.assertTrue(isinstance(r.text, bytes)) def test_html_encoding(self): """HTML is decoded""" @@ -444,7 +444,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 66e2d9ea..de69ef4e 100644 --- a/tests/test_web_http_encoding.py +++ b/tests/test_web_http_encoding.py @@ -55,7 +55,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/workflow/web.py b/workflow/web.py index 7e0b8302..b8a5cf52 100644 --- a/workflow/web.py +++ b/workflow/web.py @@ -294,7 +294,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 +343,9 @@ def text(self): """ if self.encoding: - return unicodedata.normalize('NFC', str(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): @@ -439,30 +440,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: @@ -554,7 +555,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') @@ -571,6 +573,9 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, # Make sure everything is encoded text headers = str_dict(headers) + if isinstance(data, str): + data = data.encode('utf-8') + if isinstance(url, bytes): url = url.decode('utf-8') @@ -673,9 +678,10 @@ 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 @@ -684,9 +690,9 @@ def get_content_type(filename): name = name.encode('utf-8') 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 @@ -703,18 +709,19 @@ def get_content_type(filename): filename = filename.encode('utf-8') 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) From d936a3856899e42c2a067e682e4262850603ea40 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 00:23:25 -0700 Subject: [PATCH 11/31] Fix test_workflow, test_workflow3 --- workflow/workflow.py | 2 +- workflow/workflow3.py | 11 +---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/workflow/workflow.py b/workflow/workflow.py index 89c84840..93b59bb9 100644 --- a/workflow/workflow.py +++ b/workflow/workflow.py @@ -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') #################################################################### diff --git a/workflow/workflow3.py b/workflow/workflow3.py index 736fef35..7cde690a 100644 --- a/workflow/workflow3.py +++ b/workflow/workflow3.py @@ -88,7 +88,7 @@ def obj(self): return {'alfredworkflow': o} - def __unicode__(self): + def __str__(self): """Convert to ``alfredworkflow`` JSON object. Returns: @@ -103,15 +103,6 @@ def __unicode__(self): return json.dumps(self.obj) - def __str__(self): - """Convert to ``alfredworkflow`` JSON object. - - Returns: - str: UTF-8 encoded ``alfredworkflow`` JSON object - - """ - return str(self).encode('utf-8') - class Modifier(object): """Modify :class:`Item3` arg/icon/variables when modifier key is pressed. From ad02b377a920edb92eb4a1fd2e90205a9f4f9f62 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 00:28:57 -0700 Subject: [PATCH 12/31] Fix test_workflow_encoding --- tests/test_workflow_encoding.py | 10 +++++----- workflow/workflow.py | 6 ++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/test_workflow_encoding.py b/tests/test_workflow_encoding.py index a2c3de9c..88371bc6 100644 --- a/tests/test_workflow_encoding.py +++ b/tests/test_workflow_encoding.py @@ -13,17 +13,17 @@ def test_unicode_paths(wf): """Workflow paths are Unicode""" - s = b'test.txt' - u = 'über.txt' + 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.datafile(u), str) assert isinstance(wf.cachedir, str) + assert isinstance(wf.cachefile(b), str) assert isinstance(wf.cachefile(s), str) - assert isinstance(wf.cachefile(u), str) assert isinstance(wf.workflowdir, str) + assert isinstance(wf.workflowfile(b), str) assert isinstance(wf.workflowfile(s), str) - assert isinstance(wf.workflowfile(u), str) if __name__ == '__main__': # pragma: no cover diff --git a/workflow/workflow.py b/workflow/workflow.py index 93b59bb9..e0c4c37a 100644 --- a/workflow/workflow.py +++ b/workflow/workflow.py @@ -1340,6 +1340,8 @@ def cachefile(self, filename): :rtype: ``unicode`` """ + if isinstance(filename, bytes): + filename = filename.decode('utf8') return os.path.join(self.cachedir, filename) def datafile(self, filename): @@ -1354,6 +1356,8 @@ def datafile(self, filename): :rtype: ``unicode`` """ + if isinstance(filename, bytes): + filename = filename.decode('utf8') return os.path.join(self.datadir, filename) def workflowfile(self, filename): @@ -1365,6 +1369,8 @@ def workflowfile(self, filename): :rtype: ``unicode`` """ + if isinstance(filename, bytes): + filename = filename.decode('utf8') return os.path.join(self.workflowdir, filename) @property From de407b7a9cbbb9d36da225b38ec6b5b9badf68b2 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 00:43:31 -0700 Subject: [PATCH 13/31] Fix test_workflow_files --- tests/test_workflow_files.py | 4 +-- workflow/workflow.py | 52 +++++------------------------------- 2 files changed, 9 insertions(+), 47 deletions(-) diff --git a/tests/test_workflow_files.py b/tests/test_workflow_files.py index fb036a3c..b4834e1c 100644 --- a/tests/test_workflow_files.py +++ b/tests/test_workflow_files.py @@ -226,7 +226,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 +305,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/workflow/workflow.py b/workflow/workflow.py index e0c4c37a..ab87a157 100644 --- a/workflow/workflow.py +++ b/workflow/workflow.py @@ -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: ``str`` :param serializer: object with ``load()`` and ``dump()`` methods @@ -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) - - -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 pickle.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 pickle.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) @@ -1576,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) @@ -1663,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: From 47dd2cb818e7ee10a399e8cf27e4bab7d74b9e5f Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 00:49:31 -0700 Subject: [PATCH 14/31] str -> bytes, unicode -> str --- docs/guide/magic-arguments.rst | 2 +- docs/guide/text-encoding.rst | 4 +- workflow/background.py | 8 +-- workflow/util.py | 2 +- workflow/workflow.py | 120 ++++++++++++++++----------------- 5 files changed, 68 insertions(+), 68 deletions(-) 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 ` for details. :type capture_args: :class:`Boolean` @@ -889,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` """ @@ -937,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. @@ -1054,7 +1054,7 @@ def bundleid(self): """Workflow bundle ID from environmental vars or ``info.plist``. :returns: bundle ID - :rtype: ``unicode`` + :rtype: ``str`` """ if not self._bundleid: @@ -1080,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: @@ -1297,9 +1297,9 @@ 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): @@ -1313,9 +1313,9 @@ 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): @@ -1326,9 +1326,9 @@ 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): @@ -1340,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) @@ -1408,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: @@ -1449,7 +1449,7 @@ def cache_serializer(self): See :class:`SerializerManager` for details. :returns: serializer name - :rtype: ``unicode`` + :rtype: ``str`` """ return self._cache_serializer @@ -1492,7 +1492,7 @@ def data_serializer(self): See :class:`SerializerManager` for details. :returns: serializer name - :rtype: ``unicode`` + :rtype: ``str`` """ return self._data_serializer @@ -1717,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`` @@ -1741,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 @@ -2080,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 @@ -2105,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. @@ -2199,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`` """ @@ -2360,12 +2360,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: @@ -2396,12 +2396,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: @@ -2435,10 +2435,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: @@ -2645,10 +2645,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 @@ -2675,9 +2675,9 @@ 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`` """ if isascii(text): @@ -2696,9 +2696,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): @@ -2711,7 +2711,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`` @@ -2738,9 +2738,9 @@ 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): @@ -2755,20 +2755,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) From 9528288af5824888e2a55ac855146ea0f3ea40ec Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 01:03:09 -0700 Subject: [PATCH 15/31] Fix test_workflow_filter --- workflow/workflow.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/workflow/workflow.py b/workflow/workflow.py index 332ac2ac..b6e2488d 100644 --- a/workflow/workflow.py +++ b/workflow/workflow.py @@ -2679,12 +2679,13 @@ def fold_to_ascii(self, text): :returns: text containing only ASCII characters :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 str(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. From 233dd8528c110851bbbd6609ecd8b13a8ac99990 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 01:10:36 -0700 Subject: [PATCH 16/31] Fix test_workflow_magic --- tests/test_workflow_magic.py | 6 +++--- tests/util.py | 2 +- workflow/update.py | 2 +- workflow/workflow.py | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_workflow_magic.py b/tests/test_workflow_magic.py index b53ad5c0..4f514dd3 100644 --- a/tests/test_workflow_magic.py +++ b/tests/test_workflow_magic.py @@ -143,7 +143,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 +158,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 +178,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/util.py b/tests/util.py index de639424..ab59cb12 100644 --- a/tests/util.py +++ b/tests/util.py @@ -156,7 +156,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) diff --git a/workflow/update.py b/workflow/update.py index 63986e33..083d3fe1 100644 --- a/workflow/update.py +++ b/workflow/update.py @@ -225,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)) diff --git a/workflow/workflow.py b/workflow/workflow.py index b6e2488d..3d510035 100644 --- a/workflow/workflow.py +++ b/workflow/workflow.py @@ -1124,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 From cc37aaa6ac3cfb72fa51ae607d1fea238104a26c Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 01:11:19 -0700 Subject: [PATCH 17/31] Fix test_workflow_run --- tests/test_workflow_run.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_workflow_run.py b/tests/test_workflow_run.py index 27cff40c..8e376574 100644 --- a/tests/test_workflow_run.py +++ b/tests/test_workflow_run.py @@ -93,7 +93,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): From e0f99eabf1b363c435d57fb0dddc242bf77ab636 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 01:12:34 -0700 Subject: [PATCH 18/31] Fix test_workflow_serializers --- docs/api/workflow.rst.inc | 3 --- docs/guide/serialization.rst | 7 ++----- tests/test_workflow_serializers.py | 3 +-- 3 files changed, 3 insertions(+), 10 deletions(-) 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/guide/serialization.rst b/docs/guide/serialization.rst index 97dd24be..77b17838 100644 --- a/docs/guide/serialization.rst +++ b/docs/guide/serialization.rst @@ -66,11 +66,8 @@ Built-in serializers There are 3 built-in, pre-configured serializers: -- :class:`cpickle ` — 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/tests/test_workflow_serializers.py b/tests/test_workflow_serializers.py index 061f4b6e..868721a3 100644 --- a/tests/test_workflow_serializers.py +++ b/tests/test_workflow_serializers.py @@ -19,7 +19,6 @@ from workflow.workflow import ( SerializerManager, JSONSerializer, - CPickleSerializer, PickleSerializer, manager as default_manager, ) @@ -33,7 +32,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 From c06c2293c1148a02932455587628d4a86c6f4344 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 01:13:23 -0700 Subject: [PATCH 19/31] Fix test_workflow_settings --- tests/test_workflow_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_workflow_settings.py b/tests/test_workflow_settings.py index c3d41f9a..9721129f 100644 --- a/tests/test_workflow_settings.py +++ b/tests/test_workflow_settings.py @@ -31,7 +31,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): From f5947c85734d95d148b5c9c095bec52f6d613602 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 01:46:58 -0700 Subject: [PATCH 20/31] Fix test_workflow_update --- tests/test_update.py | 12 ++++++------ tests/test_workflow_update.py | 12 ++++-------- workflow/update.py | 2 +- workflow/workflow.py | 10 ++++------ 4 files changed, 15 insertions(+), 21 deletions(-) diff --git a/tests/test_update.py b/tests/test_update.py index 89c461b7..3411fcfe 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -12,6 +12,7 @@ from contextlib import contextmanager +from unittest import mock import os import re @@ -81,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_workflow_update.py b/tests/test_workflow_update.py index 839cbadf..7a64d5c6 100644 --- a/tests/test_workflow_update.py +++ b/tests/test_workflow_update.py @@ -91,15 +91,13 @@ 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 +114,7 @@ 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 +161,7 @@ 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/workflow/update.py b/workflow/update.py index 083d3fe1..2410b920 100644 --- a/workflow/update.py +++ b/workflow/update.py @@ -273,7 +273,7 @@ def _parse_dotted_string(self, s): parsed = [] parts = s.split('.') for p in parts: - if all(c.isdigit() for c in p): + if p and all(c.isdigit() for c in p): p = int(p) parsed.append(p) return parsed diff --git a/workflow/workflow.py b/workflow/workflow.py index 3d510035..f6b94890 100644 --- a/workflow/workflow.py +++ b/workflow/workflow.py @@ -2293,10 +2293,9 @@ def check_update(self, force=False): 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') @@ -2332,10 +2331,9 @@ def start_update(self): from .background import run_in_background # update.py is adjacent to this file - update_script = os.path.join(os.path.dirname(__file__), - b'update.py') + update_script = os.path.join(os.path.dirname(__file__), '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') From c3237731d59994d49d7b10bea803b54463bc5153 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 01:48:58 -0700 Subject: [PATCH 21/31] Fix test_workflow_xml --- workflow/workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workflow/workflow.py b/workflow/workflow.py index f6b94890..1c10b559 100644 --- a/workflow/workflow.py +++ b/workflow/workflow.py @@ -2146,7 +2146,7 @@ def send_feedback(self): for item in self._items: root.append(item.elem) print('', file=sys.stdout) - print(ET.tostring(root), file=sys.stdout) + print(ET.tostring(root, encoding='unicode'), file=sys.stdout) #################################################################### # Updating methods From 0b4bf33f08f79e20860a541896bfc75922b407ce Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 02:28:13 -0700 Subject: [PATCH 22/31] Fix gaps in coverage --- tests/test_util.py | 1 + tests/test_web.py | 20 +++++++++++++++----- workflow/update.py | 11 +++-------- workflow/web.py | 18 ------------------ 4 files changed, 19 insertions(+), 31 deletions(-) diff --git a/tests/test_util.py b/tests/test_util.py index c898c7b3..16ac7c55 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -79,6 +79,7 @@ def test_utf8ify(): ('UTF-8', 'UTF-8'), (10, '10'), ([1, 2, 3], '[1, 2, 3]'), + (b'foo', 'foo'), ] for s, x in data: diff --git a/tests/test_web.py b/tests/test_web.py index b95c53b3..fc5a171b 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -10,23 +10,23 @@ """Unit tests for :mod:`workflow.web`""" -import os -import unittest -import urllib.request, urllib.error, urllib.parse 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_localserver # noqa: F401 - from workflow import web - DATA_DIR = os.path.join(os.path.dirname(__file__), 'data') HTTPBIN_URL = 'https://eu.httpbin.org' @@ -341,6 +341,16 @@ def test_file_upload_without_form_data(self): 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' + files = {'file': {'filename': 'cönfüsed.txt', + 'content': 'Hére ïs søme ÜÑÎÇÒDÈ™' + }} + r = web.post(url, files=files) + self.assertEqual(r.status_code, 200) + data = r.json() + def test_json_encoding(self): """JSON decoded correctly""" url = HTTPBIN_URL + '/get' diff --git a/workflow/update.py b/workflow/update.py index 2410b920..14cbc3b0 100644 --- a/workflow/update.py +++ b/workflow/update.py @@ -306,14 +306,9 @@ def __lt__(self, other): lft.pop(0) rgt.pop(0) - if lft and not rgt: - return False - elif rgt and not lft: - return True - else: - # Alphanumeric versions are earlier than numeric versions, - # therefore lft < rgt if the right version is numeric. - return isinstance(rgt[0], int) + # 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/web.py b/workflow/web.py index b8a5cf52..494dd255 100644 --- a/workflow/web.py +++ b/workflow/web.py @@ -166,21 +166,6 @@ def values(self): """Return all values.""" return [v['val'] for v in dict.values(self)] - def iteritems(self): - """Iterate over ``(key, value)`` pairs.""" - for v in dict.values(self): - yield v['key'], v['val'] - - def iterkeys(self): - """Iterate over original keys.""" - for v in dict.values(self): - yield v['key'] - - def itervalues(self): - """Interate over values.""" - for v in dict.values(self): - yield v['val'] - class Request(urllib.request.Request): """Subclass of :class:`urllib.Request` that supports custom methods.""" @@ -576,9 +561,6 @@ def request(method, url, params=None, data=None, headers=None, cookies=None, if isinstance(data, str): data = data.encode('utf-8') - if isinstance(url, bytes): - url = url.decode('utf-8') - if params: # GET args (POST args are handled in encode_multipart_formdata) scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url) From 4443fc0fe522cd4394a0a49b09639fbea00de492 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 02:28:28 -0700 Subject: [PATCH 23/31] Remove utf8ify (unnecessary) --- docs/api/util.rst.inc | 2 -- tests/test_util.py | 18 ------------------ workflow/util.py | 26 +------------------------- 3 files changed, 1 insertion(+), 45 deletions(-) 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/tests/test_util.py b/tests/test_util.py index 16ac7c55..80457c4e 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -34,7 +34,6 @@ set_theme, unicodify, unset_config, - utf8ify, ) from .util import MockCall @@ -71,23 +70,6 @@ def test_unicodify(): assert isinstance(s, str) -def test_utf8ify(): - """UTF-8 encoding.""" - data = [ - # input, expected output - ('Köln', 'Köln'), - ('UTF-8', 'UTF-8'), - (10, '10'), - ([1, 2, 3], '[1, 2, 3]'), - (b'foo', 'foo'), - ] - - for s, x in data: - r = utf8ify(s) - assert x == r - assert isinstance(x, str) - - def test_applescript_escape(): """Escape AppleScript strings.""" data = [ diff --git a/workflow/util.py b/workflow/util.py index 664aa05e..154b9881 100644 --- a/workflow/util.py +++ b/workflow/util.py @@ -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, str): - return s.encode('utf-8') - - return str(s) - - def applescriptify(s): """Escape string for insertion into an AppleScript string. @@ -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) From 67ca1d0f2fa52b1905d794433911b99fe0ef9b3f Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 02:35:01 -0700 Subject: [PATCH 24/31] /usr/bin/python -> /usr/bin/python3 --- README_PYPI.rst | 2 +- bin/publish-cheeseshop.sh | 2 +- docs/guide/background.rst | 2 +- docs/guide/setup.rst | 8 ++++---- docs/guide/variables.rst | 2 +- docs/index.rst | 2 +- docs/supported-versions.rst | 8 ++++---- docs/tutorial_1.rst | 4 ++-- docs/tutorial_2.rst | 4 ++-- extras/benchmarks/00-python-interpreter-only/run.sh | 2 +- extras/benchmarks/01-read-info-plist/run.sh | 2 +- extras/benchmarks/02-large-info-plist/run.sh | 2 +- extras/benchmarks/03-read-envvars/run.sh | 2 +- 13 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README_PYPI.rst b/README_PYPI.rst index 4d1ef993..e9f507be 100644 --- a/README_PYPI.rst +++ b/README_PYPI.rst @@ -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/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/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/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..779149b1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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..32fe57c8 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``), which is 2.6 on 10.6/Snow Leopard and 2.7 on later versions. .. important:: @@ -79,7 +79,7 @@ Here is the `full list of new features in Python 2.7`_, but the most important t - 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. +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/python32.6`` in addition to ``/usr/bin/python3`` (2.7) to make sure they will run on Snow Leopard. Why no Python 3 support? 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/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/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/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 From ae5a1b519b1b3621e926e92000d376f23ccef4d3 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 03:01:31 -0700 Subject: [PATCH 25/31] Run tests on python3.{7,8,9} --- setup.py | 4 +++- tests/test_notify.py | 6 +++++- tox.ini | 2 +- workflow/notify.py | 7 +++++-- workflow/workflow.py | 4 +++- 5 files changed, 17 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index a20fbce2..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', ] diff --git a/tests/test_notify.py b/tests/test_notify.py index 9168bfb7..99b20cb5 100644 --- a/tests/test_notify.py +++ b/tests/test_notify.py @@ -80,11 +80,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/tox.ini b/tox.ini index 72d96348..9b697428 100644 --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ addopts = --doctest-modules [tox] -envlist=py38 +envlist=py37,py38,py39 [testenv] usedevelop = true diff --git a/workflow/notify.py b/workflow/notify.py index 65362204..2759eb43 100644 --- a/workflow/notify.py +++ b/workflow/notify.py @@ -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/workflow.py b/workflow/workflow.py index 1c10b559..61587623 100644 --- a/workflow/workflow.py +++ b/workflow/workflow.py @@ -2730,7 +2730,9 @@ 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): From 71312cdbfa20906012c0cc78250072c3ef35442f Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 03:02:42 -0700 Subject: [PATCH 26/31] Update CI/CD to use Python 3 (untested) --- .github/workflows/continuous-integration.yml | 2 +- .travis.yml | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 52f7e7e8..975a5af2 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: - python-version: 2.7 + python-version: 3.8 - 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 From 13aceb5f1fce51eeb7204b396a75a67d92383ab9 Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 08:38:44 -0700 Subject: [PATCH 27/31] Start updating docs to note switch to Python 3 --- README.md | 2 +- README_PYPI.rst | 2 +- docs/api/web.rst.inc | 21 --------------------- docs/conf.py | 2 +- docs/index.rst | 2 +- docs/supported-versions.rst | 35 +---------------------------------- tests/README.md | 2 +- 7 files changed, 6 insertions(+), 60 deletions(-) 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 e9f507be..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. 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/conf.py b/docs/conf.py index cab491b7..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'] diff --git a/docs/index.rst b/docs/index.rst index 779149b1..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 diff --git a/docs/supported-versions.rst b/docs/supported-versions.rst index 32fe57c8..0646135c 100644 --- a/docs/supported-versions.rst +++ b/docs/supported-versions.rst @@ -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/python3``), 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/python32.6`` in addition to ``/usr/bin/python3`` (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/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 From ba2a8abc3714b7b0b3a1fef676e6ae2adfb2648a Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Wed, 20 Apr 2022 08:37:51 -0700 Subject: [PATCH 28/31] 2.0.0 --- workflow/version | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From a1935ec4b0845d7eebf99698e4351ee062d2438a Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Thu, 21 Apr 2022 10:09:01 -0700 Subject: [PATCH 29/31] Fix linter errors, add linter to tox --- .github/workflows/continuous-integration.yml | 6 +++++- tests/conftest.py | 1 - tests/test_background.py | 1 - tests/test_notify.py | 1 - tests/test_util_atomic.py | 1 - tests/test_util_lockfile.py | 1 - tests/test_util_uninterruptible.py | 1 - tests/test_web.py | 5 ++++- tests/test_web_http_encoding.py | 1 - tests/test_workflow.py | 1 - tests/test_workflow3.py | 1 - tests/test_workflow_encoding.py | 1 - tests/test_workflow_env.py | 1 - tests/test_workflow_files.py | 1 - tests/test_workflow_filter.py | 1 - tests/test_workflow_import.py | 1 - tests/test_workflow_keychain.py | 1 - tests/test_workflow_magic.py | 1 - tests/test_workflow_magic_alfred2.py | 1 - tests/test_workflow_run.py | 1 - tests/test_workflow_serializers.py | 1 - tests/test_workflow_settings.py | 1 - tests/test_workflow_update.py | 21 +++++++++++++++----- tests/test_workflow_versions.py | 1 - tests/test_workflow_xml.py | 1 - tests/util.py | 1 - tox.ini | 9 ++++++++- 27 files changed, 33 insertions(+), 31 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 975a5af2..7c6e89f4 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -10,6 +10,10 @@ jobs: run: name: "tests & coverage" runs-on: macos-latest + strategy: + matrix: + python-version: [3.8] + env: COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} CODACY_PROJECT_TOKEN: ${{ secrets.CODACY_PROJECT_TOKEN }} @@ -19,7 +23,7 @@ jobs: - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: ${{ matrix.python-version }} - name: Install test dependencies run: | diff --git a/tests/conftest.py b/tests/conftest.py index e482ccfe..da9ac770 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,6 @@ """Common pytest fixtures.""" - from contextlib import contextmanager import os from shutil import rmtree diff --git a/tests/test_background.py b/tests/test_background.py index 7040cb45..03b98f94 100644 --- a/tests/test_background.py +++ b/tests/test_background.py @@ -11,7 +11,6 @@ """Unit tests for :mod:`workflow.background`.""" - import os from time import sleep diff --git a/tests/test_notify.py b/tests/test_notify.py index 99b20cb5..704d612f 100644 --- a/tests/test_notify.py +++ b/tests/test_notify.py @@ -11,7 +11,6 @@ """Unit tests for notifications.""" - import hashlib import logging import os diff --git a/tests/test_util_atomic.py b/tests/test_util_atomic.py index f3f46d5d..4a6df161 100644 --- a/tests/test_util_atomic.py +++ b/tests/test_util_atomic.py @@ -11,7 +11,6 @@ """Unit tests for :func:`~workflow.util.atomic_writer`.""" - import json import os diff --git a/tests/test_util_lockfile.py b/tests/test_util_lockfile.py index b02aa28b..17294c67 100644 --- a/tests/test_util_lockfile.py +++ b/tests/test_util_lockfile.py @@ -11,7 +11,6 @@ """Test LockFile functionality.""" - from collections import namedtuple from multiprocessing import Pool import os diff --git a/tests/test_util_uninterruptible.py b/tests/test_util_uninterruptible.py index 56b8a1a5..d4a07109 100644 --- a/tests/test_util_uninterruptible.py +++ b/tests/test_util_uninterruptible.py @@ -11,7 +11,6 @@ """Unit tests for ``uninterruptible`` decorator.""" - import os import signal diff --git a/tests/test_web.py b/tests/test_web.py index fc5a171b..a071fcb6 100644 --- a/tests/test_web.py +++ b/tests/test_web.py @@ -344,12 +344,15 @@ def test_file_upload_without_form_data(self): 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': 'Hére ïs søme ÜÑÎÇÒDÈ™' + '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""" diff --git a/tests/test_web_http_encoding.py b/tests/test_web_http_encoding.py index de69ef4e..76b0cf12 100644 --- a/tests/test_web_http_encoding.py +++ b/tests/test_web_http_encoding.py @@ -11,7 +11,6 @@ """HTTP unit tests.""" - import os import pytest diff --git a/tests/test_workflow.py b/tests/test_workflow.py index 8afe0b9f..d4c64753 100644 --- a/tests/test_workflow.py +++ b/tests/test_workflow.py @@ -11,7 +11,6 @@ """Unit tests for :mod:`workflow.Workflow`.""" - import logging import os import sys diff --git a/tests/test_workflow3.py b/tests/test_workflow3.py index 9ccf88b3..dd604eb0 100644 --- a/tests/test_workflow3.py +++ b/tests/test_workflow3.py @@ -11,7 +11,6 @@ """Test Workflow3 feedback.""" - import json import os from io import StringIO diff --git a/tests/test_workflow_encoding.py b/tests/test_workflow_encoding.py index 88371bc6..c9e85bf0 100644 --- a/tests/test_workflow_encoding.py +++ b/tests/test_workflow_encoding.py @@ -7,7 +7,6 @@ """Unit tests for serializers.""" - import pytest diff --git a/tests/test_workflow_env.py b/tests/test_workflow_env.py index bc51066f..2be8bc9a 100644 --- a/tests/test_workflow_env.py +++ b/tests/test_workflow_env.py @@ -7,7 +7,6 @@ """Unit tests for environment/info.plist.""" - import logging import os diff --git a/tests/test_workflow_files.py b/tests/test_workflow_files.py index b4834e1c..a468c5d2 100644 --- a/tests/test_workflow_files.py +++ b/tests/test_workflow_files.py @@ -7,7 +7,6 @@ """Unit tests for Workflow directory & file APIs.""" - import json import os import time diff --git a/tests/test_workflow_filter.py b/tests/test_workflow_filter.py index 1dc64da7..848c6e62 100644 --- a/tests/test_workflow_filter.py +++ b/tests/test_workflow_filter.py @@ -7,7 +7,6 @@ """Unit tests for :meth:`workflow.Workflow.filter`.""" - import pytest from workflow.workflow import ( diff --git a/tests/test_workflow_import.py b/tests/test_workflow_import.py index cc749e80..db11d041 100644 --- a/tests/test_workflow_import.py +++ b/tests/test_workflow_import.py @@ -7,7 +7,6 @@ """Unit tests for sys.path manipulation.""" - import os import sys diff --git a/tests/test_workflow_keychain.py b/tests/test_workflow_keychain.py index f24d7097..07e973ae 100644 --- a/tests/test_workflow_keychain.py +++ b/tests/test_workflow_keychain.py @@ -7,7 +7,6 @@ """Unit tests for Keychain API.""" - import pytest from workflow.workflow import PasswordNotFound, KeychainError diff --git a/tests/test_workflow_magic.py b/tests/test_workflow_magic.py index 4f514dd3..b193703a 100644 --- a/tests/test_workflow_magic.py +++ b/tests/test_workflow_magic.py @@ -11,7 +11,6 @@ """Unit tests for magic arguments.""" - import os import pytest diff --git a/tests/test_workflow_magic_alfred2.py b/tests/test_workflow_magic_alfred2.py index 28698538..211eae33 100644 --- a/tests/test_workflow_magic_alfred2.py +++ b/tests/test_workflow_magic_alfred2.py @@ -7,7 +7,6 @@ """Unit tests for Alfred 2 magic argument handling.""" - import pytest from workflow import Workflow diff --git a/tests/test_workflow_run.py b/tests/test_workflow_run.py index 8e376574..1204dc3e 100644 --- a/tests/test_workflow_run.py +++ b/tests/test_workflow_run.py @@ -7,7 +7,6 @@ """Unit tests for Workflow.run.""" - from io import StringIO import sys diff --git a/tests/test_workflow_serializers.py b/tests/test_workflow_serializers.py index 868721a3..adc65e81 100644 --- a/tests/test_workflow_serializers.py +++ b/tests/test_workflow_serializers.py @@ -11,7 +11,6 @@ """Unit tests for serializer classes.""" - import os import pytest diff --git a/tests/test_workflow_settings.py b/tests/test_workflow_settings.py index 9721129f..190e0d2b 100644 --- a/tests/test_workflow_settings.py +++ b/tests/test_workflow_settings.py @@ -11,7 +11,6 @@ """Unit tests for Workflow.settings API.""" - import json import os import shutil diff --git a/tests/test_workflow_update.py b/tests/test_workflow_update.py index 7a64d5c6..91fba298 100644 --- a/tests/test_workflow_update.py +++ b/tests/test_workflow_update.py @@ -11,7 +11,6 @@ """Unit tests for Workflow's update API.""" - from contextlib import contextmanager import os @@ -91,13 +90,19 @@ def update(wf): with fakeresponse(httpserver, RELEASES_JSON, HTTP_HEADERS_JSON): with ctx() as (wf, c): wf.run(update) - assert c.cmd == ['/usr/bin/python3', '-m', 'workflow.background', '__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 == ['/usr/bin/python3', '-m', 'workflow.background', '__workflow_update_check'] + assert c.cmd == [ + '/usr/bin/python3', '-m', 'workflow.background', + '__workflow_update_check' + ] def test_install_update(httpserver, alfred4): @@ -114,7 +119,10 @@ def fake(wf): print('Magic update command : {0!r}'.format(c.cmd)) - assert c.cmd == ['/usr/bin/python3', '-m', 'workflow.background', '__workflow_update_install'] + assert c.cmd == [ + '/usr/bin/python3', '-m', 'workflow.background', + '__workflow_update_install' + ] update_settings = UPDATE_SETTINGS.copy() del update_settings['version'] @@ -161,7 +169,10 @@ def fake(wf): print('Magic update command : {!r}'.format(c.cmd)) - assert c.cmd == ['/usr/bin/python3', '-m', 'workflow.background', '__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 dc551255..81f0b41e 100644 --- a/tests/test_workflow_versions.py +++ b/tests/test_workflow_versions.py @@ -7,7 +7,6 @@ """Unit tests for workflow version determination.""" - import pytest from workflow.update import Version diff --git a/tests/test_workflow_xml.py b/tests/test_workflow_xml.py index 245c534c..832bb079 100644 --- a/tests/test_workflow_xml.py +++ b/tests/test_workflow_xml.py @@ -11,7 +11,6 @@ """Unit tests for Workflow's XML feedback generation.""" - from contextlib import contextmanager from io import StringIO import sys diff --git a/tests/util.py b/tests/util.py index ab59cb12..edd8bd0a 100644 --- a/tests/util.py +++ b/tests/util.py @@ -11,7 +11,6 @@ """Stuff used in multiple tests.""" - from io import StringIO import sys import os diff --git a/tox.ini b/tox.ini index 9b697428..228267ef 100644 --- a/tox.ini +++ b/tox.ini @@ -20,8 +20,15 @@ usedevelop = true deps = -r requirements-test.txt coverage +commands = + ./run-tests.sh -commands = ./run-tests.sh +[testenv:lint] +usedevelop = true +deps = + -r requirements-test.txt +commands = + ./run-tests.sh -l [flake8] builtins = unicode From a3081b756f01d16b2ff42894845de0056b1a31fc Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Thu, 21 Apr 2022 10:40:36 -0700 Subject: [PATCH 30/31] Run GitHub workflows on 3.7, 3.8, 3.9 --- .github/workflows/continuous-integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 7c6e89f4..31d1b870 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -12,7 +12,7 @@ jobs: runs-on: macos-latest strategy: matrix: - python-version: [3.8] + python-version: [3.7, 3.8, 3.9] env: COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} From 14f5c09a5d733d42f1eaa746e23427efa4ae882c Mon Sep 17 00:00:00 2001 From: Alex Levy Date: Fri, 22 Apr 2022 10:22:33 -0700 Subject: [PATCH 31/31] Pass CODECOV_TOKEN into CI environment --- .github/workflows/continuous-integration.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 31d1b870..5763065a 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -17,6 +17,7 @@ jobs: 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