Skip to content

Initial Python 3 compatibility fixes #23256

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 9 commits into from
Closed

Initial Python 3 compatibility fixes #23256

wants to merge 9 commits into from

Conversation

gwynne
Copy link
Contributor

@gwynne gwynne commented Mar 13, 2019

This PR adds compatibility with Python versions 2 and 3 for the following parts of Swift:

  • Various .gyb files in the stdlib
  • gyb
  • gyb_sourcekit_support
  • gyb_syntax_support
  • line-directive
  • update_checkout
  • build_swift
  • swift_build_support
  • bug_reducer
  • Several additional miscellaneous utilities
  • Initial support for the primary test driver
  • A few bits and pieces in the test suite, validation test suite, unit tests, and benchmarks.

Note: All of llvm and clang, as well as build-script, are already fully compatible with Python 3.

Note: An upstream issue with lit.py's Windows support is blocking Python 3 compatibility for the test suites at this time.

This PR does NOT provide complete Python 3 support; considerable further work is needed. Thoughts on whether this PR should be broken into multiple pieces and if so in what way would be most appreciated.

gwynne added 9 commits March 12, 2019 18:08
Note: Unlike Python 2, in Python 3 `unittest discover` loads the module, forcing the manual inclusion of a known path to swift_build_support in update_checkout
Note: Renamed `ParserError` to `ArgParserError` to avoid confusion with `parser` module in build_swift
Note: The syntax for declaring a metaclass changed between Python 2 and 3 in a severely incompatible way; it is not possible to (sanely) use the Python 3 syntax with a Python 2 interpreter. Here we take inspiration from the six module's `with_metaclass()` function to directly specify the metaclass in a way that, while ugly, is fully cross-compatible.
Note: Includes multiple whitespace and minor style changes to satisfy python_lint.py
Note: Retain old array shuffle by using random explicitly (Python 3 changed default algorithm) - test success depends on exact ordering, this seems wrong but fixing it is out of scope here.
@gwynne
Copy link
Contributor Author

gwynne commented Mar 13, 2019

Tests for this (beyond verifying the existing config stays working) will be tricky. The test suite doesn't run with Python 3 yet and I'm not aware of any infrastructure for Python 3 CI.

@gwynne
Copy link
Contributor Author

gwynne commented Mar 13, 2019

@swift-ci please smoke test

@compnerd
Copy link
Member

Nothing here stands out as an issue. Personally, I am a fan of separate changes when possible so that they can get merged earlier rather than later.

Copy link
Contributor

@drodriguez drodriguez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems that we are working in the same things and finding the same problems.

In #23038 I have many of this fixes. I wanted to break them apart per script, so it will be easier to review and accept, but all at the same time is also a good approach.

If you don’t mind, I will leave a couple of notes here and there.

I can review later which pieces I seem to have that these changes are missing, and see if they are interesting.

@@ -37,6 +38,9 @@ import time

from compare_perf_tests import LogParser

if not hasattr(sys.modules['__builtin__'], 'reduce'):
reduce = functools.reduce

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it is easier to use functools.reduce in both Python 2 and 3.

from functools import reduce

(This happens in a couple more places in other files, I will use the same solution there)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was learning as I went along, so didn't always pick up the best solutions, especially in some of my earlier changes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you adopt that throughout? It seems cleaner.

is_text = isinstance(msg, bytes)

try:
if is_text and getattr(stream, 'encoding', None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bytes in Python 3 do not have encoding. Maybe you wanted to compare with isinstance(msg, str)?

Python 3.6.3rc1+ (default, Feb  5 2019, 15:51:57)
[GCC 5.x 20180625 5.5.0+] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> b''.encode()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'bytes' object has no attribute 'encode'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remember correctly it was specifically a check to find out whether the attribute was present or not - keep in mind I was maintaining compatibility with both 2 and 3, not just updating to 3.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I mistyped. Corrected: bytes in Python 3 do not have encode(…). str does.

from StringIO import StringIO
try:
from StringIO import StringIO # py2
except ModuleNotFoundError:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: ModuleNotFoundError was introduced in Python 3.6. Python 2 should find StringIO just fine, but maybe you can change this with ImportError (the supertype), which should work in any Python 2 and 3.

(This also happens in more places below)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was getting no such module errors from Python 3 because StringIO is io.StringIO in 3.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I wanted to say is that ModuleNotFoundError was introduced in Python 3.6. ModuleNotFoundError is a subtype of ImportError. It will be more portable to use ImportError, so anything from Python 3.0 to Python 3.6 can import io.StringIO.

@@ -65,7 +71,7 @@ public struct SIMD${n}<Scalar> : SIMD where Scalar: SIMDScalar {
self.init(${', '.join('xyzw'[:n])})
}

% for i in range(n):
% for i in list(range(n)):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the three cases above, list(range(n)) can be just range(n). range returns a list in Python 2, and an iterator in Python 3. Both can be used directly by for … in ….

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, these shouldn't need any changes.

try:
xrange
except NameError:
xrange = range
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my version I simply change the xrange to range. In Py3, range works as Py2's xrange, and for the usages here, range in Py2 is not generating very long list, and the list is completely iterated. It makes the code easier to read, IMO. The only change was in L156.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tend to agree; there was some reason I went with this approach that I've forgotten now

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, please just use range for these cases; I think there's only like one instance of xrange in this file anyway, it should just be converted to keep the diffs minimal.

quote(local_file))
for local_file, remote_file
in local_to_remote_files.viewitems())
self.run_sftp(sftp_commands)

