modAL 1.2.5.dev0__py3-none-any.whl → 1.2.5.dev2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,27 +15,19 @@ if telemetry_socket:
15
15
  instrument_imports(telemetry_socket)
16
16
 
17
17
  import asyncio
18
- import inspect
19
18
  import queue
20
19
  import signal
21
- import sys
22
20
  import threading
23
21
  import time
24
- from collections.abc import Sequence
25
- from typing import TYPE_CHECKING, Any, Callable, Optional
22
+ import types
23
+ from typing import TYPE_CHECKING, Any, Optional, cast
26
24
 
27
25
  from google.protobuf.message import Message
28
26
 
29
27
  from modal._clustered_functions import initialize_clustered_function
30
- from modal._partial_function import (
31
- _find_callables_for_obj,
32
- _PartialFunctionFlags,
33
- )
28
+ from modal._runtime.user_code_event_loop import UserCodeEventLoop
34
29
  from modal._serialization import deserialize, deserialize_params
35
30
  from modal._utils.async_utils import TaskContext, aclosing, synchronizer
36
- from modal._utils.function_utils import (
37
- callable_has_non_self_params,
38
- )
39
31
  from modal.app import App, _App
40
32
  from modal.client import Client, _Client
41
33
  from modal.config import logger
@@ -53,6 +45,7 @@ from ._runtime.container_io_manager import (
53
45
  if TYPE_CHECKING:
54
46
  import modal._object
55
47
  import modal._runtime.container_io_manager
48
+ import modal._runtime.user_code_imports
56
49
 
57
50
 
58
51
  class DaemonizedThreadPool:
@@ -101,81 +94,6 @@ class DaemonizedThreadPool:
101
94
  self.inputs.put((func, args))
102
95
 
103
96
 
104
- class UserCodeEventLoop:
105
- """Run an async event loop as a context manager and handle signals.
106
-
107
- This will run all *user supplied* async code, i.e. async functions, as well as async enter/exit managers
108
-
109
- The following signals are handled while a coroutine is running on the event loop until
110
- completion (and then handlers are deregistered):
111
-
112
- - `SIGUSR1`: converted to an async task cancellation. Note that this only affects the event
113
- loop, and the signal handler defined here doesn't run for sync functions.
114
- - `SIGINT`: Unless the global signal handler has been set to SIGIGN, the loop's signal handler
115
- is set to cancel the current task and raise KeyboardInterrupt to the caller.
116
- """
117
-
118
- def __enter__(self):
119
- self.loop = asyncio.new_event_loop()
120
- self.tasks = set()
121
- return self
122
-
123
- def __exit__(self, exc_type, exc_value, traceback):
124
- self.loop.run_until_complete(self.loop.shutdown_asyncgens())
125
- if sys.version_info[:2] >= (3, 9):
126
- self.loop.run_until_complete(self.loop.shutdown_default_executor()) # Introduced in Python 3.9
127
-
128
- for task in self.tasks:
129
- task.cancel()
130
-
131
- self.loop.close()
132
-
133
- def create_task(self, coro):
134
- task = self.loop.create_task(coro)
135
- self.tasks.add(task)
136
- task.add_done_callback(self.tasks.discard)
137
- return task
138
-
139
- def run(self, coro):
140
- task = asyncio.ensure_future(coro, loop=self.loop)
141
- self._sigints = 0
142
-
143
- def _sigint_handler():
144
- # cancel the task in order to have run_until_complete return soon and
145
- # prevent a bunch of unwanted tracebacks when shutting down the
146
- # event loop.
147
-
148
- # this basically replicates the sigint handler installed by asyncio.run()
149
- self._sigints += 1
150
- if self._sigints == 1:
151
- # first sigint is graceful
152
- task.cancel()
153
- return
154
-
155
- # this should normally not happen, but the second sigint would "hard kill" the event loop!
156
- raise KeyboardInterrupt()
157
-
158
- ignore_sigint = signal.getsignal(signal.SIGINT) == signal.SIG_IGN
159
- if not ignore_sigint:
160
- self.loop.add_signal_handler(signal.SIGINT, _sigint_handler)
161
-
162
- # Before Python 3.9 there is no argument to Task.cancel
163
- if sys.version_info[:2] >= (3, 9):
164
- self.loop.add_signal_handler(signal.SIGUSR1, task.cancel, "Input was cancelled by user")
165
- else:
166
- self.loop.add_signal_handler(signal.SIGUSR1, task.cancel)
167
-
168
- try:
169
- return self.loop.run_until_complete(task)
170
- except asyncio.CancelledError:
171
- if self._sigints > 0:
172
- raise KeyboardInterrupt()
173
- finally:
174
- self.loop.remove_signal_handler(signal.SIGUSR1)
175
- if not ignore_sigint:
176
- self.loop.remove_signal_handler(signal.SIGINT)
177
-
178
-
179
97
  def call_function(
180
98
  user_code_event_loop: UserCodeEventLoop,
181
99
  container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
@@ -331,23 +249,25 @@ def call_function(
331
249
  signal.signal(signal.SIGUSR1, usr1_handler) # reset signal handler
332
250
 
333
251
 
334
- def call_lifecycle_functions(
335
- event_loop: UserCodeEventLoop,
336
- container_io_manager, #: ContainerIOManager, TODO: this type is generated at runtime
337
- funcs: Sequence[Callable[..., Any]],
338
- ) -> None:
339
- """Call function(s), can be sync or async, but any return values are ignored."""
340
- with container_io_manager.handle_user_exception():
341
- for func in funcs:
342
- # We are deprecating parametrized exit methods but want to gracefully handle old code.
343
- # We can remove this once the deprecation in the actual @exit decorator is enforced.
344
- args = (None, None, None) if callable_has_non_self_params(func) else ()
345
- # in case func is non-async, it's executed here and sigint will by default
346
- # interrupt it using a KeyboardInterrupt exception
347
- res = func(*args)
348
- if inspect.iscoroutine(res):
349
- # if however func is async, we have to jump through some hoops
350
- event_loop.run(res)
252
+ def get_serialized_user_class_and_function(
253
+ function_def: api_pb2.Function, client: _Client
254
+ ) -> tuple[Optional[type], Optional[types.FunctionType]]:
255
+ if function_def.definition_type == api_pb2.Function.DEFINITION_TYPE_SERIALIZED:
256
+ assert function_def.function_serialized or function_def.class_serialized
257
+
258
+ if function_def.function_serialized:
259
+ ser_fun = deserialize(function_def.function_serialized, client)
260
+ else:
261
+ ser_fun = None
262
+
263
+ if function_def.class_serialized:
264
+ ser_usr_cls = deserialize(function_def.class_serialized, client)
265
+ else:
266
+ ser_usr_cls = None
267
+ else:
268
+ ser_usr_cls, ser_fun = None, None
269
+
270
+ return ser_usr_cls, ser_fun
351
271
 
352
272
 
353
273
  def main(container_args: api_pb2.ContainerArguments, client: Client):
@@ -357,34 +277,20 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
357
277
  active_app: _App
358
278
  service: Service
359
279
  function_def = container_args.function_def
360
- is_auto_snapshot: bool = function_def.is_auto_snapshot
361
280
  # The worker sets this flag to "1" for snapshot and restore tasks. Otherwise, this flag is unset,
362
281
  # in which case snapshots should be disabled.
363
282
  is_snapshotting_function = (
364
283
  function_def.is_checkpointing_function and os.environ.get("MODAL_ENABLE_SNAP_RESTORE") == "1"
365
284
  )
366
285
 
367
- _client: _Client = synchronizer._translate_in(client) # TODO(erikbern): ugly
286
+ _client: _Client = cast(_Client, synchronizer._translate_in(client)) # TODO(erikbern): ugly
368
287
 
369
288
  # Call ContainerHello - currently a noop but might be used later for things
370
289
  container_io_manager.hello()
371
290
 
372
291
  with container_io_manager.heartbeats(is_snapshotting_function), UserCodeEventLoop() as event_loop:
373
292
  # If this is a serialized function, fetch the definition from the server
374
- if function_def.definition_type == api_pb2.Function.DEFINITION_TYPE_SERIALIZED:
375
- assert function_def.function_serialized or function_def.class_serialized
376
-
377
- if function_def.function_serialized:
378
- ser_fun = deserialize(function_def.function_serialized, _client)
379
- else:
380
- ser_fun = None
381
-
382
- if function_def.class_serialized:
383
- ser_usr_cls = deserialize(function_def.class_serialized, _client)
384
- else:
385
- ser_usr_cls = None
386
- else:
387
- ser_usr_cls, ser_fun = None, None
293
+ ser_usr_cls, ser_fun = get_serialized_user_class_and_function(function_def, _client)
388
294
 
389
295
  # Initialize the function, importing user code.
390
296
  with container_io_manager.handle_user_exception():
@@ -436,7 +342,7 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
436
342
 
437
343
  # Initialize objects on the app.
438
344
  # This is basically only functions and classes - anything else is deprecated and will be unsupported soon
439
- app: App = synchronizer._translate_out(active_app)
345
+ app: App = cast(App, synchronizer._translate_out(active_app))
440
346
  app._init_container(client, container_app)
441
347
 
442
348
  # Hydrate all function dependencies.
@@ -456,7 +362,7 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
456
362
  "that evaluates differently in the local and remote environments."
457
363
  )
458
364
  for object_id, obj in zip(dep_object_ids, service.service_deps):
459
- metadata: Message = container_app.object_handle_metadata[object_id]
365
+ metadata: Optional[Message] = container_app.object_handle_metadata[object_id]
460
366
  obj._hydrate(object_id, _client, metadata)
461
367
 
462
368
  # Initialize clustered functions.
@@ -467,91 +373,8 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
467
373
  function_def._experimental_group_size,
468
374
  )
469
375
 
470
- # Identify all "enter" methods that need to run before we snapshot.
471
- if service.user_cls_instance is not None and not is_auto_snapshot:
472
- pre_snapshot_methods = _find_callables_for_obj(
473
- service.user_cls_instance, _PartialFunctionFlags.ENTER_PRE_SNAPSHOT
474
- )
475
- call_lifecycle_functions(event_loop, container_io_manager, list(pre_snapshot_methods.values()))
476
-
477
- # If this container is being used to create a checkpoint, checkpoint the container after
478
- # global imports and initialization. Checkpointed containers run from this point onwards.
479
- if is_snapshotting_function:
480
- container_io_manager.memory_snapshot()
481
-
482
- # Install hooks for interactive functions.
483
- def breakpoint_wrapper():
484
- # note: it would be nice to not have breakpoint_wrapper() included in the backtrace
485
- container_io_manager.interact(from_breakpoint=True)
486
- import pdb
487
-
488
- frame = inspect.currentframe().f_back
489
-
490
- pdb.Pdb().set_trace(frame)
491
-
492
- sys.breakpointhook = breakpoint_wrapper
493
-
494
- # Identify the "enter" methods to run after resuming from a snapshot.
495
- if service.user_cls_instance is not None and not is_auto_snapshot:
496
- post_snapshot_methods = _find_callables_for_obj(
497
- service.user_cls_instance, _PartialFunctionFlags.ENTER_POST_SNAPSHOT
498
- )
499
- call_lifecycle_functions(event_loop, container_io_manager, list(post_snapshot_methods.values()))
500
-
501
- with container_io_manager.handle_user_exception():
502
- finalized_functions = service.get_finalized_functions(function_def, container_io_manager)
503
- # Execute the function.
504
- lifespan_background_tasks = []
505
- try:
506
- for finalized_function in finalized_functions.values():
507
- if finalized_function.lifespan_manager:
508
- lifespan_background_tasks.append(
509
- event_loop.create_task(finalized_function.lifespan_manager.background_task())
510
- )
511
- with container_io_manager.handle_user_exception():
512
- event_loop.run(finalized_function.lifespan_manager.lifespan_startup())
513
- call_function(
514
- event_loop,
515
- container_io_manager,
516
- finalized_functions,
517
- batch_max_size,
518
- batch_wait_ms,
519
- )
520
- finally:
521
- # Run exit handlers. From this point onward, ignore all SIGINT signals that come from
522
- # graceful shutdowns originating on the worker, as well as stray SIGUSR1 signals that
523
- # may have been sent to cancel inputs.
524
- int_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
525
- usr1_handler = signal.signal(signal.SIGUSR1, signal.SIG_IGN)
526
-
527
- try:
528
- try:
529
- # run lifespan shutdown for asgi apps
530
- for finalized_function in finalized_functions.values():
531
- if finalized_function.lifespan_manager:
532
- with container_io_manager.handle_user_exception():
533
- event_loop.run(finalized_function.lifespan_manager.lifespan_shutdown())
534
- finally:
535
- # no need to keep the lifespan asgi call around - we send it no more messages
536
- for lifespan_background_task in lifespan_background_tasks:
537
- lifespan_background_task.cancel() # prevent dangling tasks
538
-
539
- # Identify "exit" methods and run them.
540
- # want to make sure this is called even if the lifespan manager fails
541
- if service.user_cls_instance is not None and not is_auto_snapshot:
542
- exit_methods = _find_callables_for_obj(service.user_cls_instance, _PartialFunctionFlags.EXIT)
543
- call_lifecycle_functions(event_loop, container_io_manager, list(exit_methods.values()))
544
-
545
- # Finally, commit on exit to catch uncommitted volume changes and surface background
546
- # commit errors.
547
- container_io_manager.volume_commit(
548
- [v.volume_id for v in function_def.volume_mounts if v.allow_background_commits]
549
- )
550
- finally:
551
- # Restore the original signal handler, needed for container_test hygiene since the
552
- # test runs `main()` multiple times in the same process.
553
- signal.signal(signal.SIGINT, int_handler)
554
- signal.signal(signal.SIGUSR1, usr1_handler)
376
+ with service.execution_context(event_loop, container_io_manager) as finalized_functions:
377
+ call_function(event_loop, container_io_manager, finalized_functions, batch_max_size, batch_wait_ms)
555
378
 
556
379
 
557
380
  if __name__ == "__main__":
@@ -0,0 +1,80 @@
1
+ # Copyright Modal Labs 2022
2
+ # ruff: noqa: E402
3
+ import asyncio
4
+ import signal
5
+ import sys
6
+
7
+
8
+ class UserCodeEventLoop:
9
+ """Run an async event loop as a context manager and handle signals.
10
+
11
+ This will run all *user supplied* async code, i.e. async functions, as well as async enter/exit managers
12
+
13
+ The following signals are handled while a coroutine is running on the event loop until
14
+ completion (and then handlers are deregistered):
15
+
16
+ - `SIGUSR1`: converted to an async task cancellation. Note that this only affects the event
17
+ loop, and the signal handler defined here doesn't run for sync functions.
18
+ - `SIGINT`: Unless the global signal handler has been set to SIGIGN, the loop's signal handler
19
+ is set to cancel the current task and raise KeyboardInterrupt to the caller.
20
+ """
21
+
22
+ def __enter__(self):
23
+ self.loop = asyncio.new_event_loop()
24
+ self.tasks = set()
25
+ return self
26
+
27
+ def __exit__(self, exc_type, exc_value, traceback):
28
+ self.loop.run_until_complete(self.loop.shutdown_asyncgens())
29
+ if sys.version_info[:2] >= (3, 9):
30
+ self.loop.run_until_complete(self.loop.shutdown_default_executor()) # Introduced in Python 3.9
31
+
32
+ for task in self.tasks:
33
+ task.cancel()
34
+
35
+ self.loop.close()
36
+
37
+ def create_task(self, coro):
38
+ task = self.loop.create_task(coro)
39
+ self.tasks.add(task)
40
+ task.add_done_callback(self.tasks.discard)
41
+ return task
42
+
43
+ def run(self, coro):
44
+ task = asyncio.ensure_future(coro, loop=self.loop)
45
+ self._sigints = 0
46
+
47
+ def _sigint_handler():
48
+ # cancel the task in order to have run_until_complete return soon and
49
+ # prevent a bunch of unwanted tracebacks when shutting down the
50
+ # event loop.
51
+
52
+ # this basically replicates the sigint handler installed by asyncio.run()
53
+ self._sigints += 1
54
+ if self._sigints == 1:
55
+ # first sigint is graceful
56
+ task.cancel()
57
+ return
58
+
59
+ # this should normally not happen, but the second sigint would "hard kill" the event loop!
60
+ raise KeyboardInterrupt()
61
+
62
+ ignore_sigint = signal.getsignal(signal.SIGINT) == signal.SIG_IGN
63
+ if not ignore_sigint:
64
+ self.loop.add_signal_handler(signal.SIGINT, _sigint_handler)
65
+
66
+ # Before Python 3.9 there is no argument to Task.cancel
67
+ if sys.version_info[:2] >= (3, 9):
68
+ self.loop.add_signal_handler(signal.SIGUSR1, task.cancel, "Input was cancelled by user")
69
+ else:
70
+ self.loop.add_signal_handler(signal.SIGUSR1, task.cancel)
71
+
72
+ try:
73
+ return self.loop.run_until_complete(task)
74
+ except asyncio.CancelledError:
75
+ if self._sigints > 0:
76
+ raise KeyboardInterrupt()
77
+ finally:
78
+ self.loop.remove_signal_handler(signal.SIGUSR1)
79
+ if not ignore_sigint:
80
+ self.loop.remove_signal_handler(signal.SIGINT)
@@ -1,17 +1,32 @@
1
1
  # Copyright Modal Labs 2024
2
2
  import importlib
3
+ import inspect
4
+ import os
5
+ import signal
6
+ import sys
3
7
  import typing
4
8
  from abc import ABCMeta, abstractmethod
9
+ from contextlib import contextmanager
5
10
  from dataclasses import dataclass
6
- from typing import Any, Callable, Optional, Sequence
11
+ from typing import Any, Callable, Generator, Optional, Sequence
7
12
 
8
13
  import modal._object
9
14
  import modal._runtime.container_io_manager
10
15
  import modal.cls
11
16
  from modal import Function
12
17
  from modal._functions import _Function
18
+ from modal._partial_function import (
19
+ _find_callables_for_obj,
20
+ _PartialFunctionFlags,
21
+ )
22
+ from modal._runtime.user_code_event_loop import UserCodeEventLoop
13
23
  from modal._utils.async_utils import synchronizer
14
- from modal._utils.function_utils import LocalFunctionError, is_async as get_is_async, is_global_object
24
+ from modal._utils.function_utils import (
25
+ LocalFunctionError,
26
+ callable_has_non_self_params,
27
+ is_async as get_is_async,
28
+ is_global_object,
29
+ )
15
30
  from modal.app import _App
16
31
  from modal.config import logger
17
32
  from modal.exception import ExecutionError, InvalidError
@@ -33,6 +48,74 @@ class FinalizedFunction:
33
48
  lifespan_manager: Optional["LifespanManager"] = None
34
49
 
35
50
 
51
+ def call_lifecycle_functions(
52
+ event_loop: UserCodeEventLoop,
53
+ container_io_manager: Any,
54
+ funcs: Sequence[Callable[..., Any]],
55
+ ) -> None:
56
+ """Call function(s), can be sync or async, but any return values are ignored."""
57
+ with container_io_manager.handle_user_exception():
58
+ for func in funcs:
59
+ # We are deprecating parametrized exit methods but want to gracefully handle old code.
60
+ args = (None, None, None) if callable_has_non_self_params(func) else ()
61
+ res = func(*args)
62
+ if inspect.iscoroutine(res):
63
+ event_loop.run(res)
64
+
65
+
66
+ @contextmanager
67
+ def _run_service_lifecycle(
68
+ event_loop: UserCodeEventLoop,
69
+ container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
70
+ function_def: api_pb2.Function,
71
+ finalized_functions: dict[str, FinalizedFunction],
72
+ exit_callback: Optional[Callable[[], None]],
73
+ ) -> Generator[None, None, None]:
74
+ lifespan_background_tasks = []
75
+ try:
76
+ for finalized_function in finalized_functions.values():
77
+ if finalized_function.lifespan_manager:
78
+ lifespan_background_tasks.append(
79
+ event_loop.create_task(finalized_function.lifespan_manager.background_task())
80
+ )
81
+ with container_io_manager.handle_user_exception():
82
+ event_loop.run(finalized_function.lifespan_manager.lifespan_startup())
83
+ yield
84
+ finally:
85
+ # Run exit handlers. From this point onward, ignore all SIGINT signals that come from
86
+ # graceful shutdowns originating on the worker, as well as stray SIGUSR1 signals that
87
+ # may have been sent to cancel inputs.
88
+ int_handler = signal.signal(signal.SIGINT, signal.SIG_IGN)
89
+ usr1_handler = signal.signal(signal.SIGUSR1, signal.SIG_IGN)
90
+ try:
91
+ try:
92
+ # run lifespan shutdown for asgi apps
93
+ for finalized_function in finalized_functions.values():
94
+ if finalized_function.lifespan_manager:
95
+ with container_io_manager.handle_user_exception():
96
+ event_loop.run(finalized_function.lifespan_manager.lifespan_shutdown())
97
+ finally:
98
+ # no need to keep the lifespan asgi call around - we send it no more messages
99
+ for task in lifespan_background_tasks:
100
+ task.cancel()
101
+
102
+ # Identify "exit" methods and run them.
103
+ # want to make sure this is called even if the lifespan manager fails
104
+ if exit_callback:
105
+ exit_callback()
106
+
107
+ # Finally, commit on exit to catch uncommitted volume changes and surface background
108
+ # commit errors.
109
+ container_io_manager.volume_commit(
110
+ [v.volume_id for v in function_def.volume_mounts if v.allow_background_commits]
111
+ )
112
+ finally:
113
+ # Restore the original signal handler, needed for container_test hygiene since the
114
+ # test runs `main()` multiple times in the same process.
115
+ signal.signal(signal.SIGINT, int_handler)
116
+ signal.signal(signal.SIGUSR1, usr1_handler)
117
+
118
+
36
119
  class Service(metaclass=ABCMeta):
37
120
  """Common interface for singular functions and class-based "services"
38
121
 
@@ -50,6 +133,24 @@ class Service(metaclass=ABCMeta):
50
133
  self, fun_def: api_pb2.Function, container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"
51
134
  ) -> dict[str, "FinalizedFunction"]: ...
52
135
 
136
+ @contextmanager
137
+ @abstractmethod
138
+ def execution_context(
139
+ self,
140
+ event_loop: UserCodeEventLoop,
141
+ container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
142
+ ) -> Generator[dict[str, "FinalizedFunction"], None, None]:
143
+ """
144
+ Manages the lifecycle of the user code:
145
+ 1. Runs pre-snapshot 'enter' methods
146
+ 2. Calls maybe_snapshot(container_io_manager, function_def)
147
+ 3. Creates breakpoint wrapper
148
+ 4. Runs post-snapshot 'enter' methods
149
+ 5. Initializes finalized functions (and ASGI/WSGI lifespan)
150
+ 6. Yield finalized_functions for execution
151
+ 7. Handles cleanup (lifespan shutdown, 'exit' methods)
152
+ """
153
+
53
154
 
54
155
  def construct_webhook_callable(
55
156
  user_defined_callable: Callable,
@@ -91,11 +192,36 @@ def construct_webhook_callable(
91
192
  raise InvalidError(f"Unrecognized web endpoint type {webhook_config.type}")
92
193
 
93
194
 
195
+ def maybe_snapshot(
196
+ container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager", function_def: api_pb2.Function
197
+ ):
198
+ if function_def.is_checkpointing_function and os.environ.get("MODAL_ENABLE_SNAP_RESTORE") == "1":
199
+ container_io_manager.memory_snapshot()
200
+
201
+
202
+ def create_breakpoint_wrapper(container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"):
203
+ # Install hooks for interactive functions.
204
+ def breakpoint_wrapper():
205
+ # note: it would be nice to not have breakpoint_wrapper() included in the backtrace
206
+ container_io_manager.interact(from_breakpoint=True)
207
+ import pdb
208
+
209
+ current_frame = inspect.currentframe()
210
+ if current_frame is not None:
211
+ frame = current_frame.f_back
212
+ pdb.Pdb().set_trace(frame)
213
+ else:
214
+ raise RuntimeError("No current frame found")
215
+
216
+ sys.breakpointhook = breakpoint_wrapper
217
+
218
+
94
219
  @dataclass
95
220
  class ImportedFunction(Service):
96
221
  app: modal.app._App
97
222
  service_deps: Optional[Sequence["modal._object._Object"]]
98
223
  user_cls_instance = None
224
+ function_def: api_pb2.Function
99
225
 
100
226
  _user_defined_callable: Callable[..., Any]
101
227
 
@@ -138,6 +264,29 @@ class ImportedFunction(Service):
138
264
  )
139
265
  }
140
266
 
267
+ @contextmanager
268
+ def execution_context(
269
+ self,
270
+ event_loop: UserCodeEventLoop,
271
+ container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
272
+ ) -> Generator[dict[str, "FinalizedFunction"], None, None]:
273
+ # If this container is being used to create a checkpoint, checkpoint the container after
274
+ # global imports and initialization. Checkpointed containers run from this point onwards.
275
+ maybe_snapshot(container_io_manager, self.function_def)
276
+ create_breakpoint_wrapper(container_io_manager)
277
+
278
+ with container_io_manager.handle_user_exception():
279
+ finalized_functions = self.get_finalized_functions(self.function_def, container_io_manager)
280
+
281
+ with _run_service_lifecycle(
282
+ event_loop,
283
+ container_io_manager,
284
+ self.function_def,
285
+ finalized_functions,
286
+ None,
287
+ ):
288
+ yield finalized_functions
289
+
141
290
 
142
291
  @dataclass
143
292
  class ImportedClass(Service):
@@ -146,6 +295,7 @@ class ImportedClass(Service):
146
295
  service_deps: Optional[Sequence["modal._object._Object"]]
147
296
 
148
297
  _partial_functions: dict[str, "modal._partial_function._PartialFunction"]
298
+ function_def: api_pb2.Function
149
299
 
150
300
  def get_finalized_functions(
151
301
  self, fun_def: api_pb2.Function, container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager"
@@ -189,6 +339,54 @@ class ImportedClass(Service):
189
339
  finalized_functions[method_name] = finalized_function
190
340
  return finalized_functions
191
341
 
342
+ @contextmanager
343
+ def execution_context(
344
+ self,
345
+ event_loop: UserCodeEventLoop,
346
+ container_io_manager: "modal._runtime.container_io_manager.ContainerIOManager",
347
+ ) -> Generator[dict[str, "FinalizedFunction"], None, None]:
348
+ # 1. Pre-snapshot Enter
349
+ # Identify all "enter" methods that need to run before we snapshot.
350
+ if not self.function_def.is_auto_snapshot:
351
+ pre_snapshot_methods = _find_callables_for_obj(
352
+ self.user_cls_instance, _PartialFunctionFlags.ENTER_PRE_SNAPSHOT
353
+ )
354
+ call_lifecycle_functions(event_loop, container_io_manager, list(pre_snapshot_methods.values()))
355
+
356
+ # 2. Snapshot
357
+ # If this container is being used to create a checkpoint, checkpoint the container after
358
+ # global imports and initialization. Checkpointed containers run from this point onwards.
359
+ maybe_snapshot(container_io_manager, self.function_def)
360
+
361
+ # 3. Breakpoint wrapper
362
+ create_breakpoint_wrapper(container_io_manager)
363
+
364
+ # 4. Post-snapshot Enter
365
+ # Identify the "enter" methods to run after resuming from a snapshot.
366
+ if not self.function_def.is_auto_snapshot:
367
+ post_snapshot_methods = _find_callables_for_obj(
368
+ self.user_cls_instance, _PartialFunctionFlags.ENTER_POST_SNAPSHOT
369
+ )
370
+ call_lifecycle_functions(event_loop, container_io_manager, list(post_snapshot_methods.values()))
371
+
372
+ # 5. Get Functions & Start Lifespan
373
+ with container_io_manager.handle_user_exception():
374
+ finalized_functions = self.get_finalized_functions(self.function_def, container_io_manager)
375
+
376
+ def exit_callback():
377
+ if not self.function_def.is_auto_snapshot:
378
+ exit_methods = _find_callables_for_obj(self.user_cls_instance, _PartialFunctionFlags.EXIT)
379
+ call_lifecycle_functions(event_loop, container_io_manager, list(exit_methods.values()))
380
+
381
+ with _run_service_lifecycle(
382
+ event_loop,
383
+ container_io_manager,
384
+ self.function_def,
385
+ finalized_functions,
386
+ exit_callback,
387
+ ):
388
+ yield finalized_functions
389
+
192
390
 
193
391
  def get_user_class_instance(_cls: modal.cls._Cls, args: tuple[Any, ...], kwargs: dict[str, Any]) -> typing.Any:
194
392
  """Returns instance of the underlying class to be used as the `self`
@@ -266,9 +464,10 @@ def import_single_function_service(
266
464
  active_app = get_active_app_fallback(function_def)
267
465
 
268
466
  return ImportedFunction(
269
- active_app,
270
- service_deps,
271
- user_defined_callable,
467
+ app=active_app,
468
+ service_deps=service_deps,
469
+ function_def=function_def,
470
+ _user_defined_callable=user_defined_callable,
272
471
  )
273
472
 
274
473
 
@@ -338,11 +537,12 @@ def import_class_service(
338
537
  user_cls_instance = get_user_class_instance(_cls, cls_args, cls_kwargs)
339
538
 
340
539
  return ImportedClass(
341
- user_cls_instance,
342
- active_app,
343
- service_deps,
540
+ user_cls_instance=user_cls_instance,
541
+ app=active_app,
542
+ service_deps=service_deps,
344
543
  # TODO (elias/deven): instead of using method_partials here we should use a set of api_pb2.MethodDefinition
345
- method_partials,
544
+ _partial_functions=method_partials,
545
+ function_def=function_def,
346
546
  )
347
547
 
348
548
 
modal/client.pyi CHANGED
@@ -32,7 +32,7 @@ class _Client:
32
32
  server_url: str,
33
33
  client_type: int,
34
34
  credentials: typing.Optional[tuple[str, str]],
35
- version: str = "1.2.5.dev0",
35
+ version: str = "1.2.5.dev2",
36
36
  ):
37
37
  """mdmd:hidden
38
38
  The Modal client object is not intended to be instantiated directly by users.
@@ -163,7 +163,7 @@ class Client:
163
163
  server_url: str,
164
164
  client_type: int,
165
165
  credentials: typing.Optional[tuple[str, str]],
166
- version: str = "1.2.5.dev0",
166
+ version: str = "1.2.5.dev2",
167
167
  ):
168
168
  """mdmd:hidden
169
169
  The Modal client object is not intended to be instantiated directly by users.
@@ -85,6 +85,19 @@ async def list_deployed_apps(environment_name: str = "", client: Optional[_Clien
85
85
  return app_infos
86
86
 
87
87
 
88
+ @synchronizer.create_blocking
89
+ async def stop_app(name: str, *, environment_name: Optional[str] = None, client: Optional[_Client] = None) -> None:
90
+ """Stop a deployed App.
91
+
92
+ This interface is experimental and may change in the future,
93
+ although the functionality will continue to be supported.
94
+ """
95
+ client_ = client or await _Client.from_env()
96
+ app = await _App.lookup(name, environment_name=environment_name, client=client_)
97
+ req = api_pb2.AppStopRequest(app_id=app.app_id, source=api_pb2.APP_STOP_SOURCE_PYTHON_CLIENT)
98
+ await client_.stub.AppStop(req)
99
+
100
+
88
101
  @synchronizer.create_blocking
89
102
  async def get_app_objects(
90
103
  app_name: str, *, environment_name: Optional[str] = None, client: Optional[_Client] = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: modal
3
- Version: 1.2.5.dev0
3
+ Version: 1.2.5.dev2
4
4
  Summary: Python client library for Modal
5
5
  Author-email: Modal Labs <support@modal.com>
6
6
  License: Apache-2.0
@@ -3,7 +3,7 @@ modal/__main__.py,sha256=45H-GtwzaDfN-1nP4_HYvzN3s7AG_HXR4-ynrsjO_OI,2803
3
3
  modal/_billing.py,sha256=C1jUN9f_1WqozSZAt9EOk1nImXUdiLrgFeeAu3R23cI,3012
4
4
  modal/_clustered_functions.py,sha256=7amiOxErtlciWeBJBL2KvaxerUomJpTAJ3qElflFHQA,2884
5
5
  modal/_clustered_functions.pyi,sha256=JmYwAGOLEnD5AF-gYF9O5tu-SgGjeoJz-X1j48b1Ijg,1157
6
- modal/_container_entrypoint.py,sha256=zYk5mC8_IMx4kWnyFMRHKOrFRLBWchy6x-py3M8FYEw,28084
6
+ modal/_container_entrypoint.py,sha256=aRuWG1xl3kDy28CfQrqQ-LU8liX2W_bwm2avVOE63Tw,19812
7
7
  modal/_functions.py,sha256=fnmNMS4nGi-YvbWPn6nGt4RYqs11JmSa0b_B1UNSwPU,91499
8
8
  modal/_grpc_client.py,sha256=KZcggVNvFL5jRQkFumshMppeFHMbaZI9Dzf13run6As,6083
9
9
  modal/_ipython.py,sha256=TW1fkVOmZL3YYqdS2YlM1hqpf654Yf8ZyybHdBnlhSw,301
@@ -26,7 +26,7 @@ modal/app.pyi,sha256=nI_Tibj-rI5I7dJFJnczbHvcmVzCvvCih0tMzoQlr_Y,50835
26
26
  modal/billing.py,sha256=zmQ3bcCJlwa4KD1IA_QgdWpm1pn13c-7qfy79iEauYI,195
27
27
  modal/call_graph.py,sha256=1g2DGcMIJvRy-xKicuf63IVE98gJSnQsr8R_NVMptNc,2581
28
28
  modal/client.py,sha256=k1CqQ_0t4-vHylkqYZgYCnClg-FT0QrTPAKivL01A_k,14354
29
- modal/client.pyi,sha256=6JQpR6YUw22b9TRuqx9FZYKMIZyqF7cLd7v3ed_JJJ8,13146
29
+ modal/client.pyi,sha256=DDiMjmFSIARj1BaJNqxZtDRDkt1GM2jSvnvh06g7hxo,13146
30
30
  modal/cloud_bucket_mount.py,sha256=cvVj5Rt4nDHoQbTneoJPTaAcnl_Ab_mrsjnnzd0v9OI,6013
31
31
  modal/cloud_bucket_mount.pyi,sha256=vXusGMOUk9HjCs4lhxteuCK4xLQclPfugGWKo7LECEA,7539
32
32
  modal/cls.py,sha256=4T__mtYlQUnkatif1Wnr8Ld9DN_ySydHj2UwhRg89ok,41984
@@ -92,7 +92,8 @@ modal/_runtime/execution_context.py,sha256=AYrNQRHHXEqX2MwMf8zxelKZnYf25RE_B-NRL
92
92
  modal/_runtime/execution_context.pyi,sha256=FVzakehz72ndL-ufe8-EC7TM4IHO_MEBcAdgWuU4W9k,2426
93
93
  modal/_runtime/gpu_memory_snapshot.py,sha256=8T85kyfGJ11AnhFRCZTeWj9zin9e9f5XbrRqU8_WjAk,11955
94
94
  modal/_runtime/telemetry.py,sha256=T1RoAGyjBDr1swiM6pPsGRSITm7LI5FDK18oNXxY08U,5163
95
- modal/_runtime/user_code_imports.py,sha256=1MlOgw810aj0MeDvFPvHBIz-aHd7jUX6dwRfIcM3-KE,16498
95
+ modal/_runtime/user_code_event_loop.py,sha256=HwOuDyCZLOakr_DG7p2MrcscjSQNheIhBBuLgB5I3Cc,3014
96
+ modal/_runtime/user_code_imports.py,sha256=9mxg2FMZGR2UQfLcxEPNIiH51SBJTl_1JCCRrGMM0rI,25086
96
97
  modal/_utils/__init__.py,sha256=waLjl5c6IPDhSsdWAm9Bji4e2PVxamYABKAze6CHVXY,28
97
98
  modal/_utils/app_utils.py,sha256=88BT4TPLWfYAQwKTHcyzNQRHg8n9B-QE2UyJs96iV-0,108
98
99
  modal/_utils/async_utils.py,sha256=PnwHfaHcqjLwMnlGC2kn8nqubxpXcSKcy4jHx6477dg,30625
@@ -155,11 +156,11 @@ modal/cli/programs/launch_instance_ssh.py,sha256=GrwK_Vy8-7B4x5a6AqFaF7lqNVgu75J
155
156
  modal/cli/programs/run_jupyter.py,sha256=IJw8nds8Cjl9j4dxBqMGxhz-bIyVX0kle7jbt8HFOhM,2688
156
157
  modal/cli/programs/run_marimo.py,sha256=QlCGPkwQ0XLajt2LtujR_BGsRV1AZ3OCUgZxkkM1lug,2893
157
158
  modal/cli/programs/vscode.py,sha256=E1aJPU7b8RWWj-JX71DifsCoLYsCAnre7lATiBJjUms,3386
158
- modal/experimental/__init__.py,sha256=sCwNbBLcR2t-jhrpwtMAPGKt2WNqXBg0xkNZdyB-6CE,13940
159
+ modal/experimental/__init__.py,sha256=WlBNjzahOTuhWdfHt0OIArU3pdrErsFrprVhZ7UqU0k,14520
159
160
  modal/experimental/flash.py,sha256=0afbNEg8SLBdUbvFpT3R9FE1mfHAV_Iual8x2hqfPMY,26785
160
161
  modal/experimental/flash.pyi,sha256=BU7v9gFLWX1OanZ3HpRoVGp4qFfHo93lM9UkzcNFZUY,14832
161
162
  modal/experimental/ipython.py,sha256=TrCfmol9LGsRZMeDoeMPx3Hv3BFqQhYnmD_iH0pqdhk,2904
162
- modal-1.2.5.dev0.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
163
+ modal-1.2.5.dev2.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
163
164
  modal_docs/__init__.py,sha256=svYKtV8HDwDCN86zbdWqyq5T8sMdGDj0PVlzc2tIxDM,28
164
165
  modal_docs/gen_cli_docs.py,sha256=c1yfBS_x--gL5bs0N4ihMwqwX8l3IBWSkBAKNNIi6bQ,3801
165
166
  modal_docs/gen_reference_docs.py,sha256=d_CQUGQ0rfw28u75I2mov9AlS773z9rG40-yq5o7g2U,6359
@@ -187,10 +188,10 @@ modal_proto/task_command_router_pb2.py,sha256=_pD2ZpU0bNzhwBdzmLoLyLtAtftI_Agxwn
187
188
  modal_proto/task_command_router_pb2.pyi,sha256=EyDgXPLr7alqjXYERV8w_MPuO404x0uCppmSkrfE9IE,14589
188
189
  modal_proto/task_command_router_pb2_grpc.py,sha256=uEQ0HdrCp8v-9bB5yIic9muA8spCShLHY6Bz9cCgOUE,10114
189
190
  modal_proto/task_command_router_pb2_grpc.pyi,sha256=s3Yxsrawdj4nr8vqQqsAxyX6ilWaGbdECy425KKbLIA,3301
190
- modal_version/__init__.py,sha256=cDrHJM8riTJ8snnSygifEVrn53ihgXeZmewwxUgD6IY,120
191
+ modal_version/__init__.py,sha256=y2ih8aafsJLowH_EmA8i4Ghzhd9x2mf_p9cG3NYWFfk,120
191
192
  modal_version/__main__.py,sha256=2FO0yYQQwDTh6udt1h-cBnGd1c4ZyHnHSI4BksxzVac,105
192
- modal-1.2.5.dev0.dist-info/METADATA,sha256=8xhfLMDoSYMY_-hEl9dXQTVFmdJZZrBtouaROoxy92E,2489
193
- modal-1.2.5.dev0.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
194
- modal-1.2.5.dev0.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
195
- modal-1.2.5.dev0.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
196
- modal-1.2.5.dev0.dist-info/RECORD,,
193
+ modal-1.2.5.dev2.dist-info/METADATA,sha256=3yOTToFAFT9kAmIjL-NrDmej6f3kE8okAIrwF8r2W3Y,2489
194
+ modal-1.2.5.dev2.dist-info/WHEEL,sha256=1tXe9gY0PYatrMPMDd6jXqjfpz_B-Wqm32CPfRC58XU,91
195
+ modal-1.2.5.dev2.dist-info/entry_points.txt,sha256=An-wYgeEUnm6xzrAP9_NTSTSciYvvEWsMZILtYrvpAI,46
196
+ modal-1.2.5.dev2.dist-info/top_level.txt,sha256=4BWzoKYREKUZ5iyPzZpjqx4G8uB5TWxXPDwibLcVa7k,43
197
+ modal-1.2.5.dev2.dist-info/RECORD,,
modal_version/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # Copyright Modal Labs 2025
2
2
  """Supplies the current version of the modal client library."""
3
3
 
4
- __version__ = "1.2.5.dev0"
4
+ __version__ = "1.2.5.dev2"