Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 36 additions & 22 deletions Lib/contextlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class AbstractContextManager(abc.ABC):

__class_getitem__ = classmethod(GenericAlias)

__slots__ = ()

def __enter__(self):
"""Return `self` upon entering the runtime context."""
return self
Expand All @@ -42,6 +44,8 @@ class AbstractAsyncContextManager(abc.ABC):

__class_getitem__ = classmethod(GenericAlias)

__slots__ = ()

async def __aenter__(self):
"""Return `self` upon entering the runtime context."""
return self
Expand Down Expand Up @@ -565,11 +569,12 @@ def __enter__(self):
return self

def __exit__(self, *exc_details):
received_exc = exc_details[0] is not None
exc = exc_details[1]
received_exc = exc is not None

# We manipulate the exception state so it behaves as though
# we were actually nesting multiple with statements
frame_exc = sys.exc_info()[1]
frame_exc = sys.exception()
def _fix_exception_context(new_exc, old_exc):
# Context may not be correct, so find the end of the chain
while 1:
Expand All @@ -592,24 +597,28 @@ def _fix_exception_context(new_exc, old_exc):
is_sync, cb = self._exit_callbacks.pop()
assert is_sync
try:
if exc is None:
exc_details = None, None, None
else:
exc_details = type(exc), exc, exc.__traceback__
if cb(*exc_details):
suppressed_exc = True
pending_raise = False
exc_details = (None, None, None)
except:
new_exc_details = sys.exc_info()
exc = None
except BaseException as new_exc:
# simulate the stack of exceptions by setting the context
_fix_exception_context(new_exc_details[1], exc_details[1])
_fix_exception_context(new_exc, exc)
pending_raise = True
exc_details = new_exc_details
exc = new_exc

if pending_raise:
try:
# bare "raise exc_details[1]" replaces our carefully
# bare "raise exc" replaces our carefully
# set-up context
fixed_ctx = exc_details[1].__context__
raise exc_details[1]
fixed_ctx = exc.__context__
raise exc
except BaseException:
exc_details[1].__context__ = fixed_ctx
exc.__context__ = fixed_ctx
raise
return received_exc and suppressed_exc

Expand Down Expand Up @@ -705,11 +714,12 @@ async def __aenter__(self):
return self

async def __aexit__(self, *exc_details):
received_exc = exc_details[0] is not None
exc = exc_details[1]
received_exc = exc is not None

# We manipulate the exception state so it behaves as though
# we were actually nesting multiple with statements
frame_exc = sys.exc_info()[1]
frame_exc = sys.exception()
def _fix_exception_context(new_exc, old_exc):
# Context may not be correct, so find the end of the chain
while 1:
Expand All @@ -731,6 +741,10 @@ def _fix_exception_context(new_exc, old_exc):
while self._exit_callbacks:
is_sync, cb = self._exit_callbacks.pop()
try:
if exc is None:
exc_details = None, None, None
else:
exc_details = type(exc), exc, exc.__traceback__
if is_sync:
cb_suppress = cb(*exc_details)
else:
Expand All @@ -739,21 +753,21 @@ def _fix_exception_context(new_exc, old_exc):
if cb_suppress:
suppressed_exc = True
pending_raise = False
exc_details = (None, None, None)
except:
new_exc_details = sys.exc_info()
exc = None
except BaseException as new_exc:
# simulate the stack of exceptions by setting the context
_fix_exception_context(new_exc_details[1], exc_details[1])
_fix_exception_context(new_exc, exc)
pending_raise = True
exc_details = new_exc_details
exc = new_exc

if pending_raise:
try:
# bare "raise exc_details[1]" replaces our carefully
# bare "raise exc" replaces our carefully
# set-up context
fixed_ctx = exc_details[1].__context__
raise exc_details[1]
fixed_ctx = exc.__context__
raise exc
except BaseException:
exc_details[1].__context__ = fixed_ctx
exc.__context__ = fixed_ctx
raise
return received_exc and suppressed_exc

Expand Down
36 changes: 36 additions & 0 deletions Lib/test/archivetestdata/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Test data for `test_zipfile`, `test_tarfile` (and even some others)

## `test_zipfile`

The test executables in this directory are created manually from `header.sh` and
the `testdata_module_inside_zip.py` file. You must have Info-ZIP's zip utility
installed (`apt install zip` on Debian).

### Purpose of `exe_with_zip` and `exe_with_z64`

