From 00e856fed5a3447a60bc575cd2b75118a39d2770 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Jan 2021 11:00:19 -0500 Subject: [PATCH 01/30] Declare scripts.types_ as ported (never had Python 2 support). --- src/allmydata/scripts/types_.py | 2 ++ src/allmydata/util/_python3.py | 1 + 2 files changed, 3 insertions(+) diff --git a/src/allmydata/scripts/types_.py b/src/allmydata/scripts/types_.py index 3937cb803..217dc2d28 100644 --- a/src/allmydata/scripts/types_.py +++ b/src/allmydata/scripts/types_.py @@ -1,3 +1,5 @@ +# Python 3 only + from typing import List, Tuple, Type, Sequence, Any from allmydata.scripts.common import BaseOptions diff --git a/src/allmydata/util/_python3.py b/src/allmydata/util/_python3.py index 38d0f4d7e..b5acd989d 100644 --- a/src/allmydata/util/_python3.py +++ b/src/allmydata/util/_python3.py @@ -72,6 +72,7 @@ PORTED_MODULES = [ "allmydata.mutable.servermap", "allmydata.node", "allmydata.nodemaker", + "allmydata.scripts.types_", "allmydata.stats", "allmydata.storage_client", "allmydata.storage.common", From 5bf2b09b81b28ca16cf6fbfb569057ec27843254 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Jan 2021 11:12:01 -0500 Subject: [PATCH 02/30] In scripts.runner, replace hard failure on Python 3 with 'experimental' warning. Ref #3603. Closes #3035. --- src/allmydata/scripts/runner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/allmydata/scripts/runner.py b/src/allmydata/scripts/runner.py index 9a632a57d..af8b89d79 100644 --- a/src/allmydata/scripts/runner.py +++ b/src/allmydata/scripts/runner.py @@ -1,5 +1,6 @@ from __future__ import print_function +import warnings import os, sys from six.moves import StringIO import six @@ -177,9 +178,8 @@ def _maybe_enable_eliot_logging(options, reactor): return options def run(): - # TODO(3035): Remove tox-check when error becomes a warning - if 'TOX_ENV_NAME' not in os.environ: - assert sys.version_info < (3,), u"Tahoe-LAFS does not run under Python 3. Please use Python 2.7.x." + if not six.PY2: + warnings.warn("Support for Python 3 is experimental. Use at your own risk.") if sys.platform == "win32": from allmydata.windows.fixups import initialize From 2bd244dde95d32a0949abfd465d06428b56179d9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Jan 2021 11:24:06 -0500 Subject: [PATCH 03/30] Declare scripts.runner as ported. Ref #3603. --- src/allmydata/util/_python3.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/allmydata/util/_python3.py b/src/allmydata/util/_python3.py index b5acd989d..242bdaae8 100644 --- a/src/allmydata/util/_python3.py +++ b/src/allmydata/util/_python3.py @@ -72,6 +72,7 @@ PORTED_MODULES = [ "allmydata.mutable.servermap", "allmydata.node", "allmydata.nodemaker", + "allmydata.scripts.runner", "allmydata.scripts.types_", "allmydata.stats", "allmydata.storage_client", From 210eb5b5293f4bd4d6734a2a4317484fd26215e8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Jan 2021 11:32:12 -0500 Subject: [PATCH 04/30] Add test for escape_path. --- src/allmydata/scripts/common.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/allmydata/scripts/common.py b/src/allmydata/scripts/common.py index d73344274..e50617fd7 100644 --- a/src/allmydata/scripts/common.py +++ b/src/allmydata/scripts/common.py @@ -1,3 +1,5 @@ +# coding: utf-8 + from __future__ import print_function import os, sys, urllib, textwrap @@ -270,6 +272,16 @@ def get_alias(aliases, path_unicode, default): return uri.from_string_dirnode(aliases[alias]).to_string(), path[colon+1:] def escape_path(path): + # type: (str) -> str + """ + Return path quoted to US-ASCII. + + >>> path = u'/føö/bar/☃' + >>> escape_path(path) + '/f%C3%B8%C3%B6/bar/%E2%98%83' + >>> escape_path(path).encode('ascii') + b'/f%C3%B8%C3%B6/bar/%E2%98%83' + """ # this always returns bytes, specifically US-ASCII, valid URL characters segments = path.split("/") return "/".join([urllib.quote(unicode_to_url(s)) for s in segments]) From 5fc9674d3a55d0e9591fdce8307b865ae0256e5d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Jan 2021 11:47:53 -0500 Subject: [PATCH 05/30] Update escape_path for Python 3 compatibility. --- src/allmydata/scripts/common.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/allmydata/scripts/common.py b/src/allmydata/scripts/common.py index e50617fd7..ae9d78db7 100644 --- a/src/allmydata/scripts/common.py +++ b/src/allmydata/scripts/common.py @@ -2,10 +2,12 @@ from __future__ import print_function -import os, sys, urllib, textwrap +import os, sys, textwrap import codecs from os.path import join +from six.moves import urllib # import urllib.parse + try: from typing import Optional from .types_ import Parameters @@ -284,4 +286,4 @@ def escape_path(path): """ # this always returns bytes, specifically US-ASCII, valid URL characters segments = path.split("/") - return "/".join([urllib.quote(unicode_to_url(s)) for s in segments]) + return "/".join([urllib.parse.quote(unicode_to_url(s)) for s in segments]) From 36b7fdaecf7cd3bfcc410b64c07007bcc61d6a5d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Jan 2021 12:15:36 -0500 Subject: [PATCH 06/30] Apply futurize to create_node. --- src/allmydata/scripts/create_node.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/allmydata/scripts/create_node.py b/src/allmydata/scripts/create_node.py index 0f507f518..2d949b1ed 100644 --- a/src/allmydata/scripts/create_node.py +++ b/src/allmydata/scripts/create_node.py @@ -1,4 +1,11 @@ from __future__ import print_function +from __future__ import absolute_import +from __future__ import division +from __future__ import unicode_literals + +from future.utils import PY2 +if PY2: + from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401 import os import json @@ -301,13 +308,13 @@ def write_node_config(c, config): if tor_config: c.write("[tor]\n") - for key, value in tor_config.items(): + for key, value in list(tor_config.items()): c.write("%s = %s\n" % (key, value)) c.write("\n") if i2p_config: c.write("[i2p]\n") - for key, value in i2p_config.items(): + for key, value in list(i2p_config.items()): c.write("%s = %s\n" % (key, value)) c.write("\n") @@ -370,7 +377,7 @@ def _get_config_via_wormhole(config): relay_url=relay_url, reactor=reactor, ) - code = unicode(config['join']) + code = str(config['join']) wh.set_code(code) yield wh.get_welcome() print("Connected to wormhole server", file=out) @@ -402,7 +409,7 @@ def create_node(config): err = config.stderr basedir = config['basedir'] # This should always be called with an absolute Unicode basedir. - precondition(isinstance(basedir, unicode), basedir) + precondition(isinstance(basedir, str), basedir) if os.path.exists(basedir): if listdir_unicode(basedir): @@ -437,7 +444,7 @@ def create_node(config): v = remote_config.get(k, None) if v is not None: # we're faking usually argv-supplied options :/ - if isinstance(v, unicode): + if isinstance(v, str): v = v.encode(get_io_encoding()) config[k] = v if k not in sensitive_keys: @@ -475,7 +482,7 @@ def create_introducer(config): err = config.stderr basedir = config['basedir'] # This should always be called with an absolute Unicode basedir. - precondition(isinstance(basedir, unicode), basedir) + precondition(isinstance(basedir, str), basedir) if os.path.exists(basedir): if listdir_unicode(basedir): From d6082d853a0babdf570b54123cf0a8c78023ec3e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Jan 2021 12:23:16 -0500 Subject: [PATCH 07/30] Declare scripts.create_node as ported. --- src/allmydata/scripts/create_node.py | 2 ++ src/allmydata/util/_python3.py | 1 + 2 files changed, 3 insertions(+) diff --git a/src/allmydata/scripts/create_node.py b/src/allmydata/scripts/create_node.py index 2d949b1ed..782bccda6 100644 --- a/src/allmydata/scripts/create_node.py +++ b/src/allmydata/scripts/create_node.py @@ -1,3 +1,5 @@ +# Ported to Python 3 + from __future__ import print_function from __future__ import absolute_import from __future__ import division diff --git a/src/allmydata/util/_python3.py b/src/allmydata/util/_python3.py index 242bdaae8..e7d8e2049 100644 --- a/src/allmydata/util/_python3.py +++ b/src/allmydata/util/_python3.py @@ -72,6 +72,7 @@ PORTED_MODULES = [ "allmydata.mutable.servermap", "allmydata.node", "allmydata.nodemaker", + "allmydata.scripts.create_node", "allmydata.scripts.runner", "allmydata.scripts.types_", "allmydata.stats", From 57cb88638a8ee0782f9fcc5256412a93dc3c05ee Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Jan 2021 12:46:44 -0500 Subject: [PATCH 08/30] In scripts.create_node, set the encoding once and write text. --- src/allmydata/scripts/create_node.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/allmydata/scripts/create_node.py b/src/allmydata/scripts/create_node.py index 782bccda6..3afbeeb0d 100644 --- a/src/allmydata/scripts/create_node.py +++ b/src/allmydata/scripts/create_node.py @@ -9,6 +9,7 @@ from future.utils import PY2 if PY2: from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401 +import io import os import json @@ -234,7 +235,7 @@ class CreateIntroducerOptions(NoDefaultBasedirOptions): @defer.inlineCallbacks def write_node_config(c, config): # this is shared between clients and introducers - c.write("# -*- mode: conf; coding: utf-8 -*-\n") + c.write("# -*- mode: conf; coding: {c.encoding} -*-\n".format(c=c)) c.write("\n") c.write("# This file controls the configuration of the Tahoe node that\n") c.write("# lives in this directory. It is only read at node startup.\n") @@ -253,7 +254,7 @@ def write_node_config(c, config): c.write("[node]\n") nickname = argv_to_unicode(config.get("nickname") or "") - c.write("nickname = %s\n" % (nickname.encode('utf-8'),)) + c.write("nickname = %s\n" % (nickname,)) if config["hide-ip"]: c.write("reveal-IP-address = false\n") else: @@ -263,7 +264,7 @@ def write_node_config(c, config): webport = argv_to_unicode(config.get("webport") or "none") if webport.lower() == "none": webport = "" - c.write("web.port = %s\n" % (webport.encode('utf-8'),)) + c.write("web.port = %s\n" % (webport,)) c.write("web.static = public_html\n") listeners = config['listen'].split(",") @@ -288,15 +289,14 @@ def write_node_config(c, config): tub_locations.append(i2p_location) if "tcp" in listeners: if config["port"]: # --port/--location are a pair - tub_ports.append(config["port"].encode('utf-8')) - tub_locations.append(config["location"].encode('utf-8')) + tub_ports.append(config["port"]) + tub_locations.append(config["location"]) else: assert "hostname" in config hostname = config["hostname"] new_port = iputil.allocate_tcp_port() tub_ports.append("tcp:%s" % new_port) - tub_locations.append("tcp:%s:%s" % (hostname.encode('utf-8'), - new_port)) + tub_locations.append("tcp:%s:%s" % (hostname, new_port)) c.write("tub.port = %s\n" % ",".join(tub_ports)) c.write("tub.location = %s\n" % ",".join(tub_locations)) c.write("\n") @@ -456,7 +456,8 @@ def create_node(config): print(" {}: [sensitive data; see tahoe.cfg]".format(k), file=out) fileutil.make_dirs(os.path.join(basedir, "private"), 0o700) - with open(os.path.join(basedir, "tahoe.cfg"), "w") as c: + cfg_name = os.path.join(basedir, "tahoe.cfg") + with io.open(cfg_name, "w", encoding='utf-8') as c: yield write_node_config(c, config) write_client_config(c, config) @@ -498,7 +499,8 @@ def create_introducer(config): write_tac(basedir, "introducer") fileutil.make_dirs(os.path.join(basedir, "private"), 0o700) - with open(os.path.join(basedir, "tahoe.cfg"), "w") as c: + cfg_name = os.path.join(basedir, "tahoe.cfg") + with io.open(cfg_name, "w", encoding='utf-8') as c: yield write_node_config(c, config) print("Introducer created in %s" % quote_local_unicode_path(basedir), file=out) From 621ae58abea737221adc0282859794ceaa42244e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 31 Jan 2021 11:13:59 -0500 Subject: [PATCH 09/30] Avoid overthinking encoding when handling usage errors. 'test_unicode_arguments_and_output' still passes on Python 2. Ref #3603. --- src/allmydata/scripts/runner.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/allmydata/scripts/runner.py b/src/allmydata/scripts/runner.py index af8b89d79..705fa37bf 100644 --- a/src/allmydata/scripts/runner.py +++ b/src/allmydata/scripts/runner.py @@ -16,7 +16,7 @@ from twisted.internet import defer, task, threads from allmydata.scripts.common import get_default_nodedir from allmydata.scripts import debug, create_node, cli, \ admin, tahoe_run, tahoe_invite -from allmydata.util.encodingutil import quote_output, quote_local_unicode_path, get_io_encoding +from allmydata.util.encodingutil import quote_local_unicode_path from allmydata.util.eliotutil import ( opt_eliot_destination, opt_help_eliot_destinations, @@ -121,11 +121,7 @@ def parse_or_exit_with_explanation(argv, stdout=sys.stdout): while hasattr(c, 'subOptions'): c = c.subOptions print(str(c), file=stdout) - try: - msg = e.args[0].decode(get_io_encoding()) - except Exception: - msg = repr(e) - print("%s: %s\n" % (sys.argv[0], quote_output(msg, quotemarks=False)), file=stdout) + print("%s: %s\n" % (sys.argv[0], e), file=stdout) sys.exit(1) return config From 7ed3d9597eea55ff9eb17de84a0ff2507c49a560 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 12 Feb 2021 16:17:44 -0500 Subject: [PATCH 10/30] Add newsfragment --- newsfragments/3603.minor.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/3603.minor.rst diff --git a/newsfragments/3603.minor.rst b/newsfragments/3603.minor.rst new file mode 100644 index 000000000..512ad0194 --- /dev/null +++ b/newsfragments/3603.minor.rst @@ -0,0 +1 @@ +Ported allmydata.scripts.create_node and .runner to Python 3. Declared scripts.types_ as Python 3 only. From eefb7004c902d41ed08a9e42b14f56e634fdf9fd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Feb 2021 09:20:45 -0500 Subject: [PATCH 11/30] Add test_runner to the ported test modules. Selectively decode sys.argv on Python 2 only. Fixes 6 test failures on Python 3. --- src/allmydata/test/test_runner.py | 9 +++++++-- src/allmydata/util/_python3.py | 1 + 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/allmydata/test/test_runner.py b/src/allmydata/test/test_runner.py index cf56e8baa..d8f9387c0 100644 --- a/src/allmydata/test/test_runner.py +++ b/src/allmydata/test/test_runner.py @@ -10,6 +10,8 @@ from eliot import ( log_call, ) +import six + from twisted.trial import unittest from twisted.internet import reactor @@ -23,7 +25,7 @@ from twisted.python.runtime import ( platform, ) from allmydata.util import fileutil, pollmixin -from allmydata.util.encodingutil import unicode_to_argv, get_filesystem_encoding +from allmydata.util.encodingutil import unicode_to_argv from allmydata.test import common_util import allmydata from .common import ( @@ -72,7 +74,10 @@ def run_bintahoe(extra_argv, python_options=None): :return: A three-tuple of stdout (unicode), stderr (unicode), and the child process "returncode" (int). """ - argv = [sys.executable.decode(get_filesystem_encoding())] + # fixme: below, 'unicode_to_argv' is called so ensure that + # executable is unicode to support that expectation. + executable = sys.executable.decode('utf-8') if six.PY2 else sys.executable + argv = [executable] if python_options is not None: argv.extend(python_options) argv.extend([u"-m", u"allmydata.scripts.runner"]) diff --git a/src/allmydata/util/_python3.py b/src/allmydata/util/_python3.py index a331a3734..79622650c 100644 --- a/src/allmydata/util/_python3.py +++ b/src/allmydata/util/_python3.py @@ -174,6 +174,7 @@ PORTED_TEST_MODULES = [ "allmydata.test.test_pipeline", "allmydata.test.test_python3", "allmydata.test.test_repairer", + "allmydata.test.test_runner", "allmydata.test.test_sftp", "allmydata.test.test_spans", "allmydata.test.test_statistics", From dc883c0440f930ab9f01db1ca1c7db75c69cf7d0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Feb 2021 09:24:16 -0500 Subject: [PATCH 12/30] Decode config file on read. Fixes two test failures on Python 3. --- src/allmydata/test/test_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/test_runner.py b/src/allmydata/test/test_runner.py index d8f9387c0..cca1ae70f 100644 --- a/src/allmydata/test/test_runner.py +++ b/src/allmydata/test/test_runner.py @@ -184,7 +184,7 @@ class CreateNode(unittest.TestCase): # 'create-node', and disabled for 'create-client'. tahoe_cfg = os.path.join(n1, "tahoe.cfg") self.failUnless(os.path.exists(tahoe_cfg)) - content = fileutil.read(tahoe_cfg).replace('\r\n', '\n') + content = fileutil.read(tahoe_cfg).decode('utf-8').replace('\r\n', '\n') if kind == "client": self.failUnless(re.search(r"\n\[storage\]\n#.*\nenabled = false\n", content), content) else: From 3acad6544e56878513e33862d426b403d19ad25f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Feb 2021 09:28:14 -0500 Subject: [PATCH 13/30] Decode config file on read and expect bytes in the stdout. Fixes an additional test on Python 3. --- src/allmydata/test/test_runner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/allmydata/test/test_runner.py b/src/allmydata/test/test_runner.py index cca1ae70f..bee35522b 100644 --- a/src/allmydata/test/test_runner.py +++ b/src/allmydata/test/test_runner.py @@ -374,7 +374,7 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin): self.failUnlessEqual(returncode, 0) # Check that the --webport option worked. - config = fileutil.read(tahoe.config_file.path) + config = fileutil.read(tahoe.config_file.path).decode('utf-8') self.assertIn( '{}web.port = 0{}'.format(linesep, linesep), config, @@ -387,7 +387,7 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin): # This will run until we stop it. tahoe.run(on_stdout(p)) # Wait for startup to have proceeded to a reasonable point. - yield p.expect("client running") + yield p.expect(b"client running") tahoe.active() # read the storage.furl file so we can check that its contents don't @@ -406,7 +406,7 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin): # We don't have to add another cleanup for this one, the one from # above is still registered. tahoe.run(on_stdout(p)) - yield p.expect("client running") + yield p.expect(b"client running") tahoe.active() self.assertEqual( From f183be9d6a859b4600d71f6564d851e663c23a97 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Feb 2021 09:33:54 -0500 Subject: [PATCH 14/30] Decode config file on read and expect bytes in the stdout. Fixes an additional test on Python 3. --- src/allmydata/test/test_runner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/allmydata/test/test_runner.py b/src/allmydata/test/test_runner.py index bee35522b..885af5efb 100644 --- a/src/allmydata/test/test_runner.py +++ b/src/allmydata/test/test_runner.py @@ -296,7 +296,7 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin): # This makes sure that node.url is written, which allows us to # detect when the introducer restarts in _node_has_restarted below. - config = fileutil.read(tahoe.config_file.path) + config = fileutil.read(tahoe.config_file.path).decode('utf-8') self.assertIn('{}web.port = {}'.format(linesep, linesep), config) fileutil.write( tahoe.config_file.path, @@ -308,7 +308,7 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin): p = Expect() tahoe.run(on_stdout(p)) - yield p.expect("introducer running") + yield p.expect(b"introducer running") tahoe.active() yield self.poll(tahoe.introducer_furl_file.exists) @@ -331,7 +331,7 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin): p = Expect() tahoe.run(on_stdout(p)) - yield p.expect("introducer running") + yield p.expect(b"introducer running") # Again, the second incarnation of the node might not be ready yet, so # poll until it is. This time introducer_furl_file already exists, so From 30f5c71a499f1fbaa48d40aaa08ae0554b7da202 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Feb 2021 09:36:07 -0500 Subject: [PATCH 15/30] Encode expected message. Fixes remaining test failures in test_runner. --- src/allmydata/test/test_runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/test/test_runner.py b/src/allmydata/test/test_runner.py index 885af5efb..597a75fe8 100644 --- a/src/allmydata/test/test_runner.py +++ b/src/allmydata/test/test_runner.py @@ -497,7 +497,7 @@ class RunNode(common_util.SignalMixin, unittest.TestCase, pollmixin.PollMixin): client_running = p.expect(b"client running") result, index = yield DeferredList([ - p.expect(expected_message), + p.expect(expected_message.encode('utf-8')), client_running, ], fireOnOneCallback=True, consumeErrors=True, ) From 3c93605ead266dde837324fa317843b419fd4778 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Feb 2021 12:15:41 -0500 Subject: [PATCH 16/30] Add docstring to types_. --- src/allmydata/scripts/types_.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/allmydata/scripts/types_.py b/src/allmydata/scripts/types_.py index 217dc2d28..289e674ce 100644 --- a/src/allmydata/scripts/types_.py +++ b/src/allmydata/scripts/types_.py @@ -1,3 +1,7 @@ +""" +Type definitions used by modules in this package. +""" + # Python 3 only from typing import List, Tuple, Type, Sequence, Any From 2dc0a55d5d85fd9e9a41de1d9565ff978c5318c7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Feb 2021 12:18:10 -0500 Subject: [PATCH 17/30] Truncate newsfragment --- newsfragments/3603.minor.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/newsfragments/3603.minor.rst b/newsfragments/3603.minor.rst index 512ad0194..e69de29bb 100644 --- a/newsfragments/3603.minor.rst +++ b/newsfragments/3603.minor.rst @@ -1 +0,0 @@ -Ported allmydata.scripts.create_node and .runner to Python 3. Declared scripts.types_ as Python 3 only. From d59d64d6bdd087fba765129b2e0496ebff77dc98 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Feb 2021 12:38:37 -0500 Subject: [PATCH 18/30] Fully port test_runner. Introduces three new errors on Python 2. --- src/allmydata/test/test_runner.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/allmydata/test/test_runner.py b/src/allmydata/test/test_runner.py index 597a75fe8..12b658a71 100644 --- a/src/allmydata/test/test_runner.py +++ b/src/allmydata/test/test_runner.py @@ -2,7 +2,13 @@ from __future__ import ( absolute_import, ) +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals +from future.utils import PY2 +if PY2: + from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401 import os.path, re, sys from os import linesep From 746e1b2664816d40936cba7d38fb5cfbae5c94ef Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Feb 2021 12:47:26 -0500 Subject: [PATCH 19/30] Fix test failures on Python 2 by wrapping cli calls in unicode_to_argv. --- src/allmydata/test/test_runner.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/allmydata/test/test_runner.py b/src/allmydata/test/test_runner.py index 12b658a71..d419eed37 100644 --- a/src/allmydata/test/test_runner.py +++ b/src/allmydata/test/test_runner.py @@ -178,7 +178,7 @@ class CreateNode(unittest.TestCase): n1 = os.path.join(basedir, command + "-n1") argv = ["--quiet", command, "--basedir", n1] + list(args) - rc, out, err = yield run_cli(*argv) + rc, out, err = yield run_cli(*map(unicode_to_argv, argv)) self.failUnlessEqual(err, "") self.failUnlessEqual(out, "") self.failUnlessEqual(rc, 0) @@ -198,7 +198,7 @@ class CreateNode(unittest.TestCase): self.failUnless("\nreserved_space = 1G\n" in content) # creating the node a second time should be rejected - rc, out, err = yield run_cli(*argv) + rc, out, err = yield run_cli(*map(unicode_to_argv, argv)) self.failIfEqual(rc, 0, str((out, err, rc))) self.failUnlessEqual(out, "") self.failUnless("is not empty." in err) @@ -211,7 +211,7 @@ class CreateNode(unittest.TestCase): # test that the non --basedir form works too n2 = os.path.join(basedir, command + "-n2") argv = ["--quiet", command] + list(args) + [n2] - rc, out, err = yield run_cli(*argv) + rc, out, err = yield run_cli(*map(unicode_to_argv, argv)) self.failUnlessEqual(err, "") self.failUnlessEqual(out, "") self.failUnlessEqual(rc, 0) @@ -221,7 +221,7 @@ class CreateNode(unittest.TestCase): # test the --node-directory form n3 = os.path.join(basedir, command + "-n3") argv = ["--quiet", "--node-directory", n3, command] + list(args) - rc, out, err = yield run_cli(*argv) + rc, out, err = yield run_cli(*map(unicode_to_argv, argv)) self.failUnlessEqual(err, "") self.failUnlessEqual(out, "") self.failUnlessEqual(rc, 0) @@ -232,7 +232,7 @@ class CreateNode(unittest.TestCase): # test that the output (without --quiet) includes the base directory n4 = os.path.join(basedir, command + "-n4") argv = [command] + list(args) + [n4] - rc, out, err = yield run_cli(*argv) + rc, out, err = yield run_cli(*map(unicode_to_argv, argv)) self.failUnlessEqual(err, "") self.failUnlessIn(" created in ", out) self.failUnlessIn(n4, out) From 4cb8c420d321125ec9ccf0658d46dfd13128e428 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Feb 2021 12:49:20 -0500 Subject: [PATCH 20/30] Manually reviewed test_runner (no legacy map, filter, or keys found). Added docstring to tag module as fully ported. --- src/allmydata/test/test_runner.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/allmydata/test/test_runner.py b/src/allmydata/test/test_runner.py index d419eed37..e7e9892fb 100644 --- a/src/allmydata/test/test_runner.py +++ b/src/allmydata/test/test_runner.py @@ -1,3 +1,6 @@ +""" +Ported to Python 3 +""" from __future__ import ( absolute_import, From ebbe645cb613527b56d872a86ddbc19254368f0a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Feb 2021 13:06:42 -0500 Subject: [PATCH 21/30] Use explicit unicode literal for docstring. Now test passes on Python 2 when ALLOW_BYTES is indicated. --- src/allmydata/scripts/common.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/scripts/common.py b/src/allmydata/scripts/common.py index e50617fd7..ecb135b57 100644 --- a/src/allmydata/scripts/common.py +++ b/src/allmydata/scripts/common.py @@ -273,7 +273,7 @@ def get_alias(aliases, path_unicode, default): def escape_path(path): # type: (str) -> str - """ + u""" Return path quoted to US-ASCII. >>> path = u'/føö/bar/☃' From 2fb603e6032032c1c84e8df6ed3978e6d659072e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Feb 2021 13:10:21 -0500 Subject: [PATCH 22/30] Rewrite doctest to pass on Python 2+3. --- src/allmydata/scripts/common.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/allmydata/scripts/common.py b/src/allmydata/scripts/common.py index ecb135b57..2d8d9c63a 100644 --- a/src/allmydata/scripts/common.py +++ b/src/allmydata/scripts/common.py @@ -277,10 +277,11 @@ def escape_path(path): Return path quoted to US-ASCII. >>> path = u'/føö/bar/☃' - >>> escape_path(path) + >>> escaped = escape_path(path) + >>> str(escaped) '/f%C3%B8%C3%B6/bar/%E2%98%83' - >>> escape_path(path).encode('ascii') - b'/f%C3%B8%C3%B6/bar/%E2%98%83' + >>> escaped.encode('ascii').decode('ascii') == escaped + True """ # this always returns bytes, specifically US-ASCII, valid URL characters segments = path.split("/") From 09f3e3f6b54e340294de84df5d599091a91564d9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Feb 2021 13:26:20 -0500 Subject: [PATCH 23/30] Remove comment, superseded by docstring. --- src/allmydata/scripts/common.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/allmydata/scripts/common.py b/src/allmydata/scripts/common.py index 58c7e33db..3d5237033 100644 --- a/src/allmydata/scripts/common.py +++ b/src/allmydata/scripts/common.py @@ -276,7 +276,7 @@ def get_alias(aliases, path_unicode, default): def escape_path(path): # type: (str) -> str u""" - Return path quoted to US-ASCII. + Return path quoted to US-ASCII, valid URL characters. >>> path = u'/føö/bar/☃' >>> escaped = escape_path(path) @@ -285,6 +285,5 @@ def escape_path(path): >>> escaped.encode('ascii').decode('ascii') == escaped True """ - # this always returns bytes, specifically US-ASCII, valid URL characters segments = path.split("/") return "/".join([urllib.parse.quote(unicode_to_url(s)) for s in segments]) From d02334bfd5ac8a1ec72943b01afd18cf33929fc2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Feb 2021 14:13:59 -0500 Subject: [PATCH 24/30] Rely on futurize to expose the moved modules. --- src/allmydata/scripts/common.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/allmydata/scripts/common.py b/src/allmydata/scripts/common.py index 3d5237033..c10309491 100644 --- a/src/allmydata/scripts/common.py +++ b/src/allmydata/scripts/common.py @@ -5,8 +5,7 @@ from __future__ import print_function import os, sys, textwrap import codecs from os.path import join - -from six.moves import urllib # import urllib.parse +import urllib.parse try: from typing import Optional From 6118d1a2d23d181c2d3f39ef2ed1aeb0ba58742a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Feb 2021 14:39:27 -0500 Subject: [PATCH 25/30] Remove runner from ported modules. --- src/allmydata/util/_python3.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/allmydata/util/_python3.py b/src/allmydata/util/_python3.py index 79622650c..ef846886d 100644 --- a/src/allmydata/util/_python3.py +++ b/src/allmydata/util/_python3.py @@ -73,7 +73,6 @@ PORTED_MODULES = [ "allmydata.node", "allmydata.nodemaker", "allmydata.scripts.create_node", - "allmydata.scripts.runner", "allmydata.scripts.types_", "allmydata.stats", "allmydata.storage_client", From 939988a042116f85c8d9027086633b9ff2cd0f67 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 15 Feb 2021 15:13:56 -0500 Subject: [PATCH 26/30] Add workaround for compatibility on Python 2 where test.cli.test_create_alias expects the URL to be a byte string, broken in d02334bfd5ac. --- src/allmydata/scripts/common.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/allmydata/scripts/common.py b/src/allmydata/scripts/common.py index c10309491..bcaa84649 100644 --- a/src/allmydata/scripts/common.py +++ b/src/allmydata/scripts/common.py @@ -285,4 +285,8 @@ def escape_path(path): True """ segments = path.split("/") - return "/".join([urllib.parse.quote(unicode_to_url(s)) for s in segments]) + result = "/".join([urllib.parse.quote(unicode_to_url(s)) for s in segments]) + # fixme: test.cli.test_create_alias fails if it gets Unicode on Python 2 + if PY2 and isinstance(result, type(u'')): + result = result.encode('ascii') + return result From c673726139c52bfe34de7cf96f0a4b27f2ba2b2e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Mar 2021 14:43:44 -0500 Subject: [PATCH 27/30] Alongside unicode_to_argv, declare the argv type to avoid errors on Windows now that args are actually unicode. --- src/allmydata/test/common_util.py | 8 ++++---- src/allmydata/util/encodingutil.py | 9 +++++++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/allmydata/test/common_util.py b/src/allmydata/test/common_util.py index f898e75b2..95ea1fc02 100644 --- a/src/allmydata/test/common_util.py +++ b/src/allmydata/test/common_util.py @@ -1,6 +1,6 @@ from __future__ import print_function -from future.utils import PY2, native_str, bchr, binary_type +from future.utils import PY2, bchr, binary_type from future.builtins import str as future_str from past.builtins import unicode @@ -20,7 +20,7 @@ from twisted.trial import unittest from ..util.assertutil import precondition from ..scripts import runner -from allmydata.util.encodingutil import unicode_platform, get_filesystem_encoding, get_io_encoding +from allmydata.util.encodingutil import unicode_platform, get_filesystem_encoding, get_io_encoding, argv_type def skip_if_cannot_represent_filename(u): @@ -75,8 +75,8 @@ def run_cli_native(verb, *args, **kwargs): nodeargs = kwargs.pop("nodeargs", []) encoding = kwargs.pop("encoding", None) precondition( - all(isinstance(arg, native_str) for arg in [verb] + nodeargs + list(args)), - "arguments to run_cli must be a native string -- convert using unicode_to_argv", + all(isinstance(arg, argv_type) for arg in [verb] + nodeargs + list(args)), + "arguments to run_cli must be {argv_type} -- convert using unicode_to_argv".format(argv_type=argv_type), verb=verb, args=args, nodeargs=nodeargs, diff --git a/src/allmydata/util/encodingutil.py b/src/allmydata/util/encodingutil.py index 483871b5d..69e858e19 100644 --- a/src/allmydata/util/encodingutil.py +++ b/src/allmydata/util/encodingutil.py @@ -13,6 +13,7 @@ from __future__ import print_function from __future__ import unicode_literals from future.utils import PY2, PY3, native_str +from future.builtins import str as future_str if PY2: # We omit str() because that seems too tricky to get right. from builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, max, min # noqa: F401 @@ -142,6 +143,14 @@ def unicode_to_argv(s, mangle=False): return ensure_str(s) +# According to unicode_to_argv above, the expected type for +# cli args depends on the platform, so capture that expectation. +argv_type = future_str if sys.platform == "win32" else native_str +""" +The expected type for args to a subprocess +""" + + def unicode_to_url(s): """ Encode an unicode object used in an URL to bytes. From ee99c610b3a4bd57ae93ae8c557e4dca548635e4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Mar 2021 16:06:24 -0500 Subject: [PATCH 28/30] argv_type on Windows can be either --- src/allmydata/util/encodingutil.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/util/encodingutil.py b/src/allmydata/util/encodingutil.py index 69e858e19..637374064 100644 --- a/src/allmydata/util/encodingutil.py +++ b/src/allmydata/util/encodingutil.py @@ -145,7 +145,7 @@ def unicode_to_argv(s, mangle=False): # According to unicode_to_argv above, the expected type for # cli args depends on the platform, so capture that expectation. -argv_type = future_str if sys.platform == "win32" else native_str +argv_type = (future_str, native_str) if sys.platform == "win32" else native_str """ The expected type for args to a subprocess """ From 7c2a0685bb7ac5dea260ecec961f9ca47d010822 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Fri, 12 Mar 2021 11:22:19 -0500 Subject: [PATCH 29/30] Port test_create.py to Python 3. --- src/allmydata/test/cli/test_create.py | 12 ++++++++++++ src/allmydata/test/common_util.py | 12 +++++++++++- src/allmydata/util/_python3.py | 2 ++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/allmydata/test/cli/test_create.py b/src/allmydata/test/cli/test_create.py index aee07a671..282f26163 100644 --- a/src/allmydata/test/cli/test_create.py +++ b/src/allmydata/test/cli/test_create.py @@ -1,3 +1,15 @@ +""" +Ported to Python 3. +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function +from __future__ import unicode_literals + +from future.utils import PY2 +if PY2: + from future.builtins import filter, map, zip, ascii, chr, hex, input, next, oct, open, pow, round, super, bytes, dict, list, object, range, str, max, min # noqa: F401 + import os import mock from twisted.trial import unittest diff --git a/src/allmydata/test/common_util.py b/src/allmydata/test/common_util.py index 95ea1fc02..f62cd34cc 100644 --- a/src/allmydata/test/common_util.py +++ b/src/allmydata/test/common_util.py @@ -20,7 +20,7 @@ from twisted.trial import unittest from ..util.assertutil import precondition from ..scripts import runner -from allmydata.util.encodingutil import unicode_platform, get_filesystem_encoding, get_io_encoding, argv_type +from allmydata.util.encodingutil import unicode_platform, get_filesystem_encoding, get_io_encoding, argv_type, unicode_to_argv def skip_if_cannot_represent_filename(u): @@ -49,6 +49,13 @@ def _getvalue(io): return io.read() +def maybe_unicode_to_argv(o): + """Convert object to argv form if necessary.""" + if isinstance(o, unicode): + return unicode_to_argv(o) + return o + + def run_cli_native(verb, *args, **kwargs): """ Run a Tahoe-LAFS CLI command specified as bytes (on Python 2) or Unicode @@ -74,6 +81,9 @@ def run_cli_native(verb, *args, **kwargs): """ nodeargs = kwargs.pop("nodeargs", []) encoding = kwargs.pop("encoding", None) + verb = maybe_unicode_to_argv(verb) + args = [maybe_unicode_to_argv(a) for a in args] + nodeargs = [maybe_unicode_to_argv(a) for a in nodeargs] precondition( all(isinstance(arg, argv_type) for arg in [verb] + nodeargs + list(args)), "arguments to run_cli must be {argv_type} -- convert using unicode_to_argv".format(argv_type=argv_type), diff --git a/src/allmydata/util/_python3.py b/src/allmydata/util/_python3.py index f020ee8d7..5bcf64f0f 100644 --- a/src/allmydata/util/_python3.py +++ b/src/allmydata/util/_python3.py @@ -138,6 +138,8 @@ PORTED_MODULES = [ ] PORTED_TEST_MODULES = [ + "allmydata.test.cli.test_create", + "allmydata.test.mutable.test_checker", "allmydata.test.mutable.test_datahandle", "allmydata.test.mutable.test_different_encoding", From 68d342ee298ccb24f9698218871e6370f41c4963 Mon Sep 17 00:00:00 2001 From: Itamar Turner-Trauring Date: Fri, 12 Mar 2021 11:26:14 -0500 Subject: [PATCH 30/30] Get rid of trailing whitespace. --- src/allmydata/util/_python3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/allmydata/util/_python3.py b/src/allmydata/util/_python3.py index 5bcf64f0f..51dd8111d 100644 --- a/src/allmydata/util/_python3.py +++ b/src/allmydata/util/_python3.py @@ -139,7 +139,7 @@ PORTED_MODULES = [ PORTED_TEST_MODULES = [ "allmydata.test.cli.test_create", - + "allmydata.test.mutable.test_checker", "allmydata.test.mutable.test_datahandle", "allmydata.test.mutable.test_different_encoding",