##// END OF EJS Templates
tests: make check-py3-compat.py actually load the specified files correctly...
Kyle Lippincott -
r45776:4e5da64d default
parent child Browse files
Show More
@@ -1,113 +1,122 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # check-py3-compat - check Python 3 compatibility of Mercurial files
3 # check-py3-compat - check Python 3 compatibility of Mercurial files
4 #
4 #
5 # Copyright 2015 Gregory Szorc <gregory.szorc@gmail.com>
5 # Copyright 2015 Gregory Szorc <gregory.szorc@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import, print_function
10 from __future__ import absolute_import, print_function
11
11
12 import ast
12 import ast
13 import importlib
13 import importlib
14 import os
14 import os
15 import sys
15 import sys
16 import traceback
16 import traceback
17 import warnings
17 import warnings
18
18
19
19
20 def check_compat_py2(f):
20 def check_compat_py2(f):
21 """Check Python 3 compatibility for a file with Python 2"""
21 """Check Python 3 compatibility for a file with Python 2"""
22 with open(f, 'rb') as fh:
22 with open(f, 'rb') as fh:
23 content = fh.read()
23 content = fh.read()
24 root = ast.parse(content)
24 root = ast.parse(content)
25
25
26 # Ignore empty files.
26 # Ignore empty files.
27 if not root.body:
27 if not root.body:
28 return
28 return
29
29
30 futures = set()
30 futures = set()
31 haveprint = False
31 haveprint = False
32 for node in ast.walk(root):
32 for node in ast.walk(root):
33 if isinstance(node, ast.ImportFrom):
33 if isinstance(node, ast.ImportFrom):
34 if node.module == '__future__':
34 if node.module == '__future__':
35 futures |= {n.name for n in node.names}
35 futures |= {n.name for n in node.names}
36 elif isinstance(node, ast.Print):
36 elif isinstance(node, ast.Print):
37 haveprint = True
37 haveprint = True
38
38
39 if 'absolute_import' not in futures:
39 if 'absolute_import' not in futures:
40 print('%s not using absolute_import' % f)
40 print('%s not using absolute_import' % f)
41 if haveprint and 'print_function' not in futures:
41 if haveprint and 'print_function' not in futures:
42 print('%s requires print_function' % f)
42 print('%s requires print_function' % f)
43
43
44
44
45 def check_compat_py3(f):
45 def check_compat_py3(f):
46 """Check Python 3 compatibility of a file with Python 3."""
46 """Check Python 3 compatibility of a file with Python 3."""
47 with open(f, 'rb') as fh:
47 with open(f, 'rb') as fh:
48 content = fh.read()
48 content = fh.read()
49
49
50 try:
50 try:
51 ast.parse(content, filename=f)
51 ast.parse(content, filename=f)
52 except SyntaxError as e:
52 except SyntaxError as e:
53 print('%s: invalid syntax: %s' % (f, e))
53 print('%s: invalid syntax: %s' % (f, e))
54 return
54 return
55
55
56 # Try to import the module.
56 # Try to import the module.
57 # For now we only support modules in packages because figuring out module
57 # For now we only support modules in packages because figuring out module
58 # paths for things not in a package can be confusing.
58 # paths for things not in a package can be confusing.
59 if f.startswith(
59 if f.startswith(
60 ('hgdemandimport/', 'hgext/', 'mercurial/')
60 ('hgdemandimport/', 'hgext/', 'mercurial/')
61 ) and not f.endswith('__init__.py'):
61 ) and not f.endswith('__init__.py'):
62 assert f.endswith('.py')
62 assert f.endswith('.py')
63 name = f.replace('/', '.')[:-3]
63 name = f.replace('/', '.')[:-3]
64 try:
64 try:
65 importlib.import_module(name)
65 importlib.import_module(name)
66 except Exception as e:
66 except Exception as e:
67 exc_type, exc_value, tb = sys.exc_info()
67 exc_type, exc_value, tb = sys.exc_info()
68 # We walk the stack and ignore frames from our custom importer,
68 # We walk the stack and ignore frames from our custom importer,
69 # import mechanisms, and stdlib modules. This kinda/sorta
69 # import mechanisms, and stdlib modules. This kinda/sorta
70 # emulates CPython behavior in import.c while also attempting
70 # emulates CPython behavior in import.c while also attempting
71 # to pin blame on a Mercurial file.
71 # to pin blame on a Mercurial file.
72 for frame in reversed(traceback.extract_tb(tb)):
72 for frame in reversed(traceback.extract_tb(tb)):
73 if frame.name == '_call_with_frames_removed':
73 if frame.name == '_call_with_frames_removed':
74 continue
74 continue
75 if 'importlib' in frame.filename:
75 if 'importlib' in frame.filename:
76 continue
76 continue
77 if 'mercurial/__init__.py' in frame.filename:
77 if 'mercurial/__init__.py' in frame.filename:
78 continue
78 continue
79 if frame.filename.startswith(sys.prefix):
79 if frame.filename.startswith(sys.prefix):
80 continue
80 continue
81 break
81 break
82
82
83 if frame.filename:
83 if frame.filename:
84 filename = os.path.basename(frame.filename)
84 filename = os.path.basename(frame.filename)
85 print(
85 print(
86 '%s: error importing: <%s> %s (error at %s:%d)'
86 '%s: error importing: <%s> %s (error at %s:%d)'
87 % (f, type(e).__name__, e, filename, frame.lineno)
87 % (f, type(e).__name__, e, filename, frame.lineno)
88 )
88 )
89 else:
89 else:
90 print(
90 print(
91 '%s: error importing module: <%s> %s (line %d)'
91 '%s: error importing module: <%s> %s (line %d)'
92 % (f, type(e).__name__, e, frame.lineno)
92 % (f, type(e).__name__, e, frame.lineno)
93 )
93 )
94
94
95
95
96 if __name__ == '__main__':
96 if __name__ == '__main__':
97 if sys.version_info[0] == 2:
97 if sys.version_info[0] == 2:
98 fn = check_compat_py2
98 fn = check_compat_py2
99 else:
99 else:
100 # check_compat_py3 will import every filename we specify as long as it
101 # starts with one of a few prefixes. It does this by converting
102 # specified filenames like 'mercurial/foo.py' to 'mercurial.foo' and
103 # importing that. When running standalone (not as part of a test), this
104 # means we actually import the installed versions, not the files we just
105 # specified. When running as test-check-py3-compat.t, we technically
106 # would import the correct paths, but it's cleaner to have both cases
107 # use the same import logic.
108 sys.path.insert(0, '.')
100 fn = check_compat_py3
109 fn = check_compat_py3
101
110
102 for f in sys.argv[1:]:
111 for f in sys.argv[1:]:
103 with warnings.catch_warnings(record=True) as warns:
112 with warnings.catch_warnings(record=True) as warns:
104 fn(f)
113 fn(f)
105
114
106 for w in warns:
115 for w in warns:
107 print(
116 print(
108 warnings.formatwarning(
117 warnings.formatwarning(
109 w.message, w.category, w.filename, w.lineno
118 w.message, w.category, w.filename, w.lineno
110 ).rstrip()
119 ).rstrip()
111 )
120 )
112
121
113 sys.exit(0)
122 sys.exit(0)
General Comments 0
You need to be logged in to leave comments. Login now