bpo-29302: Implement contextlib.AsyncExitStack. (#4790) · python/cpython@1aa094f (original) (raw)

`@@ -7,7 +7,7 @@

`

7

7

``

8

8

`all = ["asynccontextmanager", "contextmanager", "closing", "nullcontext",

`

9

9

`"AbstractContextManager", "AbstractAsyncContextManager",

`

10

``

`-

"ContextDecorator", "ExitStack",

`

``

10

`+

"AsyncExitStack", "ContextDecorator", "ExitStack",

`

11

11

`"redirect_stdout", "redirect_stderr", "suppress"]

`

12

12

``

13

13

``

`@@ -365,85 +365,102 @@ def exit(self, exctype, excinst, exctb):

`

365

365

`return exctype is not None and issubclass(exctype, self._exceptions)

`

366

366

``

367

367

``

368

``

`-

Inspired by discussions on http://bugs.python.org/issue13585

`

369

``

`-

class ExitStack(AbstractContextManager):

`

370

``

`-

"""Context manager for dynamic management of a stack of exit callbacks

`

``

368

`+

class _BaseExitStack:

`

``

369

`+

"""A base class for ExitStack and AsyncExitStack."""

`

371

370

``

372

``

`-

For example:

`

``

371

`+

@staticmethod

`

``

372

`+

def _create_exit_wrapper(cm, cm_exit):

`

``

373

`+

def _exit_wrapper(exc_type, exc, tb):

`

``

374

`+

return cm_exit(cm, exc_type, exc, tb)

`

``

375

`+

return _exit_wrapper

`

373

376

``

374

``

`-

with ExitStack() as stack:

`

375

``

`-

files = [stack.enter_context(open(fname)) for fname in filenames]

`

376

``

`-

All opened files will automatically be closed at the end of

`

377

``

`-

the with statement, even if attempts to open files later

`

378

``

`-

in the list raise an exception

`

``

377

`+

@staticmethod

`

``

378

`+

def _create_cb_wrapper(callback, *args, **kwds):

`

``

379

`+

def _exit_wrapper(exc_type, exc, tb):

`

``

380

`+

callback(*args, **kwds)

`

``

381

`+

return _exit_wrapper

`

379

382

``

380

``

`-

"""

`

381

383

`def init(self):

`

382

384

`self._exit_callbacks = deque()

`

383

385

``

384

386

`def pop_all(self):

`

385

``

`-

"""Preserve the context stack by transferring it to a new instance"""

`

``

387

`+

"""Preserve the context stack by transferring it to a new instance."""

`

386

388

`new_stack = type(self)()

`

387

389

`new_stack._exit_callbacks = self._exit_callbacks

`

388

390

`self._exit_callbacks = deque()

`

389

391

`return new_stack

`

390

392

``

391

``

`-

def _push_cm_exit(self, cm, cm_exit):

`

392

``

`-

"""Helper to correctly register callbacks to exit methods"""

`

393

``

`-

def _exit_wrapper(*exc_details):

`

394

``

`-

return cm_exit(cm, *exc_details)

`

395

``

`-

_exit_wrapper.self = cm

`

396

``

`-

self.push(_exit_wrapper)

`

397

``

-

398

393

`def push(self, exit):

`

399

``

`-

"""Registers a callback with the standard exit method signature

`

400

``

-

401

``

`-

Can suppress exceptions the same way exit methods can.

`

``

394

`+

"""Registers a callback with the standard exit method signature.

`

402

395

``

``

396

`+

Can suppress exceptions the same way exit method can.

`

403

397

` Also accepts any object with an exit method (registering a call

`

404

``

`-

to the method instead of the object itself)

`

``

398

`+

to the method instead of the object itself).

`

405

399

` """

`

406

400

`# We use an unbound method rather than a bound method to follow

`

407

``

`-

the standard lookup behaviour for special methods

`

``

401

`+

the standard lookup behaviour for special methods.

`

408

402

`_cb_type = type(exit)

`

``

403

+

409

404

`try:

`

410

405

`exit_method = _cb_type.exit

`

411

406

`except AttributeError:

`

412

``

`-

Not a context manager, so assume its a callable

`

413

``

`-

self._exit_callbacks.append(exit)

`

``

407

`+

Not a context manager, so assume it's a callable.

`

``

408

`+

self._push_exit_callback(exit)

`

414

409

`else:

`

415

410

`self._push_cm_exit(exit, exit_method)

`

416

``

`-

return exit # Allow use as a decorator

`

417

``

-

418

``

`-

def callback(self, callback, *args, **kwds):

`

419

``

`-

"""Registers an arbitrary callback and arguments.

`

420

``

-

421

``

`-

Cannot suppress exceptions.

`

422

``

`-

"""

`

423

``

`-

def _exit_wrapper(exc_type, exc, tb):

`

424

``

`-

callback(*args, **kwds)

`

425

``

`-

We changed the signature, so using @wraps is not appropriate, but

`

426

``

`-

setting wrapped may still help with introspection

`

427

``

`-

_exit_wrapper.wrapped = callback

`

428

``

`-

self.push(_exit_wrapper)

`

429

``

`-

return callback # Allow use as a decorator

`

``

411

`+

return exit # Allow use as a decorator.

`

430

412

``

431

413

`def enter_context(self, cm):

`

432

``

`-

"""Enters the supplied context manager

`

``

414

`+

"""Enters the supplied context manager.

`

433

415

``

434

416

` If successful, also pushes its exit method as a callback and

`

435

417

` returns the result of the enter method.

`

436

418

` """

`

437

``

`-

We look up the special methods on the type to match the with statement

`

``

419

`+

We look up the special methods on the type to match the with

`

``

420

`+

statement.

`

438

421

`_cm_type = type(cm)

`

439

422

`_exit = _cm_type.exit

`

440

423

`result = _cm_type.enter(cm)

`

441

424

`self._push_cm_exit(cm, _exit)

`

442

425

`return result

`

443

426

``

444

``

`-

def close(self):

`

445

``

`-

"""Immediately unwind the context stack"""

`

446

``

`-

self.exit(None, None, None)

`

``

427

`+

def callback(self, callback, *args, **kwds):

`

``

428

`+

"""Registers an arbitrary callback and arguments.

`

``

429

+

``

430

`+

Cannot suppress exceptions.

`

``

431

`+

"""

`

``

432

`+

_exit_wrapper = self._create_cb_wrapper(callback, *args, **kwds)

`

``

433

+

``

434

`+

We changed the signature, so using @wraps is not appropriate, but

`

``

435

`+

setting wrapped may still help with introspection.

`

``

436

`+

_exit_wrapper.wrapped = callback

`

``

437

`+

self._push_exit_callback(_exit_wrapper)

`

``

438

`+

return callback # Allow use as a decorator

`

``

439

+

``

440

`+

def _push_cm_exit(self, cm, cm_exit):

`

``

441

`+

"""Helper to correctly register callbacks to exit methods."""

`

``

442

`+

_exit_wrapper = self._create_exit_wrapper(cm, cm_exit)

`

``

443

`+

_exit_wrapper.self = cm

`

``

444

`+

self._push_exit_callback(_exit_wrapper, True)

`

``

445

+

``

446

`+

def _push_exit_callback(self, callback, is_sync=True):

`

``

447

`+

self._exit_callbacks.append((is_sync, callback))

`

``

448

+

``

449

+

``

450

`+

Inspired by discussions on http://bugs.python.org/issue13585

`

``

451

`+

class ExitStack(_BaseExitStack, AbstractContextManager):

`

``

452

`+

"""Context manager for dynamic management of a stack of exit callbacks.

`

``

453

+

``

454

`+

For example:

`

``

455

`+

with ExitStack() as stack:

`

``

456

`+

files = [stack.enter_context(open(fname)) for fname in filenames]

`

``

457

`+

All opened files will automatically be closed at the end of

`

``

458

`+

the with statement, even if attempts to open files later

`

``

459

`+

in the list raise an exception.

`

``

460

`+

"""

`

``

461

+

``

462

`+

def enter(self):

`

``

463

`+

return self

`

447

464

``

448

465

`def exit(self, *exc_details):

`

449

466

`received_exc = exc_details[0] is not None

`

`@@ -470,7 +487,8 @@ def _fix_exception_context(new_exc, old_exc):

`

470

487

`suppressed_exc = False

`

471

488

`pending_raise = False

`

472

489

`while self._exit_callbacks:

`

473

``

`-

cb = self._exit_callbacks.pop()

`

``

490

`+

is_sync, cb = self._exit_callbacks.pop()

`

``

491

`+

assert is_sync

`

474

492

`try:

`

475

493

`if cb(*exc_details):

`

476

494

`suppressed_exc = True

`

`@@ -493,6 +511,147 @@ def _fix_exception_context(new_exc, old_exc):

`

493

511

`raise

`

494

512

`return received_exc and suppressed_exc

`

495

513

``

``

514

`+

def close(self):

`

``

515

`+

"""Immediately unwind the context stack."""

`

``

516

`+

self.exit(None, None, None)

`

``

517

+

``

518

+

``

519

`+

Inspired by discussions on https://bugs.python.org/issue29302

`

``

520

`+

class AsyncExitStack(_BaseExitStack, AbstractAsyncContextManager):

`

``

521

`+

"""Async context manager for dynamic management of a stack of exit

`

``

522

`+

callbacks.

`

``

523

+

``

524

`+

For example:

`

``

525

`+

async with AsyncExitStack() as stack:

`

``

526

`+

connections = [await stack.enter_async_context(get_connection())

`

``

527

`+

for i in range(5)]

`

``

528

`+

All opened connections will automatically be released at the

`

``

529

`+

end of the async with statement, even if attempts to open a

`

``

530

`+

connection later in the list raise an exception.

`

``

531

`+

"""

`

``

532

+

``

533

`+

@staticmethod

`

``

534

`+

def _create_async_exit_wrapper(cm, cm_exit):

`

``

535

`+

async def _exit_wrapper(exc_type, exc, tb):

`

``

536

`+

return await cm_exit(cm, exc_type, exc, tb)

`

``

537

`+

return _exit_wrapper

`

``

538

+

``

539

`+

@staticmethod

`

``

540

`+

def _create_async_cb_wrapper(callback, *args, **kwds):

`

``

541

`+

async def _exit_wrapper(exc_type, exc, tb):

`

``

542

`+

await callback(*args, **kwds)

`

``

543

`+

return _exit_wrapper

`

``

544

+

``

545

`+

async def enter_async_context(self, cm):

`

``

546

`+

"""Enters the supplied async context manager.

`

``

547

+

``

548

`+

If successful, also pushes its aexit method as a callback and

`

``

549

`+

returns the result of the aenter method.

`

``

550

`+

"""

`

``

551

`+

_cm_type = type(cm)

`

``

552

`+

_exit = _cm_type.aexit

`

``

553

`+

result = await _cm_type.aenter(cm)

`

``

554

`+

self._push_async_cm_exit(cm, _exit)

`

``

555

`+

return result

`

``

556

+

``

557

`+

def push_async_exit(self, exit):

`

``

558

`+

"""Registers a coroutine function with the standard aexit method

`

``

559

`+

signature.

`

``

560

+

``

561

`+

Can suppress exceptions the same way aexit method can.

`

``

562

`+

Also accepts any object with an aexit method (registering a call

`

``

563

`+

to the method instead of the object itself).

`

``

564

`+

"""

`

``

565

`+

_cb_type = type(exit)

`

``

566

`+

try:

`

``

567

`+

exit_method = _cb_type.aexit

`

``

568

`+

except AttributeError:

`

``

569

`+

Not an async context manager, so assume it's a coroutine function

`

``

570

`+

self._push_exit_callback(exit, False)

`

``

571

`+

else:

`

``

572

`+

self._push_async_cm_exit(exit, exit_method)

`

``

573

`+

return exit # Allow use as a decorator

`

``

574

+

``

575

`+

def push_async_callback(self, callback, *args, **kwds):

`

``

576

`+

"""Registers an arbitrary coroutine function and arguments.

`

``

577

+

``

578

`+

Cannot suppress exceptions.

`

``

579

`+

"""

`

``

580

`+

_exit_wrapper = self._create_async_cb_wrapper(callback, *args, **kwds)

`

``

581

+

``

582

`+

We changed the signature, so using @wraps is not appropriate, but

`

``

583

`+

setting wrapped may still help with introspection.

`

``

584

`+

_exit_wrapper.wrapped = callback

`

``

585

`+

self._push_exit_callback(_exit_wrapper, False)

`

``

586

`+

return callback # Allow use as a decorator

`

``

587

+

``

588

`+

async def aclose(self):

`

``

589

`+

"""Immediately unwind the context stack."""

`

``

590

`+

await self.aexit(None, None, None)

`

``

591

+

``

592

`+

def _push_async_cm_exit(self, cm, cm_exit):

`

``

593

`+

"""Helper to correctly register coroutine function to aexit

`

``

594

`+

method."""

`

``

595

`+

_exit_wrapper = self._create_async_exit_wrapper(cm, cm_exit)

`

``

596

`+

_exit_wrapper.self = cm

`

``

597

`+

self._push_exit_callback(_exit_wrapper, False)

`

``

598

+

``

599

`+

async def aenter(self):

`

``

600

`+

return self

`

``

601

+

``

602

`+

async def aexit(self, *exc_details):

`

``

603

`+

received_exc = exc_details[0] is not None

`

``

604

+

``

605

`+

We manipulate the exception state so it behaves as though

`

``

606

`+

we were actually nesting multiple with statements

`

``

607

`+

frame_exc = sys.exc_info()[1]

`

``

608

`+

def _fix_exception_context(new_exc, old_exc):

`

``

609

`+

Context may not be correct, so find the end of the chain

`

``

610

`+

while 1:

`

``

611

`+

exc_context = new_exc.context

`

``

612

`+

if exc_context is old_exc:

`

``

613

`+

Context is already set correctly (see issue 20317)

`

``

614

`+

return

`

``

615

`+

if exc_context is None or exc_context is frame_exc:

`

``

616

`+

break

`

``

617

`+

new_exc = exc_context

`

``

618

`+

Change the end of the chain to point to the exception

`

``

619

`+

we expect it to reference

`

``

620

`+

new_exc.context = old_exc

`

``

621

+

``

622

`+

Callbacks are invoked in LIFO order to match the behaviour of

`

``

623

`+

nested context managers

`

``

624

`+

suppressed_exc = False

`

``

625

`+

pending_raise = False

`

``

626

`+

while self._exit_callbacks:

`

``

627

`+

is_sync, cb = self._exit_callbacks.pop()

`

``

628

`+

try:

`

``

629

`+

if is_sync:

`

``

630

`+

cb_suppress = cb(*exc_details)

`

``

631

`+

else:

`

``

632

`+

cb_suppress = await cb(*exc_details)

`

``

633

+

``

634

`+

if cb_suppress:

`

``

635

`+

suppressed_exc = True

`

``

636

`+

pending_raise = False

`

``

637

`+

exc_details = (None, None, None)

`

``

638

`+

except:

`

``

639

`+

new_exc_details = sys.exc_info()

`

``

640

`+

simulate the stack of exceptions by setting the context

`

``

641

`+

_fix_exception_context(new_exc_details[1], exc_details[1])

`

``

642

`+

pending_raise = True

`

``

643

`+

exc_details = new_exc_details

`

``

644

`+

if pending_raise:

`

``

645

`+

try:

`

``

646

`+

bare "raise exc_details[1]" replaces our carefully

`

``

647

`+

set-up context

`

``

648

`+

fixed_ctx = exc_details[1].context

`

``

649

`+

raise exc_details[1]

`

``

650

`+

except BaseException:

`

``

651

`+

exc_details[1].context = fixed_ctx

`

``

652

`+

raise

`

``

653

`+

return received_exc and suppressed_exc

`

``

654

+

496

655

``

497

656

`class nullcontext(AbstractContextManager):

`

498

657

`"""Context manager that does no additional processing.

`