Skip to content

Commit 50ccc80

Browse files
committed
Experiments with making the stdlib hooks more robust
1 parent f9316f1 commit 50ccc80

4 files changed

Lines changed: 177 additions & 42 deletions

File tree

docs/changelog.rst

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,41 @@ What's new
22
**********
33

44

5+
.. whats-new-0.11:
6+
7+
What's new in version 0.11
8+
==========================
9+
10+
More robust implementation of standard_library hooks
11+
----------------------------------------------------
12+
13+
``future.standard_library`` now no longer installs import hooks by default.
14+
These were bleeding into surrounding code, causing incompatibilities with
15+
modules like ``requests`` (issue #19).
16+
17+
Now ``future.standard_library`` provides the context manager
18+
``enable_hooks()``. Use it as follows::
19+
20+
>>> from future import standard_library
21+
>>> with standard_library.enable_hooks():
22+
... import queue
23+
... import socketserver
24+
... from http.client import HTTPConnection
25+
>>> import requests
26+
>>> # etc.
27+
28+
If you prefer, the following imports are also available directly::
29+
30+
>>> from future.standard_library import queue
31+
>>> from future.standard_library import socketserver
32+
>>> from future.standard_library.http.client import HTTPConnection
33+
34+
35+
As usual, this has no effect on Python 3.
36+
37+
*Note*: this is a backward-incompatible change.
38+
39+
540
.. whats-new-0.10:
641
742
What's new in version 0.10.x

docs/roadmap.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ futurize script
2222
future package
2323
--------------
2424

25-
- Add more tests for bytes ... preferably all from test_bytes.py in Py3.3.
26-
- Add disable_hooks() and enable_hooks() as functions in the
25+
- [Done] Add more tests for bytes ... preferably all from test_bytes.py in Py3.3.
26+
- [Done] Add remove_hooks() and install_hooks() as functions in the
2727
:mod:`future.standard_library` module. (See the uprefix module for how
2828
to do this.)
2929

future/standard_library/__init__.py

Lines changed: 94 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@
3131
3232
To turn off the import hooks, use::
3333
34-
standard_library.disable_hooks()
34+
standard_library.remove_hooks()
3535
3636
and to turn it on again, use::
3737
38-
standard_library.enable_hooks()
38+
standard_library.install_hooks()
3939
4040
This is a cleaner alternative to this idiom (see
4141
http://docs.pythonsprints.com/python3_porting/py-porting.html)::
@@ -102,6 +102,7 @@
102102
# pickle (fast one)
103103
# dbm
104104
# urllib
105+
# test
105106

106107
# These ones are new (i.e. no problem)
107108
# http
@@ -208,7 +209,7 @@ class RenameImport(object):
208209
# Different RenameImport classes are created when importing this module from
209210
# different source files. This causes isinstance(hook, RenameImport) checks
210211
# to produce inconsistent results. We add this RENAMER attribute here so
211-
# disable_hooks() and enable_hooks() can find instances of these classes
212+
# remove_hooks() and install_hooks() can find instances of these classes
212213
# easily:
213214
RENAMER = True
214215

@@ -325,7 +326,53 @@ def _find_and_load_module(self, name, path=None):
325326
]
326327

327328

328-
def enable_hooks():
329+
class enable_hooks(object):
330+
"""
331+
Acts as a context manager. Use like this:
332+
333+
>>> from future import standard_library
334+
>>> with standard_library.enable_hooks():
335+
... import http.client
336+
>>> import requests # incompatible with ``future``'s standard library hooks
337+
"""
338+
def __enter__(self):
339+
print('Entering CM')
340+
self.hooks_were_installed = detect_hooks()
341+
install_hooks()
342+
return self
343+
344+
def __exit__(self, *args):
345+
print('Exiting CM')
346+
if not self.hooks_were_installed:
347+
remove_hooks()
348+
349+
350+
class suspend_hooks(object):
351+
"""
352+
Acts as a context manager. Use like this:
353+
354+
>>> from future import standard_library
355+
>>> standard_library.install_hooks()
356+
>>> import http.client
357+
>>> # ...
358+
>>> with standard_library.suspend_hooks():
359+
>>> import requests # incompatible with ``future``'s standard library hooks
360+
361+
If the hooks were disabled before the context, they are not installed when
362+
the context is left.
363+
"""
364+
def __enter__(self):
365+
self.hooks_were_installed = detect_hooks()
366+
remove_hooks()
367+
return self
368+
def __exit__(self, *args):
369+
if not self.hooks_were_installed:
370+
install_hooks()
371+
372+
373+
def install_hooks():
374+
print('sys.meta_path was: {}'.format(sys.meta_path))
375+
print('Installing hooks ...')
329376
if utils.PY3:
330377
return
331378
for (newmodname, newobjname, oldmodname, oldobjname) in MOVES:
@@ -336,28 +383,58 @@ def enable_hooks():
336383

337384
# Add it unless it's there already
338385
newhook = RenameImport(RENAMES)
339-
if not any([hasattr(hook, 'RENAMER') for hook in sys.meta_path]):
386+
if not detect_hooks():
340387
sys.meta_path.append(newhook)
388+
print('sys.meta_path is now: {}'.format(sys.meta_path))
341389

342390

343-
def disable_hooks():
391+
def remove_hooks():
392+
"""
393+
Use to remove the ``future.standard_library`` import hooks.
394+
"""
395+
print('sys.meta_path was: {}'.format(sys.meta_path))
396+
print('Uninstalling hooks ...')
344397
if not utils.PY3:
345398
# Loop backwards, so deleting items keeps the ordering:
346399
for i, hook in list(enumerate(sys.meta_path))[::-1]:
347400
if hasattr(hook, 'RENAMER'):
348401
del sys.meta_path[i]
402+
print('sys.meta_path is now: {}'.format(sys.meta_path))
349403

350404

351-
@contextlib.contextmanager
352-
def suspend_hooks():
353-
disable_hooks()
354-
try:
355-
yield
356-
except Exception as e:
357-
raise e
358-
finally:
359-
enable_hooks()
405+
def disable_hooks():
406+
"""
407+
Deprecated. Use remove_hooks() instead. This will be removed by
408+
``future`` v1.0.
409+
"""
410+
remove_hooks()
360411

361412

362-
if not utils.PY3:
363-
enable_hooks()
413+
def detect_hooks():
414+
"""
415+
Returns True if the import hooks are installed, False if not.
416+
"""
417+
print('Detecting hooks ...')
418+
present = any([hasattr(hook, 'RENAMER') for hook in sys.meta_path])
419+
if present:
420+
print('Detected.')
421+
else:
422+
print('Not detected.')
423+
return present
424+
425+
426+
# Now import the modules:
427+
with enable_hooks():
428+
for (oldname, newname) in RENAMES.items():
429+
if newname == 'winreg' and sys.platform not in ['win32', 'win64']:
430+
continue
431+
if newname in REPLACED_MODULES:
432+
# Skip this check for e.g. the stdlib's ``test`` module,
433+
# which we have replaced completely.
434+
continue
435+
newmod = __import__(newname)
436+
globals()[newname] = newmod
437+
438+
439+
# if not utils.PY3:
440+
# install_hooks()

future/tests/test_standard_library.py

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ def test_suspend_hooks(self):
4747
"""
4848
example_PY2_check = False
4949
with standard_library.suspend_hooks():
50-
# An example of fragile import code that we don't want to break:
50+
# An example of code that we don't want to break:
5151
try:
52-
import builtins
52+
import builtins # fragile check for Python 3.x
5353
except ImportError:
5454
example_PY2_check = True
5555
if utils.PY2:
@@ -59,13 +59,14 @@ def test_suspend_hooks(self):
5959
# The import should succeed again now:
6060
import builtins
6161

62-
def test_disable_hooks(self):
62+
def test_remove_hooks(self):
6363
example_PY2_check = False
6464

65-
standard_library.enable_hooks()
65+
standard_library.install_hooks()
6666
old_meta_path = copy.copy(sys.meta_path)
67+
import builtins
6768

68-
standard_library.disable_hooks()
69+
standard_library.remove_hooks()
6970
self.assertTrue(len(old_meta_path) == len(sys.meta_path) + 1)
7071

7172
# An example of fragile import code that we don't want to break:
@@ -77,11 +78,33 @@ def test_disable_hooks(self):
7778
self.assertTrue(example_PY2_check)
7879
else:
7980
self.assertFalse(example_PY2_check)
80-
standard_library.enable_hooks()
81+
standard_library.install_hooks()
8182
# The import should succeed again now:
8283
import builtins
8384
self.assertTrue(len(old_meta_path) == len(sys.meta_path))
8485

86+
def test_remove_hooks2(self):
87+
"""
88+
This verifies that modules like http.client are no longer accessible after
89+
disabling import hooks, even if they have been previously imported.
90+
91+
The reason for this test is that Python caches imported modules in sys.modules.
92+
"""
93+
standard_library.remove_hooks()
94+
try:
95+
from . import verify_remove_hooks_affects_imported_modules
96+
except RuntimeError as e:
97+
self.fail(e.message)
98+
finally:
99+
standard_library.install_hooks()
100+
101+
def test_requests(self):
102+
"""
103+
GitHub issue #19: conflict with ``requests``
104+
"""
105+
# The below should succeed while ``requests`` is installed:
106+
from . import verify_requests_is_not_broken
107+
85108
@unittest.skipIf(utils.PY3, 'not testing for old urllib on Py3')
86109
def test_old_urllib_import(self):
87110
"""
@@ -143,23 +166,23 @@ def test_itertools_zip_longest(self):
143166
self.assertEqual(list(zip_longest(a, b)),
144167
[(1, 2), (2, 4), (None, 6)])
145168

146-
def test_import_from_module(self):
147-
"""
148-
Tests whether e.g. "import socketserver" succeeds in a module
149-
imported by another module.
150-
"""
151-
code1 = '''
152-
from future import standard_library
153-
import importme2
154-
'''
155-
code2 = '''
156-
import socketserver
157-
print('Import succeeded!')
158-
'''
159-
self._write_test_script(code1, 'importme1.py')
160-
self._write_test_script(code2, 'importme2.py')
161-
output = self._run_test_script('importme1.py')
162-
print(output)
169+
# def test_import_from_module(self):
170+
# """
171+
# Tests whether e.g. "import socketserver" succeeds in a module
172+
# imported by another module. We do not want it to!
173+
# """
174+
# code1 = '''
175+
# from future import standard_library
176+
# import importme2
177+
# '''
178+
# code2 = '''
179+
# import socketserver
180+
# print('Import succeeded!')
181+
# '''
182+
# self._write_test_script(code1, 'importme1.py')
183+
# self._write_test_script(code2, 'importme2.py')
184+
# output = self._run_test_script('importme1.py')
185+
# print(output)
163186

164187
def test_configparser(self):
165188
import configparser

0 commit comments

Comments
 (0)