def run_remote(self, command, remote_env={}):
env_strings = ['{0}={1}'.format(k,v) for k,v in remote_env.viewitems()]
env_strings = ['{0}={1}'.format(k,v) for k,v in remote_env.items()]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides this viewitems, there's a viewvalues in L44, another viewitems in L51 and L67, a viewkeys in L56. Didn’t you have problems with those in Python 3?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know that I ever finished getting this particular script working 😅

@@ -69,7 +69,7 @@ def write_input_file(args, ast, d, n):

def ensure_tmpdir(d):
if d is not None and not os.path.exists(d):
os.makedirs(d, 0700)
os.makedirs(d, 0o700)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found another one in L84 as well.

@@ -73,7 +73,7 @@ def test_capture(self):

self.assertEqual(
shell.capture(["sh", "-c", "echo foo && false"],
allow_non_zero_exit=True), "foo\n")
allow_non_zero_exit=True).decode('ascii'), "foo\n")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I solved this with a b"foo\n" instead.

@@ -1,4 +1,3 @@

from update_checkout import main
from update_checkout.update_checkout import main
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you don't want the absolute import, it can also be done with from .update_checkout import main.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is another case of "the only solution I found that worked in both 2 and 3"

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from .update_checkout import main

__all__ = ["main"]

Just to let you know that worked when Im trying to compile the tensorflow branch... dont know if compatible with python2, changint the 2 __init__.py files for from update_checkout.update_checkout import main seem to not work on my end??

SCRIPT_FILE = os.path.abspath(__file__)
SCRIPT_DIR = os.path.dirname(SCRIPT_FILE)
sys.path.append(os.path.dirname(os.path.dirname(SCRIPT_DIR)))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this for fixing the test update_checkout.test-sh? I changed the test instead to provide a working directory with the -t parameter.

# RUN: %{python} -m unittest discover -s %swift_src_root/utils/update_checkout -t %swift_src_root/utils/

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was to deal with importing swift_build_support, which lives one directory up from update-checkout and wasn't found by the default paths in 3.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I understand why you add it. In my tests with Python 3, when running the script by itself, I didn’t need any changes here. But when running from the tests, I found out that the modules weren’t in the search path. I used the solution I pointed above, since it doesn’t modify the code for all cases.

num_iters=1, quantile=20)
for _ in range(self.args.independent_samples)])
for _ in range(self.args.independent_samples)]
return reduce(lambda m, res: (m.merge(res), m)[1], result_sets)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The original version of this seems less magical and easier for non-python folks to grok.

@@ -414,6 +418,7 @@ class BenchmarkDoctor(object):
name = measurements['name']
setup, ratio = BenchmarkDoctor._setup_overhead(measurements)
setup = 0 if ratio < 0.05 else setup

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove whitespace-only changes.

@@ -111,7 +111,7 @@ public func >=(lhs: (), rhs: ()) -> Bool {
% equatableTypeParams = ", ".join(["{} : Equatable".format(c) for c in typeParams])

% originalTuple = "(\"a\", {})".format(", ".join(map(str, range(1, arity))))
% greaterTuple = "(\"a\", {})".format(", ".join(map(str, range(1, arity - 1) + [arity])))
% greaterTuple = "(\"a\", {})".format(", ".join(map(str, list(range(1, arity - 1)) + [arity])))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't need this change, either.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Python 3 definitely went bananas without this change.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this change is needed. In Python 3, range will return an iterable, which cannot be added a list. The list(…) transforms the iterable into a list, which will work.

Copy link

@tyoc213 tyoc213 Apr 7, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, because if you enter arity=3; "(\"a\", {})".format(", ".join(map(str, range(1, arity - 1) + [arity]))) to an interpreter it will cause TypeError: unsupported operand type(s) for +: 'range' and 'list' also https://stackoverflow.com/a/47472763 which leads to xrange() was renamed to range() in Python 3 and range(...) replaced with list(range(...))

@shahmishal
Copy link
Member

Please update the base branch to main by Oct 5th otherwise the pull request will be closed automatically.

  • How to change the base branch: (Link)
  • More detail about the branch update: (Link)

@shahmishal shahmishal closed this Oct 5, 2020
@gwynne gwynne deleted the gwynne/python-3-compatibility branch April 9, 2023 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants