| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 34e5a63 commit f73d2e7
6 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -564,6 +564,10 @@ Functions and classes provided: | |||
| 564 | 564 | Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm* | |
| 565 | 565 | is not a context manager. | |
| 566 | 566 | ||
| 567 | + .. versionchanged:: next | ||
| 568 | + Added support for arbitrary descriptors :meth:`!__enter__` and | ||
| 569 | + :meth:`!__exit__`. | ||
| 570 | + | ||
| 567 | 571 | .. method:: push(exit) | |
| 568 | 572 | ||
| 569 | 573 | Adds a context manager's :meth:`~object.__exit__` method to the callback stack. | |
@@ -582,6 +586,9 @@ Functions and classes provided: | |||
| 582 | 586 | The passed in object is returned from the function, allowing this | |
| 583 | 587 | method to be used as a function decorator. | |
| 584 | 588 | ||
| 589 | + .. versionchanged:: next | ||
| 590 | + Added support for arbitrary descriptors :meth:`!__exit__`. | ||
| 591 | + | ||
| 585 | 592 | .. method:: callback(callback, /, *args, **kwds) | |
| 586 | 593 | ||
| 587 | 594 | Accepts an arbitrary callback function and arguments and adds it to | |
@@ -639,11 +646,17 @@ Functions and classes provided: | |||
| 639 | 646 | Raises :exc:`TypeError` instead of :exc:`AttributeError` if *cm* | |
| 640 | 647 | is not an asynchronous context manager. | |
| 641 | 648 | ||
| 649 | + .. versionchanged:: next | ||
| 650 | + Added support for arbitrary descriptors :meth:`!__aenter__` and :meth:`!__aexit__`. | ||
| 651 | + | ||
| 642 | 652 | .. method:: push_async_exit(exit) | |
| 643 | 653 | ||
| 644 | 654 | Similar to :meth:`ExitStack.push` but expects either an asynchronous context manager | |
| 645 | 655 | or a coroutine function. | |
| 646 | 656 | ||
| 657 | + .. versionchanged:: next | ||
| 658 | + Added support for arbitrary descriptors :meth:`!__aexit__`. | ||
| 659 | + | ||
| 647 | 660 | .. method:: push_async_callback(callback, /, *args, **kwds) | |
| 648 | 661 | ||
| 649 | 662 | Similar to :meth:`ExitStack.callback` but expects a coroutine function. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -548,6 +548,16 @@ concurrent.futures | |||
| 548 | 548 | (Contributed by Jonathan Berg in :gh:`139486`.) | |
| 549 | 549 | ||
| 550 | 550 | ||
| 551 | + contextlib | ||
| 552 | + ---------- | ||
| 553 | + | ||
| 554 | + * Added support for arbitrary descriptors :meth:`!__enter__`, | ||
| 555 | + :meth:`!__exit__`, :meth:`!__aenter__`, and :meth:`!__aexit__` in | ||
| 556 | + :class:`~contextlib.ExitStack` and :class:`contextlib.AsyncExitStack`, for | ||
| 557 | + consistency with the :keyword:`with` and :keyword:`async with` statements. | ||
| 558 | + (Contributed by Serhiy Storchaka in :gh:`144386`.) | ||
| 559 | + | ||
| 560 | + | ||
| 551 | 561 | dataclasses | |
| 552 | 562 | ----------- | |
| 553 | 563 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -5,7 +5,7 @@ | |||
| 5 | 5 | import _collections_abc | |
| 6 | 6 | from collections import deque | |
| 7 | 7 | from functools import wraps | |
| 8 | - from types import MethodType, GenericAlias | ||
| 8 | + from types import GenericAlias | ||
| 9 | 9 | ||
| 10 | 10 | __all__ = ["asynccontextmanager", "contextmanager", "closing", "nullcontext", | |
| 11 | 11 | "AbstractContextManager", "AbstractAsyncContextManager", | |
@@ -469,13 +469,23 @@ def __exit__(self, exctype, excinst, exctb): | |||
| 469 | 469 | return False | |
| 470 | 470 | ||
| 471 | 471 | ||
| 472 | + def _lookup_special(obj, name, default): | ||
| 473 | + # Follow the standard lookup behaviour for special methods. | ||
| 474 | + from inspect import getattr_static, _descriptor_get | ||
| 475 | + cls = type(obj) | ||
| 476 | + try: | ||
| 477 | + descr = getattr_static(cls, name) | ||
| 478 | + except AttributeError: | ||
| 479 | + return default | ||
| 480 | + return _descriptor_get(descr, obj) | ||
| 481 | + | ||
| 482 | + | ||
| 483 | + _sentinel = ['SENTINEL'] | ||
| 484 | + | ||
| 485 | + | ||
| 472 | 486 | class _BaseExitStack: | |
| 473 | 487 | """A base class for ExitStack and AsyncExitStack.""" | |
| 474 | 488 | ||
| 475 | - @staticmethod | ||
| 476 | - def _create_exit_wrapper(cm, cm_exit): | ||
| 477 | - return MethodType(cm_exit, cm) | ||
| 478 | - | ||
| 479 | 489 | @staticmethod | |
| 480 | 490 | def _create_cb_wrapper(callback, /, *args, **kwds): | |
| 481 | 491 | def _exit_wrapper(exc_type, exc, tb): | |
@@ -499,17 +509,8 @@ def push(self, exit): | |||
| 499 | 509 | Also accepts any object with an __exit__ method (registering a call | |
| 500 | 510 | to the method instead of the object itself). | |
| 501 | 511 | """ | |
| 502 | - # We use an unbound method rather than a bound method to follow | ||
| 503 | - # the standard lookup behaviour for special methods. | ||
| 504 | - _cb_type = type(exit) | ||
| 505 | - | ||
| 506 | - try: | ||
| 507 | - exit_method = _cb_type.__exit__ | ||
| 508 | - except AttributeError: | ||
| 509 | - # Not a context manager, so assume it's a callable. | ||
| 510 | - self._push_exit_callback(exit) | ||
| 511 | - else: | ||
| 512 | - self._push_cm_exit(exit, exit_method) | ||
| 512 | + exit_method = _lookup_special(exit, '__exit__', exit) | ||
| 513 | + self._push_exit_callback(exit_method) | ||
| 513 | 514 | return exit # Allow use as a decorator. | |
| 514 | 515 | ||
| 515 | 516 | def enter_context(self, cm): | |
@@ -518,17 +519,18 @@ def enter_context(self, cm): | |||
| 518 | 519 | If successful, also pushes its __exit__ method as a callback and | |
| 519 | 520 | returns the result of the __enter__ method. | |
| 520 | 521 | """ | |
| 521 | - # We look up the special methods on the type to match the with | ||
| 522 | - # statement. | ||
| 523 | - cls = type(cm) | ||
| 524 | - try: | ||
| 525 | - _enter = cls.__enter__ | ||
| 526 | - _exit = cls.__exit__ | ||
| 527 | - except AttributeError: | ||
| 522 | + _enter = _lookup_special(cm, '__enter__', _sentinel) | ||
| 523 | + if _enter is _sentinel: | ||
| 524 | + cls = type(cm) | ||
| 528 | 525 | raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " | |
| 529 | - f"not support the context manager protocol") from None | ||
| 530 | - result = _enter(cm) | ||
| 531 | - self._push_cm_exit(cm, _exit) | ||
| 526 | + f"not support the context manager protocol") | ||
| 527 | + _exit = _lookup_special(cm, '__exit__', _sentinel) | ||
| 528 | + if _exit is _sentinel: | ||
| 529 | + cls = type(cm) | ||
| 530 | + raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " | ||
| 531 | + f"not support the context manager protocol") | ||
| 532 | + result = _enter() | ||
| 533 | + self._push_exit_callback(_exit) | ||
| 532 | 534 | return result | |
| 533 | 535 | ||
| 534 | 536 | def callback(self, callback, /, *args, **kwds): | |
@@ -544,11 +546,6 @@ def callback(self, callback, /, *args, **kwds): | |||
| 544 | 546 | self._push_exit_callback(_exit_wrapper) | |
| 545 | 547 | return callback # Allow use as a decorator | |
| 546 | 548 | ||
| 547 | - def _push_cm_exit(self, cm, cm_exit): | ||
| 548 | - """Helper to correctly register callbacks to __exit__ methods.""" | ||
| 549 | - _exit_wrapper = self._create_exit_wrapper(cm, cm_exit) | ||
| 550 | - self._push_exit_callback(_exit_wrapper, True) | ||
| 551 | - | ||
| 552 | 549 | def _push_exit_callback(self, callback, is_sync=True): | |
| 553 | 550 | self._exit_callbacks.append((is_sync, callback)) | |
| 554 | 551 | ||
@@ -641,10 +638,6 @@ class AsyncExitStack(_BaseExitStack, AbstractAsyncContextManager): | |||
| 641 | 638 | # connection later in the list raise an exception. | |
| 642 | 639 | """ | |
| 643 | 640 | ||
| 644 | - @staticmethod | ||
| 645 | - def _create_async_exit_wrapper(cm, cm_exit): | ||
| 646 | - return MethodType(cm_exit, cm) | ||
| 647 | - | ||
| 648 | 641 | @staticmethod | |
| 649 | 642 | def _create_async_cb_wrapper(callback, /, *args, **kwds): | |
| 650 | 643 | async def _exit_wrapper(exc_type, exc, tb): | |
@@ -657,16 +650,18 @@ async def enter_async_context(self, cm): | |||
| 657 | 650 | If successful, also pushes its __aexit__ method as a callback and | |
| 658 | 651 | returns the result of the __aenter__ method. | |
| 659 | 652 | """ | |
| 660 | - cls = type(cm) | ||
| 661 | - try: | ||
| 662 | - _enter = cls.__aenter__ | ||
| 663 | - _exit = cls.__aexit__ | ||
| 664 | - except AttributeError: | ||
| 653 | + _enter = _lookup_special(cm, '__aenter__', _sentinel) | ||
| 654 | + if _enter is _sentinel: | ||
| 655 | + cls = type(cm) | ||
| 665 | 656 | raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " | |
| 666 | - f"not support the asynchronous context manager protocol" | ||
| 667 | - ) from None | ||
| 668 | - result = await _enter(cm) | ||
| 669 | - self._push_async_cm_exit(cm, _exit) | ||
| 657 | + f"not support the asynchronous context manager protocol") | ||
| 658 | + _exit = _lookup_special(cm, '__aexit__', _sentinel) | ||
| 659 | + if _exit is _sentinel: | ||
| 660 | + cls = type(cm) | ||
| 661 | + raise TypeError(f"'{cls.__module__}.{cls.__qualname__}' object does " | ||
| 662 | + f"not support the asynchronous context manager protocol") | ||
| 663 | + result = await _enter() | ||
| 664 | + self._push_exit_callback(_exit, False) | ||
| 670 | 665 | return result | |
| 671 | 666 | ||
| 672 | 667 | def push_async_exit(self, exit): | |
@@ -677,14 +672,8 @@ def push_async_exit(self, exit): | |||
| 677 | 672 | Also accepts any object with an __aexit__ method (registering a call | |
| 678 | 673 | to the method instead of the object itself). | |
| 679 | 674 | """ | |
| 680 | - _cb_type = type(exit) | ||
| 681 | - try: | ||
| 682 | - exit_method = _cb_type.__aexit__ | ||
| 683 | - except AttributeError: | ||
| 684 | - # Not an async context manager, so assume it's a coroutine function | ||
| 685 | - self._push_exit_callback(exit, False) | ||
| 686 | - else: | ||
| 687 | - self._push_async_cm_exit(exit, exit_method) | ||
| 675 | + exit_method = _lookup_special(exit, '__aexit__', exit) | ||
| 676 | + self._push_exit_callback(exit_method, False) | ||
| 688 | 677 | return exit # Allow use as a decorator | |
| 689 | 678 | ||
| 690 | 679 | def push_async_callback(self, callback, /, *args, **kwds): | |
@@ -704,12 +693,6 @@ async def aclose(self): | |||
| 704 | 693 | """Immediately unwind the context stack.""" | |
| 705 | 694 | await self.__aexit__(None, None, None) | |
| 706 | 695 | ||
| 707 | - def _push_async_cm_exit(self, cm, cm_exit): | ||
| 708 | - """Helper to correctly register coroutine function to __aexit__ | ||
| 709 | - method.""" | ||
| 710 | - _exit_wrapper = self._create_async_exit_wrapper(cm, cm_exit) | ||
| 711 | - self._push_exit_callback(_exit_wrapper, False) | ||
| 712 | - | ||
| 713 | 696 | async def __aenter__(self): | |
| 714 | 697 | return self | |
| 715 | 698 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -788,6 +788,75 @@ def _exit(): | |||
| 788 | 788 | result.append(2) | |
| 789 | 789 | self.assertEqual(result, [1, 2, 3, 4]) | |
| 790 | 790 | ||
| 791 | + def test_enter_context_classmethod(self): | ||
| 792 | + class TestCM: | ||
| 793 | + @classmethod | ||
| 794 | + def __enter__(cls): | ||
| 795 | + result.append(('enter', cls)) | ||
| 796 | + @classmethod | ||
| 797 | + def __exit__(cls, *exc_details): | ||
| 798 | + result.append(('exit', cls, *exc_details)) | ||
| 799 | + | ||
| 800 | + cm = TestCM() | ||
| 801 | + result = [] | ||
| 802 | + with self.exit_stack() as stack: | ||
| 803 | + stack.enter_context(cm) | ||
| 804 | + self.assertEqual(result, [('enter', TestCM)]) | ||
| 805 | + self.assertEqual(result, [('enter', TestCM), | ||
| 806 | + ('exit', TestCM, None, None, None)]) | ||
| 807 | + | ||
| 808 | + result = [] | ||
| 809 | + with self.exit_stack() as stack: | ||
| 810 | + stack.push(cm) | ||
| 811 | + self.assertEqual(result, []) | ||
| 812 | + self.assertEqual(result, [('exit', TestCM, None, None, None)]) | ||
| 813 | + | ||
| 814 | + def test_enter_context_staticmethod(self): | ||
| 815 | + class TestCM: | ||
| 816 | + @staticmethod | ||
| 817 | + def __enter__(): | ||
| 818 | + result.append('enter') | ||
| 819 | + @staticmethod | ||
| 820 | + def __exit__(*exc_details): | ||
| 821 | + result.append(('exit', *exc_details)) | ||
| 822 | + | ||
| 823 | + cm = TestCM() | ||
| 824 | + result = [] | ||
| 825 | + with self.exit_stack() as stack: | ||
| 826 | + stack.enter_context(cm) | ||
| 827 | + self.assertEqual(result, ['enter']) | ||
| 828 | + self.assertEqual(result, ['enter', ('exit', None, None, None)]) | ||
| 829 | + | ||
| 830 | + result = [] | ||
| 831 | + with self.exit_stack() as stack: | ||
| 832 | + stack.push(cm) | ||
| 833 | + self.assertEqual(result, []) | ||
| 834 | + self.assertEqual(result, [('exit', None, None, None)]) | ||
| 835 | + | ||
| 836 | + def test_enter_context_slots(self): | ||
| 837 | + class TestCM: | ||
| 838 | + __slots__ = ('__enter__', '__exit__') | ||
| 839 | + def __init__(self): | ||
| 840 | + def enter(): | ||
| 841 | + result.append('enter') | ||
| 842 | + def exit(*exc_details): | ||
| 843 | + result.append(('exit', *exc_details)) | ||
| 844 | + self.__enter__ = enter | ||
| 845 | + self.__exit__ = exit | ||
| 846 | + | ||
| 847 | + cm = TestCM() | ||
| 848 | + result = [] | ||
| 849 | + with self.exit_stack() as stack: | ||
| 850 | + stack.enter_context(cm) | ||
| 851 | + self.assertEqual(result, ['enter']) | ||
| 852 | + self.assertEqual(result, ['enter', ('exit', None, None, None)]) | ||
| 853 | + | ||
| 854 | + result = [] | ||
| 855 | + with self.exit_stack() as stack: | ||
| 856 | + stack.push(cm) | ||
| 857 | + self.assertEqual(result, []) | ||
| 858 | + self.assertEqual(result, [('exit', None, None, None)]) | ||
| 859 | + | ||
| 791 | 860 | def test_enter_context_errors(self): | |
| 792 | 861 | class LacksEnterAndExit: | |
| 793 | 862 | pass | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -641,6 +641,78 @@ async def _exit(): | |||
| 641 | 641 | ||
| 642 | 642 | self.assertEqual(result, [1, 2, 3, 4]) | |
| 643 | 643 | ||
| 644 | + @_async_test | ||
| 645 | + async def test_enter_async_context_classmethod(self): | ||
| 646 | + class TestCM: | ||
| 647 | + @classmethod | ||
| 648 | + async def __aenter__(cls): | ||
| 649 | + result.append(('enter', cls)) | ||
| 650 | + @classmethod | ||
| 651 | + async def __aexit__(cls, *exc_details): | ||
| 652 | + result.append(('exit', cls, *exc_details)) | ||
| 653 | + | ||
| 654 | + cm = TestCM() | ||
| 655 | + result = [] | ||
| 656 | + async with self.exit_stack() as stack: | ||
| 657 | + await stack.enter_async_context(cm) | ||
| 658 | + self.assertEqual(result, [('enter', TestCM)]) | ||
| 659 | + self.assertEqual(result, [('enter', TestCM), | ||
| 660 | + ('exit', TestCM, None, None, None)]) | ||
| 661 | + | ||
| 662 | + result = [] | ||
| 663 | + async with self.exit_stack() as stack: | ||
| 664 | + stack.push_async_exit(cm) | ||
| 665 | + self.assertEqual(result, []) | ||
| 666 | + self.assertEqual(result, [('exit', TestCM, None, None, None)]) | ||
| 667 | + | ||
| 668 | + @_async_test | ||
| 669 | + async def test_enter_async_context_staticmethod(self): | ||
| 670 | + class TestCM: | ||
| 671 | + @staticmethod | ||
| 672 | + async def __aenter__(): | ||
| 673 | + result.append('enter') | ||
| 674 | + @staticmethod | ||
| 675 | + async def __aexit__(*exc_details): | ||
| 676 | + result.append(('exit', *exc_details)) | ||
| 677 | + | ||
| 678 | + cm = TestCM() | ||
| 679 | + result = [] | ||
| 680 | + async with self.exit_stack() as stack: | ||
| 681 | + await stack.enter_async_context(cm) | ||
| 682 | + self.assertEqual(result, ['enter']) | ||
| 683 | + self.assertEqual(result, ['enter', ('exit', None, None, None)]) | ||
| 684 | + | ||
| 685 | + result = [] | ||
| 686 | + async with self.exit_stack() as stack: | ||
| 687 | + stack.push_async_exit(cm) | ||
| 688 | + self.assertEqual(result, []) | ||
| 689 | + self.assertEqual(result, [('exit', None, None, None)]) | ||
| 690 | + | ||
| 691 | + @_async_test | ||
| 692 | + async def test_enter_async_context_slots(self): | ||
| 693 | + class TestCM: | ||
| 694 | + __slots__ = ('__aenter__', '__aexit__') | ||
| 695 | + def __init__(self): | ||
| 696 | + async def enter(): | ||
| 697 | + result.append('enter') | ||
| 698 | + async def exit(*exc_details): | ||
| 699 | + result.append(('exit', *exc_details)) | ||
| 700 | + self.__aenter__ = enter | ||
| 701 | + self.__aexit__ = exit | ||
| 702 | + | ||
| 703 | + cm = TestCM() | ||
| 704 | + result = [] | ||
| 705 | + async with self.exit_stack() as stack: | ||
| 706 | + await stack.enter_async_context(cm) | ||
| 707 | + self.assertEqual(result, ['enter']) | ||
| 708 | + self.assertEqual(result, ['enter', ('exit', None, None, None)]) | ||
| 709 | + | ||
| 710 | + result = [] | ||
| 711 | + async with self.exit_stack() as stack: | ||
| 712 | + stack.push_async_exit(cm) | ||
| 713 | + self.assertEqual(result, []) | ||
| 714 | + self.assertEqual(result, [('exit', None, None, None)]) | ||
| 715 | + | ||
| 644 | 716 | @_async_test | |
| 645 | 717 | async def test_enter_async_context_errors(self): | |
| 646 | 718 | class LacksEnterAndExit: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,4 @@ | |||
| 1 | + Add support for arbitrary descriptors :meth:`!__enter__`, :meth:`!__exit__`, | ||
| 2 | + :meth:`!__aenter__`, and :meth:`!__aexit__` in :class:`contextlib.ExitStack` | ||
| 3 | + and :class:`contextlib.AsyncExitStack`, for consistency with the | ||
| 4 | + :keyword:`with` and :keyword:`async with` statements. | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments