Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit ba1cc50

Browse files
authoredFeb 21, 2024
Merge pull request #632 from nsoranzo/python312-imp-module_past
Python 3.12 support
2 parents a6222d2 + 9ef05b3 commit ba1cc50

File tree

12 files changed

+152
-220
lines changed

12 files changed

+152
-220
lines changed
 

‎setup.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@
103103
"Programming Language :: Python :: 3.5",
104104
"Programming Language :: Python :: 3.6",
105105
"Programming Language :: Python :: 3.7",
106+
"Programming Language :: Python :: 3.8",
107+
"Programming Language :: Python :: 3.9",
108+
"Programming Language :: Python :: 3.10",
109+
"Programming Language :: Python :: 3.11",
110+
"Programming Language :: Python :: 3.12",
106111
"License :: OSI Approved",
107112
"License :: OSI Approved :: MIT License",
108113
"Development Status :: 4 - Beta",

‎src/future/backports/test/support.py

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,6 @@
2828
# import collections.abc # not present on Py2.7
2929
import re
3030
import subprocess
31-
try:
32-
from imp import cache_from_source
33-
except ImportError:
34-
from importlib.util import cache_from_source
3531
import time
3632
try:
3733
import sysconfig
@@ -344,37 +340,6 @@ def rmtree(path):
344340
if error.errno != errno.ENOENT:
345341
raise
346342

347-
def make_legacy_pyc(source):
348-
"""Move a PEP 3147 pyc/pyo file to its legacy pyc/pyo location.
349-
350-
The choice of .pyc or .pyo extension is done based on the __debug__ flag
351-
value.
352-
353-
:param source: The file system path to the source file. The source file
354-
does not need to exist, however the PEP 3147 pyc file must exist.
355-
:return: The file system path to the legacy pyc file.
356-
"""
357-
pyc_file = cache_from_source(source)
358-
up_one = os.path.dirname(os.path.abspath(source))
359-
legacy_pyc = os.path.join(up_one, source + ('c' if __debug__ else 'o'))
360-
os.rename(pyc_file, legacy_pyc)
361-
return legacy_pyc
362-
363-
def forget(modname):
364-
"""'Forget' a module was ever imported.
365-
366-
This removes the module from sys.modules and deletes any PEP 3147 or
367-
legacy .pyc and .pyo files.
368-
"""
369-
unload(modname)
370-
for dirname in sys.path:
371-
source = os.path.join(dirname, modname + '.py')
372-
# It doesn't matter if they exist or not, unlink all possible
373-
# combinations of PEP 3147 and legacy pyc and pyo files.
374-
unlink(source + 'c')
375-
unlink(source + 'o')
376-
unlink(cache_from_source(source, debug_override=True))
377-
unlink(cache_from_source(source, debug_override=False))
378343

379344
# On some platforms, should not run gui test even if it is allowed
380345
# in `use_resources'.

‎src/future/moves/test/support.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
from __future__ import absolute_import
2+
3+
import sys
4+
25
from future.standard_library import suspend_hooks
36
from future.utils import PY3
47

58
if PY3:
69
from test.support import *
10+
if sys.version_info[:2] >= (3, 10):
11+
from test.support.os_helper import (
12+
EnvironmentVarGuard,
13+
TESTFN,
14+
)
15+
from test.support.warnings_helper import check_warnings
716
else:
817
__future_module__ = True
918
with suspend_hooks():

‎src/future/standard_library/__init__.py

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,7 @@
6262

6363
import sys
6464
import logging
65-
try:
66-
import importlib
67-
except ImportError:
68-
import imp
6965
import contextlib
70-
import types
7166
import copy
7267
import os
7368

@@ -82,6 +77,9 @@
8277

8378
from future.utils import PY2, PY3
8479

80+
if PY2:
81+
import imp
82+
8583
# The modules that are defined under the same names on Py3 but with
8684
# different contents in a significant way (e.g. submodules) are:
8785
# pickle (fast one)
@@ -300,11 +298,8 @@ def _find_and_load_module(self, name, path=None):
300298
flog.debug('What to do here?')
301299