These are used to test executable files with an appended zipfile, in a scenario
where the executable is _not_ a Python interpreter itself so our automatic
zipimport machinery (that'd look for `__main__.py`) is not being used.

### Updating the test executables

If you update header.sh or the testdata_module_inside_zip.py file, rerun the
commands below. These are expected to be rarely changed, if ever.

#### Standard old format (2.0) zip file

```
zip -0 zip2.zip testdata_module_inside_zip.py
cat header.sh zip2.zip >exe_with_zip
rm zip2.zip
```

#### Modern format (4.5) zip64 file

Redirecting from stdin forces Info-ZIP's zip tool to create a zip64.

```
zip -0 <testdata_module_inside_zip.py >zip64.zip
cat header.sh zip64.zip >exe_with_z64
rm zip64.zip
```
Binary file added Lib/test/archivetestdata/exe_with_z64
Binary file not shown.
Binary file added Lib/test/archivetestdata/exe_with_zip
Binary file not shown.
24 changes: 24 additions & 0 deletions Lib/test/archivetestdata/header.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/bin/bash
INTERPRETER_UNDER_TEST="$1"
if [[ ! -x "${INTERPRETER_UNDER_TEST}" ]]; then
echo "Interpreter must be the command line argument."
exit 4
fi
EXECUTABLE="$0" exec "${INTERPRETER_UNDER_TEST}" -E - <<END_OF_PYTHON
import os
import zipfile

namespace = {}

filename = os.environ['EXECUTABLE']
print(f'Opening {filename} as a zipfile.')
with zipfile.ZipFile(filename, mode='r') as exe_zip:
for file_info in exe_zip.infolist():
data = exe_zip.read(file_info)
exec(data, namespace, namespace)
break # Only use the first file in the archive.

print('Favorite number in executable:', namespace["FAVORITE_NUMBER"])

### Archive contents will be appended after this file. ###
END_OF_PYTHON
Binary file added Lib/test/archivetestdata/recursion.tar
Binary file not shown.
2 changes: 2 additions & 0 deletions Lib/test/archivetestdata/testdata_module_inside_zip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Test data file to be stored within a zip file.
FAVORITE_NUMBER = 5
Binary file added Lib/test/archivetestdata/testtar.tar
Binary file not shown.
Binary file added Lib/test/archivetestdata/testtar.tar.xz
Binary file not shown.
Binary file added Lib/test/archivetestdata/zip_cp437_header.zip
Binary file not shown.
Binary file added Lib/test/archivetestdata/zipdir.zip
Binary file not shown.
Binary file added Lib/test/archivetestdata/zipdir_backslash.zip
Binary file not shown.
20 changes: 17 additions & 3 deletions Lib/test/test_contextlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,18 @@ def __exit__(self, *args):
manager = DefaultEnter()
self.assertIs(manager.__enter__(), manager)

# TODO: RUSTPYTHON
@unittest.expectedFailure
def test_slots(self):
class DefaultContextManager(AbstractContextManager):
__slots__ = ()

def __exit__(self, *args):
super().__exit__(*args)

with self.assertRaises(AttributeError):
DefaultContextManager().var = 42

def test_exit_is_abstract(self):
class MissingExit(AbstractContextManager):
pass
Expand Down Expand Up @@ -194,6 +206,7 @@ def whoo():
yield
except RuntimeError:
raise SyntaxError

ctx = whoo()
ctx.__enter__()
with self.assertRaises(SyntaxError):
Expand Down Expand Up @@ -285,9 +298,11 @@ def woohoo():
yield
except Exception as exc:
raise RuntimeError(f'caught {exc}') from exc

with self.assertRaises(RuntimeError):
with woohoo():
1 / 0

# If the context manager wrapped StopIteration in a RuntimeError,
# we also unwrap it, because we can't tell whether the wrapping was
# done by the generator machinery or by the generator itself.
Expand Down Expand Up @@ -1143,7 +1158,7 @@ def first():
class TestExitStack(TestBaseExitStack, unittest.TestCase):
exit_stack = ExitStack
callback_error_internal_frames = [
('__exit__', 'raise exc_details[1]'),
('__exit__', 'raise exc'),
('__exit__', 'if cb(*exc_details):'),
]

Expand Down Expand Up @@ -1294,7 +1309,6 @@ def test_exception_groups(self):
[KeyError("ke1"), KeyError("ke2")],
),
)

# Check handling of BaseExceptionGroup, using GeneratorExit so that
# we don't accidentally discard a ctrl-c with KeyboardInterrupt.
with suppress(GeneratorExit):
Expand Down Expand Up @@ -1338,7 +1352,7 @@ def test_simple(self):
def test_reentrant(self):
old_cwd = os.getcwd()
target1 = self.make_relative_path('data')
target2 = self.make_relative_path('ziptestdata')
target2 = self.make_relative_path('archivetestdata')
self.assertNotIn(old_cwd, (target1, target2))
chdir1, chdir2 = chdir(target1), chdir(target2)

Expand Down
Loading
Loading