Improve support for automatic 2to3 conversion of config files. It now kicks in whenever the original file raises SyntaxErrors on compiling.

This commit is contained in:
Georg Brandl 2010-07-31 19:47:15 +02:00
parent 86cd745dc1
commit 0e84c75822
3 changed files with 45 additions and 63 deletions

View File

@ -13,14 +13,10 @@ import os
import re import re
import sys import sys
from os import path from os import path
try:
from distutils.util import run_2to3
except ImportError:
run_2to3 = None
from sphinx.errors import ConfigError from sphinx.errors import ConfigError
from sphinx.util.osutil import make_filename from sphinx.util.osutil import make_filename
from sphinx.util.pycompat import bytes, b, should_run_2to3, run_2to3 from sphinx.util.pycompat import bytes, b, convert_with_2to3
nonascii_re = re.compile(b(r'[\x80-\xff]')) nonascii_re = re.compile(b(r'[\x80-\xff]'))
@ -172,17 +168,28 @@ class Config(object):
config['tags'] = tags config['tags'] = tags
olddir = os.getcwd() olddir = os.getcwd()
try: try:
# we promise to have the config dir as current dir while the
# config file is executed
os.chdir(dirname)
# get config source
f = open(config_file, 'rb')
try: try:
os.chdir(dirname) source = f.read()
if should_run_2to3(config_file): finally:
code = run_2to3(config_file) f.close()
else: try:
f = open(config_file, 'rb') # compile to a code object, handle syntax errors
try: try:
code = f.read() code = compile(source, config_file, 'exec')
finally: except SyntaxError:
f.close() if convert_with_2to3:
exec compile(code, config_file, 'exec') in config # maybe the file uses 2.x syntax; try to refactor to
# 3.x syntax using 2to3
source = convert_with_2to3(config_file)
code = compile(source, config_file, 'exec')
else:
raise
exec code in config
except SyntaxError, err: except SyntaxError, err:
raise ConfigError(CONFIG_SYNTAX_ERROR % err) raise ConfigError(CONFIG_SYNTAX_ERROR % err)
finally: finally:

View File

@ -30,58 +30,26 @@ else:
b = str b = str
encoding_re = re.compile(b(r'coding[=:]\s*([-\w.]+)')) # Support for running 2to3 over config files
unicode_literal_re = re.compile(ur"""
(?:
"(?:[^"\]]*(?:\\.[^"\\]*)*)"|
'(?:[^'\]]*(?:\\.[^'\\]*)*)'
)
""", re.VERBOSE)
if sys.version_info < (3, 0):
try: # no need to refactor on 2.x versions
from lib2to3.refactor import RefactoringTool, get_fixers_from_package convert_with_2to3 = None
except ImportError:
_run_2to3 = None
def should_run_2to3(filepath):
return False
else: else:
def should_run_2to3(filepath): def convert_with_2to3(filepath):
# th default source code encoding for python 2.x from lib2to3.refactor import RefactoringTool, get_fixers_from_package
encoding = 'ascii' from lib2to3.pgen2.parse import ParseError
# only the first match of the encoding cookie counts
encoding_set = False
f = open(filepath, 'rb')
try:
for i, line in enumerate(f):
if line.startswith(b('#')):
if i == 0 and b('python3') in line:
return False
if not encoding_set:
encoding_match = encoding_re.match(line)
if encoding_match:
encoding = encoding_match.group(1)
encodin_set = True
elif line.strip():
try:
line = line.decode(encoding)
except UnicodeDecodeError:
# I'm not sure this will work but let's try it anyway
return True
if unicode_literal_re.search(line) is not None:
return True
finally:
f.close()
return False
def run_2to3(filepath):
sys.path.append('..')
fixers = get_fixers_from_package('lib2to3.fixes') fixers = get_fixers_from_package('lib2to3.fixes')
fixers.extend(get_fixers_from_package('custom_fixers'))
refactoring_tool = RefactoringTool(fixers) refactoring_tool = RefactoringTool(fixers)
source = refactoring_tool._read_python_source(filepath)[0] source = refactoring_tool._read_python_source(filepath)[0]
ast = refactoring_tool.refactor_string(source, 'conf.py') try:
return unicode(ast) tree = refactoring_tool.refactor_string(source, 'conf.py')
except ParseError, err:
# do not propagate lib2to3 exceptions
lineno, offset = err.context[1]
# try to match ParseError details with SyntaxError details
raise SyntaxError(err.msg, (filepath, lineno, offset, err.value))
return unicode(tree)
try: try:
@ -93,7 +61,8 @@ except NameError:
try: try:
next = next next = next
except NameError: except NameError:
# this is on Python 2, where the method is called "next" # this is on Python 2, where the method is called "next" (it is refactored
# to __next__ by 2to3, but in that case never executed)
def next(iterator): def next(iterator):
return iterator.next() return iterator.next()

View File

@ -88,6 +88,12 @@ def test_errors_warnings(dir):
write_file(dir / 'conf.py', u'project = \n', 'ascii') write_file(dir / 'conf.py', u'project = \n', 'ascii')
raises_msg(ConfigError, 'conf.py', Config, dir, 'conf.py', {}, None) raises_msg(ConfigError, 'conf.py', Config, dir, 'conf.py', {}, None)
# test the automatic conversion of 2.x only code in configs
write_file(dir / 'conf.py', u'\n\nproject = u"Jägermeister"\n', 'utf-8')
cfg = Config(dir, 'conf.py', {}, None)
cfg.init_values()
assert cfg.project == u'Jägermeister'
# test the warning for bytestrings with non-ascii content # test the warning for bytestrings with non-ascii content
# bytestrings with non-ascii content are a syntax error in python3 so we # bytestrings with non-ascii content are a syntax error in python3 so we
# skip the test there # skip the test there