bpo-47062: Implement asyncio.Runner context manager (GH-31799) · python/cpython@4119d2d (original) (raw)

1

``

`-

all = 'run',

`

``

1

`+

all = ('Runner', 'run')

`

2

2

``

``

3

`+

import contextvars

`

``

4

`+

import enum

`

3

5

`from . import coroutines

`

4

6

`from . import events

`

5

7

`from . import tasks

`

6

8

``

7

9

``

``

10

`+

class _State(enum.Enum):

`

``

11

`+

CREATED = "created"

`

``

12

`+

INITIALIZED = "initialized"

`

``

13

`+

CLOSED = "closed"

`

``

14

+

``

15

+

``

16

`+

class Runner:

`

``

17

`+

"""A context manager that controls event loop life cycle.

`

``

18

+

``

19

`+

The context manager always creates a new event loop,

`

``

20

`+

allows to run async functions inside it,

`

``

21

`+

and properly finalizes the loop at the context manager exit.

`

``

22

+

``

23

`+

If debug is True, the event loop will be run in debug mode.

`

``

24

`+

If factory is passed, it is used for new event loop creation.

`

``

25

+

``

26

`+

asyncio.run(main(), debug=True)

`

``

27

+

``

28

`+

is a shortcut for

`

``

29

+

``

30

`+

with asyncio.Runner(debug=True) as runner:

`

``

31

`+

runner.run(main())

`

``

32

+

``

33

`+

The run() method can be called multiple times within the runner's context.

`

``

34

+

``

35

`+

This can be useful for interactive console (e.g. IPython),

`

``

36

`+

unittest runners, console tools, -- everywhere when async code

`

``

37

`+

is called from existing sync framework and where the preferred single

`

``

38

`+

asyncio.run() call doesn't work.

`

``

39

+

``

40

`+

"""

`

``

41

+

``

42

`+

Note: the class is final, it is not intended for inheritance.

`

``

43

+

``

44

`+

def init(self, *, debug=None, factory=None):

`

``

45

`+

self._state = _State.CREATED

`

``

46

`+

self._debug = debug

`

``

47

`+

self._factory = factory

`

``

48

`+

self._loop = None

`

``

49

`+

self._context = None

`

``

50

+

``

51

`+

def enter(self):

`

``

52

`+

self._lazy_init()

`

``

53

`+

return self

`

``

54

+

``

55

`+

def exit(self, exc_type, exc_val, exc_tb):

`

``

56

`+

self.close()

`

``

57

+

``

58

`+

def close(self):

`

``

59

`+

"""Shutdown and close event loop."""

`

``

60

`+

if self._state is not _State.INITIALIZED:

`

``

61

`+

return

`

``

62

`+

try:

`

``

63

`+

loop = self._loop

`

``

64

`+

_cancel_all_tasks(loop)

`

``

65

`+

loop.run_until_complete(loop.shutdown_asyncgens())

`

``

66

`+

loop.run_until_complete(loop.shutdown_default_executor())

`

``

67

`+

finally:

`

``

68

`+

loop.close()

`

``

69

`+

self._loop = None

`

``

70

`+

self._state = _State.CLOSED

`

``

71

+

``

72

`+

def get_loop(self):

`

``

73

`+

"""Return embedded event loop."""

`

``

74

`+

self._lazy_init()

`

``

75

`+

return self._loop

`

``

76

+

``

77

`+

def run(self, coro, *, context=None):

`

``

78

`+

"""Run a coroutine inside the embedded event loop."""

`

``

79

`+

if not coroutines.iscoroutine(coro):

`

``

80

`+

raise ValueError("a coroutine was expected, got {!r}".format(coro))

`

``

81

+

``

82

`+

if events._get_running_loop() is not None:

`

``

83

`+

fail fast with short traceback

`

``

84

`+

raise RuntimeError(

`

``

85

`+

"Runner.run() cannot be called from a running event loop")

`

``

86

+

``

87

`+

self._lazy_init()

`

``

88

+

``

89

`+

if context is None:

`

``

90

`+

context = self._context

`

``

91

`+

task = self._loop.create_task(coro, context=context)

`

``

92

`+

return self._loop.run_until_complete(task)

`

``

93

+

``

94

`+

def _lazy_init(self):

`

``

95

`+

if self._state is _State.CLOSED:

`

``

96

`+

raise RuntimeError("Runner is closed")

`

``

97

`+

if self._state is _State.INITIALIZED:

`

``

98

`+

return

`

``

99

`+

if self._factory is None:

`

``

100

`+

self._loop = events.new_event_loop()

`

``

101

`+

else:

`

``

102

`+

self._loop = self._factory()

`

``

103

`+

if self._debug is not None:

`

``

104

`+

self._loop.set_debug(self._debug)

`

``

105

`+

self._context = contextvars.copy_context()

`

``

106

`+

self._state = _State.INITIALIZED

`

``

107

+

``

108

+

``

109

+

8

110

`def run(main, *, debug=None):

`

9

111

`"""Execute the coroutine and return the result.

`

10

112

``

`@@ -30,26 +132,12 @@ async def main():

`

30

132

` asyncio.run(main())

`

31

133

` """

`

32

134

`if events._get_running_loop() is not None:

`

``

135

`+

fail fast with short traceback

`

33

136

`raise RuntimeError(

`

34

137

`"asyncio.run() cannot be called from a running event loop")

`

35

138

``

36

``

`-

if not coroutines.iscoroutine(main):

`

37

``

`-

raise ValueError("a coroutine was expected, got {!r}".format(main))

`

38

``

-

39

``

`-

loop = events.new_event_loop()

`

40

``

`-

try:

`

41

``

`-

events.set_event_loop(loop)

`

42

``

`-

if debug is not None:

`

43

``

`-

loop.set_debug(debug)

`

44

``

`-

return loop.run_until_complete(main)

`

45

``

`-

finally:

`

46

``

`-

try:

`

47

``

`-

_cancel_all_tasks(loop)

`

48

``

`-

loop.run_until_complete(loop.shutdown_asyncgens())

`

49

``

`-

loop.run_until_complete(loop.shutdown_default_executor())

`

50

``

`-

finally:

`

51

``

`-

events.set_event_loop(None)

`

52

``

`-

loop.close()

`

``

139

`+

with Runner(debug=debug) as runner:

`

``

140

`+

return runner.run(main)

`

53

141

``

54

142

``

55

143

`def _cancel_all_tasks(loop):

`