302300
name = bits[0]
303-
try:
304-
module_info = imp.find_module(name, path)
305-
return imp.load_module(name, *module_info)
306-
except AttributeError:
307-
return importlib.import_module(name, path)
301+
module_info = imp.find_module(name, path)
302+
return imp.load_module(name, *module_info)
308303

309304

310305
class hooks(object):

‎src/past/translation/__init__.py

Lines changed: 107 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,7 @@
3232
Inspired by and based on ``uprefix`` by Vinay M. Sajip.
3333
"""
3434

35-
try:
36-
import imp
37-
except ImportError:
38-
import importlib
3935
import logging
40-
import marshal
4136
import os
4237
import sys
4338
import copy
@@ -46,6 +41,17 @@
4641

4742
from libfuturize import fixes
4843

44+
try:
45+
from importlib.machinery import (
46+
PathFinder,
47+
SourceFileLoader,
48+
)
49+
except ImportError:
50+
PathFinder = None
51+
SourceFileLoader = object
52+
53+
if sys.version_info[:2] < (3, 4):
54+
import imp
4955

5056
logger = logging.getLogger(__name__)
5157
logger.setLevel(logging.DEBUG)
@@ -228,6 +234,81 @@ def detect_python2(source, pathname):
228234
return False
229235

230236

237+
def transform(source, pathname):
238+
# This implementation uses lib2to3,
239+
# you can override and use something else
240+
# if that's better for you
241+
242+
# lib2to3 likes a newline at the end
243+
RTs.setup()
244+
source += '\n'
245+
try:
246+
tree = RTs._rt.refactor_string(source, pathname)
247+
except ParseError as e:
248+
if e.msg != 'bad input' or e.value != '=':
249+
raise
250+
tree = RTs._rtp.refactor_string(source, pathname)
251+
# could optimise a bit for only doing str(tree) if
252+
# getattr(tree, 'was_changed', False) returns True
253+
return str(tree)[:-1] # remove added newline
254+
255+
256+
class PastSourceFileLoader(SourceFileLoader):
257+
exclude_paths = []
258+
include_paths = []
259+
260+
def _convert_needed(self):
261+
fullname = self.name
262+
if any(fullname.startswith(path) for path in self.exclude_paths):
263+
convert = False
264+
elif any(fullname.startswith(path) for path in self.include_paths):
265+
convert = True
266+
else:
267+
convert = False
268+
return convert
269+
270+
def _exec_transformed_module(self, module):
271+
source = self.get_source(self.name)
272+
pathname = self.path
273+
if detect_python2(source, pathname):
274+
source = transform(source, pathname)
275+
code = compile(source, pathname, "exec")
276+
exec(code, module.__dict__)
277+
278+
# For Python 3.3
279+
def load_module(self, fullname):
280+
logger.debug("Running load_module for %s", fullname)
281+
if fullname in sys.modules:
282+
mod = sys.modules[fullname]
283+
else:
284+
if self._convert_needed():
285+
logger.debug("Autoconverting %s", fullname)
286+
mod = imp.new_module(fullname)
287+
sys.modules[fullname] = mod
288+
289+
# required by PEP 302
290+
mod.__file__ = self.path
291+
mod.__loader__ = self
292+
if self.is_package(fullname):
293+
mod.__path__ = []
294+
mod.__package__ = fullname
295+
else:
296+
mod.__package__ = fullname.rpartition('.')[0]
297+
self._exec_transformed_module(mod)
298+
else:
299+
mod = super().load_module(fullname)
300+
return mod
301+
302+
# For Python >=3.4
303+
def exec_module(self, module):
304+
logger.debug("Running exec_module for %s", module)
305+
if self._convert_needed():
306+
logger.debug("Autoconverting %s", self.name)
307+
self._exec_transformed_module(module)
308+
else:
309+
super().exec_module(module)
310+
311+
231312
class Py2Fixer(object):
232313
"""
233314
An import hook class that uses lib2to3 for source-to-source translation of
@@ -261,151 +342,30 @@ def exclude(self, paths):
261342
"""
262343
self.exclude_paths += paths
263344

345+
# For Python 3.3
264346
def find_module(self, fullname, path=None):
265-
logger.debug('Running find_module: {0}...'.format(fullname))
266-
if '.' in fullname:
267-
parent, child = fullname.rsplit('.', 1)
268-
if path is None:
269-
loader = self.find_module(parent, path)
270-
mod = loader.load_module(parent)
271-
path = mod.__path__
272-
fullname = child
273-
274-
# Perhaps we should try using the new importlib functionality in Python
275-
# 3.3: something like this?
276-
# thing = importlib.machinery.PathFinder.find_module(fullname, path)
277-
try:
278-
self.found = imp.find_module(fullname, path)
279-
except Exception as e:
280-
logger.debug('Py2Fixer could not find {0}')
281-
logger.debug('Exception was: {0})'.format(fullname, e))
347+
logger.debug("Running find_module: (%s, %s)", fullname, path)
348+
loader = PathFinder.find_module(fullname, path)
349+
if not loader:
350+
logger.debug("Py2Fixer could not find %s", fullname)
282351
return None
283-
self.kind = self.found[-1][-1]
284-
if self.kind == imp.PKG_DIRECTORY:
285-
self.pathname = os.path.join(self.found[1], '__init__.py')
286-
elif self.kind == imp.PY_SOURCE:
287-
self.pathname = self.found[1]
288-
return self
289-
290-
def transform(self, source):
291-
# This implementation uses lib2to3,
292-
# you can override and use something else
293-
# if that's better for you
294-
295-
# lib2to3 likes a newline at the end
296-
RTs.setup()
297-
source += '\n'
298-
try:
299-
tree = RTs._rt.refactor_string(source, self.pathname)
300-
except ParseError as e:
301-
if e.msg != 'bad input' or e.value != '=':
302-
raise
303-
tree = RTs._rtp.refactor_string(source, self.pathname)
304-
# could optimise a bit for only doing str(tree) if
305-
# getattr(tree, 'was_changed', False) returns True
306-
return str(tree)[:-1] # remove added newline
307-
308-
def load_module(self, fullname):
309-
logger.debug('Running load_module for {0}...'.format(fullname))
310-
if fullname in sys.modules:
311-
mod = sys.modules[fullname]
312-
else:
313-
if self.kind in (imp.PY_COMPILED, imp.C_EXTENSION, imp.C_BUILTIN,
314-
imp.PY_FROZEN):
315-
convert = False
316-
# elif (self.pathname.startswith(_stdlibprefix)
317-
# and 'site-packages' not in self.pathname):
318-
# # We assume it's a stdlib package in this case. Is this too brittle?
319-
# # Please file a bug report at https://github.com/PythonCharmers/python-future
320-
# # if so.
321-
# convert = False
322-
# in theory, other paths could be configured to be excluded here too
323-
elif any([fullname.startswith(path) for path in self.exclude_paths]):
324-
convert = False
325-
elif any([fullname.startswith(path) for path in self.include_paths]):
326-
convert = True
327-
else:
328-
convert = False
329-
if not convert:
330-
logger.debug('Excluded {0} from translation'.format(fullname))
331-
mod = imp.load_module(fullname, *self.found)
332-
else:
333-
logger.debug('Autoconverting {0} ...'.format(fullname))
334-
mod = imp.new_module(fullname)
335-
sys.modules[fullname] = mod
336-
337-
# required by PEP 302
338-
mod.__file__ = self.pathname
339-
mod.__name__ = fullname
340-
mod.__loader__ = self
341-
342-
# This:
343-
# mod.__package__ = '.'.join(fullname.split('.')[:-1])
344-
# seems to result in "SystemError: Parent module '' not loaded,
345-
# cannot perform relative import" for a package's __init__.py
346-
# file. We use the approach below. Another option to try is the
347-
# minimal load_module pattern from the PEP 302 text instead.
348-
349-
# Is the test in the next line more or less robust than the
350-
# following one? Presumably less ...
351-
# ispkg = self.pathname.endswith('__init__.py')
352-
353-
if self.kind == imp.PKG_DIRECTORY:
354-
mod.__path__ = [ os.path.dirname(self.pathname) ]
355-
mod.__package__ = fullname
356-
else:
357-
#else, regular module
358-
mod.__path__ = []
359-
mod.__package__ = fullname.rpartition('.')[0]
352+
loader.__class__ = PastSourceFileLoader
353+
loader.exclude_paths = self.exclude_paths
354+
loader.include_paths = self.include_paths
355+
return loader
356+
357+
# For Python >=3.4
358+
def find_spec(self, fullname, path=None, target=None):
359+
logger.debug("Running find_spec: (%s, %s, %s)", fullname, path, target)
360+
spec = PathFinder.find_spec(fullname, path, target)
361+
if not spec:
362+
logger.debug("Py2Fixer could not find %s", fullname)
363+
return None
364+
spec.loader.__class__ = PastSourceFileLoader
365+
spec.loader.exclude_paths = self.exclude_paths
366+
spec.loader.include_paths = self.include_paths
367+
return spec
360368

361-
try:
362-
cachename = imp.cache_from_source(self.pathname)
363-
if not os.path.exists(cachename):
364-
update_cache = True
365-
else:
366-
sourcetime = os.stat(self.pathname).st_mtime
367-
cachetime = os.stat(cachename).st_mtime
368-
update_cache = cachetime < sourcetime
369-
# # Force update_cache to work around a problem with it being treated as Py3 code???
370-
# update_cache = True
371-
if not update_cache:
372-
with open(cachename, 'rb') as f:
373-
data = f.read()
374-
try:
375-
code = marshal.loads(data)
376-
except Exception:
377-
# pyc could be corrupt. Regenerate it
378-
update_cache = True
379-
if update_cache:
380-
if self.found[0]:
381-
source = self.found[0].read()
382-
elif self.kind == imp.PKG_DIRECTORY:
383-
with open(self.pathname) as f:
384-
source = f.read()
385-
386-
if detect_python2(source, self.pathname):
387-
source = self.transform(source)
388-
389-
code = compile(source, self.pathname, 'exec')
390-
391-
dirname = os.path.dirname(cachename)
392-
try:
393-
if not os.path.exists(dirname):
394-
os.makedirs(dirname)
395-
with open(cachename, 'wb') as f:
396-
data = marshal.dumps(code)
397-
f.write(data)
398-
except Exception: # could be write-protected
399-
pass
400-
exec(code, mod.__dict__)
401-
except Exception as e:
402-
# must remove module from sys.modules
403-
del sys.modules[fullname]
404-
raise # keep it simple
405-
406-
if self.found[0]:
407-
self.found[0].close()
408-
return mod
409369

410370
_hook = Py2Fixer()
411371

‎tests/test_future/test_builtins.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,8 +1303,11 @@ def test_pow(self):
13031303
self.assertAlmostEqual(pow(-1, 0.5), 1j)
13041304
self.assertAlmostEqual(pow(-1, 1/3), 0.5 + 0.8660254037844386j)
13051305

1306-
# Raises TypeError in Python < v3.5, ValueError in v3.5:
1307-
self.assertRaises((TypeError, ValueError), pow, -1, -2, 3)
1306+
# Raises TypeError in Python < v3.5, ValueError in v3.5-v3.7:
1307+
if sys.version_info[:2] < (3, 8):
1308+
self.assertRaises((TypeError, ValueError), pow, -1, -2, 3)
1309+
else:
1310+
self.assertEqual(pow(-1, -2, 3), 1)
13081311
self.assertRaises(ValueError, pow, 1, 2, 0)
13091312

13101313
self.assertRaises(TypeError, pow)

‎tests/test_future/test_standard_library.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
import sys
1111
import tempfile
12-
import os
1312
import copy
1413
import textwrap
1514
from subprocess import CalledProcessError
@@ -448,13 +447,11 @@ def test_reload(self):
448447
reload has been moved to the imp module
449448
"""
450449
try:
451-
import imp
452-
imp.reload(imp)
453-
self.assertTrue(True)
450+
from importlib import reload
454451
except ImportError:
455-
import importlib
456-
importlib.reload(importlib)
457-
self.assertTrue(True)
452+
from imp import reload
453+
reload(sys)
454+
self.assertTrue(True)
458455

459456
def test_install_aliases(self):
460457
"""

‎tests/test_future/test_urllib2.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -691,10 +691,6 @@ def connect_ftp(self, user, passwd, host, port, dirs,
691691
h = NullFTPHandler(data)
692692
h.parent = MockOpener()
693693

694-
# MIME guessing works in Python 3.8!
695-
guessed_mime = None
696-
if sys.hexversion >= 0x03080000:
697-
guessed_mime = "image/gif"
698694
for url, host, port, user, passwd, type_, dirs, filename, mimetype in [
699695
("ftp://localhost/foo/bar/baz.html",
700696
"localhost", ftplib.FTP_PORT, "", "", "I",
@@ -713,7 +709,7 @@ def connect_ftp(self, user, passwd, host, port, dirs,
713709
["foo", "bar"], "", None),
714710
("ftp://localhost/baz.gif;type=a",
715711
"localhost", ftplib.FTP_PORT, "", "", "A",
716-
[], "baz.gif", guessed_mime),
712+
[], "baz.gif", None),
717713
]:
718714
req = Request(url)
719715
req.timeout = None

‎tests/test_future/test_urllib_toplevel.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -781,8 +781,11 @@ def test_unquoting(self):
781781
"%s" % result)
782782
self.assertRaises((TypeError, AttributeError), urllib_parse.unquote, None)
783783
self.assertRaises((TypeError, AttributeError), urllib_parse.unquote, ())
784-
with support.check_warnings(('', BytesWarning), quiet=True):
785-
self.assertRaises((TypeError, AttributeError), urllib_parse.unquote, bytes(b''))
784+
if sys.version_info[:2] < (3, 9):
785+
with support.check_warnings(('', BytesWarning), quiet=True):
786+
self.assertRaises((TypeError, AttributeError), urllib_parse.unquote, bytes(b''))
787+
else:
788+
self.assertEqual(urllib_parse.unquote(bytes(b"")), "")
786789

787790
def test_unquoting_badpercent(self):
788791
# Test unquoting on bad percent-escapes

‎tests/test_future/test_utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ class Timeout(BaseException):
150150
self.assertRaises(Timeout, raise_, Timeout())
151151

152152
if PY3:
153-
self.assertRaisesRegexp(
153+
self.assertRaisesRegex(
154154
TypeError, "class must derive from BaseException",
155155
raise_, int)
156156

‎tests/test_past/test_builtins.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
from past.builtins import apply, cmp, execfile, intern, raw_input
77
from past.builtins import reduce, reload, unichr, unicode, xrange
88

9-
from future import standard_library
109
from future.backports.test.support import TESTFN #, run_unittest
1110
import tempfile
1211
import os

‎tests/test_past/test_translation.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@
77
import os
88
import textwrap
99
import sys
10-
import pprint
1110
import tempfile
12-
import os
1311
import io
14-
from subprocess import Popen, PIPE
15-
16-
from past import utils
17-
from past.builtins import basestring, str as oldstr, unicode
1812

13+
from future.tests.base import (
14+
expectedFailurePY3,
15+
unittest,
16+
)
17+
from past.builtins import (
18+
str as oldstr,
19+
unicode,
20+
)
1921
from past.translation import install_hooks, remove_hooks, common_substring
20-
from future.tests.base import (unittest, CodeHandler, skip26,
21-
expectedFailurePY3, expectedFailurePY26)
2222

2323

2424
class TestTranslate(unittest.TestCase):
@@ -58,8 +58,8 @@ def write_and_import(self, code, modulename='mymodule'):
5858
sys.path.insert(0, self.tempdir)
5959
try:
6060
module = __import__(modulename)
61-
except SyntaxError:
62-
print('Bombed!')
61+
except SyntaxError as e:
62+
print('Import failed: %s' % e)
6363
else:
6464
print('Succeeded!')
6565
finally:

0 commit comments

Comments
 (0)
Please sign in to comment.