modal 1.2.2.dev30__py3-none-any.whl → 1.2.2.dev31__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.
modal/runner.py CHANGED
@@ -8,7 +8,6 @@ import asyncio
8
8
  import dataclasses
9
9
  import os
10
10
  import time
11
- import typing
12
11
  from collections.abc import AsyncGenerator
13
12
  from contextlib import nullcontext
14
13
  from multiprocessing.synchronize import Event
@@ -19,6 +18,7 @@ from synchronicity.async_wrap import asynccontextmanager
19
18
 
20
19
  import modal._runtime.execution_context
21
20
  import modal_proto.api_pb2
21
+ from modal._load_context import LoadContext
22
22
  from modal._utils.grpc_utils import Retry
23
23
  from modal_proto import api_pb2
24
24
 
@@ -125,18 +125,14 @@ async def _init_local_app_from_name(
125
125
 
126
126
 
127
127
  async def _create_all_objects(
128
- client: _Client,
129
128
  running_app: RunningApp,
130
129
  local_app_state: "modal.app._LocalAppState",
131
- environment_name: str,
130
+ load_context: LoadContext,
132
131
  ) -> None:
133
132
  """Create objects that have been defined but not created on the server."""
134
133
  indexed_objects: dict[str, _Object] = {**local_app_state.functions, **local_app_state.classes}
135
- resolver = Resolver(
136
- client,
137
- environment_name=environment_name,
138
- app_id=running_app.app_id,
139
- )
134
+
135
+ resolver = Resolver()
140
136
  with resolver.display():
141
137
  # Get current objects, and reset all objects
142
138
  tag_to_object_id = {**running_app.function_ids, **running_app.class_ids}
@@ -159,7 +155,7 @@ async def _create_all_objects(
159
155
  # Note: preload only currently implemented for Functions, returns None otherwise
160
156
  # this is to ensure that directly referenced functions from the global scope has
161
157
  # ids associated with them when they are serialized into other functions
162
- await resolver.preload(obj, existing_object_id)
158
+ await resolver.preload(obj, load_context, existing_object_id)
163
159
  if obj.is_hydrated:
164
160
  tag_to_object_id[tag] = obj.object_id
165
161
 
@@ -167,7 +163,8 @@ async def _create_all_objects(
167
163
 
168
164
  async def _load(tag, obj):
169
165
  existing_object_id = tag_to_object_id.get(tag)
170
- await resolver.load(obj, existing_object_id)
166
+ # Pass load_context so dependencies can inherit app_id, client, etc.
167
+ await resolver.load(obj, load_context, existing_object_id=existing_object_id)
171
168
  if _Function._is_id_type(obj.object_id):
172
169
  running_app.function_ids[tag] = obj.object_id
173
170
  elif _Cls._is_id_type(obj.object_id):
@@ -266,8 +263,9 @@ async def _run_app(
266
263
  interactive: bool = False,
267
264
  ) -> AsyncGenerator["modal.app._App", None]:
268
265
  """mdmd:hidden"""
269
- if environment_name is None:
270
- environment_name = typing.cast(str, config.get("environment"))
266
+ load_context = await app._root_load_context.reset().in_place_upgrade(
267
+ client=client, environment_name=environment_name
268
+ )
271
269
 
272
270
  if modal._runtime.execution_context._is_currently_importing:
273
271
  raise InvalidError("Can not run an app in global scope within a container")
@@ -288,9 +286,6 @@ async def _run_app(
288
286
  # https://docs.python.org/3/library/__main__.html#import-main
289
287
  app.set_description(__main__.__name__)
290
288
 
291
- if client is None:
292
- client = await _Client.from_env()
293
-
294
289
  app_state = api_pb2.APP_STATE_DETACHED if detach else api_pb2.APP_STATE_EPHEMERAL
295
290
 
296
291
  output_mgr = _get_output_manager()
@@ -301,21 +296,22 @@ async def _run_app(
301
296
  local_app_state = app._local_state
302
297
 
303
298
  running_app: RunningApp = await _init_local_app_new(
304
- client,
299
+ load_context.client,
305
300
  app.description or "",
306
301
  local_app_state.tags,
307
- environment_name=environment_name or "",
302
+ environment_name=load_context.environment_name,
308
303
  app_state=app_state,
309
304
  interactive=interactive,
310
305
  )
306
+ await load_context.in_place_upgrade(app_id=running_app.app_id)
311
307
 
312
308
  logs_timeout = config["logs_timeout"]
313
- async with app._set_local_app(client, running_app), TaskContext(grace=logs_timeout) as tc:
309
+ async with app._set_local_app(load_context.client, running_app), TaskContext(grace=logs_timeout) as tc:
314
310
  # Start heartbeats loop to keep the client alive
315
311
  # we don't log heartbeat exceptions in detached mode
316
312
  # as losing the local connection will not affect the running app
317
313
  def heartbeat():
318
- return _heartbeat(client, running_app.app_id)
314
+ return _heartbeat(load_context.client, running_app.app_id)
319
315
 
320
316
  heartbeat_loop = tc.infinite_loop(heartbeat, sleep=HEARTBEAT_INTERVAL, log_exception=not detach)
321
317
  logs_loop: Optional[asyncio.Task] = None
@@ -336,24 +332,26 @@ async def _run_app(
336
332
  # Start logs loop
337
333
 
338
334
  logs_loop = tc.create_task(
339
- get_app_logs_loop(client, output_mgr, app_id=running_app.app_id, app_logs_url=running_app.app_logs_url)
335
+ get_app_logs_loop(
336
+ load_context.client, output_mgr, app_id=running_app.app_id, app_logs_url=running_app.app_logs_url
337
+ )
340
338
  )
341
339
 
342
340
  try:
343
341
  # Create all members
344
- await _create_all_objects(client, running_app, local_app_state, environment_name)
342
+ await _create_all_objects(running_app, local_app_state, load_context)
345
343
 
346
344
  # Publish the app
347
- await _publish_app(client, running_app, app_state, local_app_state)
345
+ await _publish_app(load_context.client, running_app, app_state, local_app_state)
348
346
  except asyncio.CancelledError as e:
349
347
  # this typically happens on sigint/ctrl-C during setup (the KeyboardInterrupt happens in the main thread)
350
348
  if output_mgr := _get_output_manager():
351
349
  output_mgr.print("Aborting app initialization...\n")
352
350
 
353
- await _status_based_disconnect(client, running_app.app_id, e)
351
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
354
352
  raise
355
353
  except BaseException as e:
356
- await _status_based_disconnect(client, running_app.app_id, e)
354
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
357
355
  raise
358
356
 
359
357
  detached_disconnect_msg = (
@@ -381,7 +379,7 @@ async def _run_app(
381
379
  yield app
382
380
  # successful completion!
383
381
  heartbeat_loop.cancel()
384
- await _status_based_disconnect(client, running_app.app_id, exc_info=None)
382
+ await _status_based_disconnect(load_context.client, running_app.app_id, exc_info=None)
385
383
  except KeyboardInterrupt as e:
386
384
  # this happens only if sigint comes in during the yield block above
387
385
  if detach:
@@ -390,13 +388,13 @@ async def _run_app(
390
388
  output_mgr.print(detached_disconnect_msg)
391
389
  if logs_loop:
392
390
  logs_loop.cancel()
393
- await _status_based_disconnect(client, running_app.app_id, e)
391
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
394
392
  else:
395
393
  if output_mgr := _get_output_manager():
396
394
  output_mgr.print(
397
395
  "Disconnecting from Modal - This will terminate your Modal app in a few seconds.\n"
398
396
  )
399
- await _status_based_disconnect(client, running_app.app_id, e)
397
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
400
398
  if logs_loop:
401
399
  try:
402
400
  await asyncio.wait_for(logs_loop, timeout=logs_timeout)
@@ -421,7 +419,7 @@ async def _run_app(
421
419
  raise
422
420
  except BaseException as e:
423
421
  logger.info("Exception during app run")
424
- await _status_based_disconnect(client, running_app.app_id, e)
422
+ await _status_based_disconnect(load_context.client, running_app.app_id, e)
425
423
  raise
426
424
 
427
425
  # wait for logs gracefully, even though the task context would do the same
@@ -449,21 +447,17 @@ async def _serve_update(
449
447
  ) -> None:
450
448
  """mdmd:hidden"""
451
449
  # Used by child process to reinitialize a served app
452
- client = await _Client.from_env()
450
+ load_context = await app._root_load_context.reset().in_place_upgrade(environment_name=environment_name)
453
451
  try:
454
- running_app: RunningApp = await _init_local_app_existing(client, existing_app_id, environment_name)
452
+ running_app: RunningApp = await _init_local_app_existing(load_context.client, existing_app_id, environment_name)
453
+ await load_context.in_place_upgrade(app_id=running_app.app_id)
455
454
  local_app_state = app._local_state
456
455
  # Create objects
457
- await _create_all_objects(
458
- client,
459
- running_app,
460
- local_app_state,
461
- environment_name,
462
- )
456
+ await _create_all_objects(running_app, local_app_state, load_context)
463
457
 
464
458
  # Publish the updated app
465
459
  await _publish_app(
466
- client,
460
+ load_context.client,
467
461
  running_app,
468
462
  app_state=api_pb2.APP_STATE_UNSPECIFIED,
469
463
  app_local_state=local_app_state,
@@ -498,9 +492,6 @@ async def _deploy_app(
498
492
 
499
493
  Users should prefer the `modal deploy` CLI or the `App.deploy` method.
500
494
  """
501
- if environment_name is None:
502
- environment_name = typing.cast(str, config.get("environment"))
503
-
504
495
  warn_if_passing_namespace(namespace, "modal.runner.deploy_app")
505
496
 
506
497
  name = name or app.name or ""
@@ -532,8 +523,18 @@ async def _deploy_app(
532
523
  # Get git information to track deployment history
533
524
  commit_info_task = asyncio.create_task(get_git_commit_info())
534
525
 
526
+ # We need to do in-place replacement of fields in self._root_load_context in case it has already "spread"
527
+ # to with_options() instances or similar before load
528
+ root_load_context = await app._root_load_context.reset().in_place_upgrade(
529
+ client=client,
530
+ environment_name=environment_name,
531
+ )
535
532
  running_app: RunningApp = await _init_local_app_from_name(
536
- client, name, local_app_state.tags, environment_name=environment_name
533
+ root_load_context.client, name, local_app_state.tags, environment_name=root_load_context.environment_name
534
+ )
535
+
536
+ await root_load_context.in_place_upgrade(
537
+ app_id=running_app.app_id,
537
538
  )
538
539
 
539
540
  async with TaskContext(0) as tc:
@@ -545,12 +546,7 @@ async def _deploy_app(
545
546
 
546
547
  try:
547
548
  # Create all members
548
- await _create_all_objects(
549
- client,
550
- running_app,
551
- local_app_state,
552
- environment_name=environment_name,
553
- )
549
+ await _create_all_objects(running_app, local_app_state, root_load_context)
554
550
 
555
551
  commit_info = None
556
552
  try:
modal/runner.pyi CHANGED
@@ -1,3 +1,4 @@
1
+ import modal._load_context
1
2
  import modal.app
2
3
  import modal.client
3
4
  import modal.running_app
@@ -25,10 +26,9 @@ async def _init_local_app_from_name(
25
26
  client: modal.client._Client, name: str, tags: dict[str, str], environment_name: str = ""
26
27
  ) -> modal.running_app.RunningApp: ...
27
28
  async def _create_all_objects(
28
- client: modal.client._Client,
29
29
  running_app: modal.running_app.RunningApp,
30
30
  local_app_state: modal.app._LocalAppState,
31
- environment_name: str,
31
+ load_context: modal._load_context.LoadContext,
32
32
  ) -> None:
33
33
  """Create objects that have been defined but not created on the server."""
34
34
  ...
modal/sandbox.py CHANGED
@@ -23,6 +23,7 @@ from modal.mount import _Mount
23
23
  from modal.volume import _Volume
24
24
  from modal_proto import api_pb2, task_command_router_pb2 as sr_pb2
25
25
 
26
+ from ._load_context import LoadContext
26
27
  from ._object import _get_environment_name, _Object
27
28
  from ._resolver import Resolver
28
29
  from ._resources import convert_fn_config_to_resources_config
@@ -191,7 +192,9 @@ class _Sandbox(_Object, type_prefix="sb"):
191
192
  deps.append(proxy)
192
193
  return deps
193
194
 
194
- async def _load(self: _Sandbox, resolver: Resolver, _existing_object_id: Optional[str]):
195
+ async def _load(
196
+ self: _Sandbox, resolver: Resolver, load_context: LoadContext, _existing_object_id: Optional[str]
197
+ ):
195
198
  # Relies on dicts being ordered (true as of Python 3.6).
196
199
  volume_mounts = [
197
200
  api_pb2.VolumeMount(
@@ -260,18 +263,18 @@ class _Sandbox(_Object, type_prefix="sb"):
260
263
  experimental_options=experimental_options,
261
264
  )
262
265
 
263
- create_req = api_pb2.SandboxCreateRequest(app_id=resolver.app_id, definition=definition)
266
+ create_req = api_pb2.SandboxCreateRequest(app_id=load_context.app_id, definition=definition)
264
267
  try:
265
- create_resp = await resolver.client.stub.SandboxCreate(create_req)
268
+ create_resp = await load_context.client.stub.SandboxCreate(create_req)
266
269
  except GRPCError as exc:
267
270
  if exc.status == Status.ALREADY_EXISTS:
268
271
  raise AlreadyExistsError(exc.message)
269
272
  raise exc
270
273
 
271
274
  sandbox_id = create_resp.sandbox_id
272
- self._hydrate(sandbox_id, resolver.client, None)
275
+ self._hydrate(sandbox_id, load_context.client, None)
273
276
 
274
- return _Sandbox._from_loader(_load, "Sandbox()", deps=_deps)
277
+ return _Sandbox._from_loader(_load, "Sandbox()", deps=_deps, load_context_overrides=LoadContext.empty())
275
278
 
276
279
  @staticmethod
277
280
  async def create(
@@ -486,6 +489,7 @@ class _Sandbox(_Object, type_prefix="sb"):
486
489
  app_id = app.app_id
487
490
  app_client = app._client
488
491
  elif (container_app := _App._get_container_app()) is not None:
492
+ # implicit app/client provided by running in a modal Function
489
493
  app_id = container_app.app_id
490
494
  app_client = container_app._client
491
495
  else:
@@ -498,10 +502,11 @@ class _Sandbox(_Object, type_prefix="sb"):
498
502
  "```",
499
503
  )
500
504
 
501
- client = client or app_client or await _Client.from_env()
505
+ client = client or app_client
502
506
 
503
- resolver = Resolver(client, app_id=app_id)
504
- await resolver.load(obj)
507
+ resolver = Resolver()
508
+ load_context = LoadContext(client=client, app_id=app_id)
509
+ await resolver.load(obj, load_context)
505
510
  return obj
506
511
 
507
512
  def _hydrate_metadata(self, handle_metadata: Optional[Message]):
@@ -606,12 +611,13 @@ class _Sandbox(_Object, type_prefix="sb"):
606
611
  image_id = resp.image_id
607
612
  metadata = resp.image_metadata
608
613
 
609
- async def _load(self: _Image, resolver: Resolver, existing_object_id: Optional[str]):
614
+ async def _load(self: _Image, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]):
610
615
  # no need to hydrate again since we do it eagerly below
611
616
  pass
612
617
 
613
618
  rep = "Image()"
614
- image = _Image._from_loader(_load, rep, hydrate_lazily=True)
619
+ # TODO: use ._new_hydrated instead
620
+ image = _Image._from_loader(_load, rep, hydrate_lazily=True, load_context_overrides=LoadContext.empty())
615
621
  image._hydrate(image_id, self._client, metadata) # hydrating eagerly since we have all of the data
616
622
 
617
623
  return image
@@ -990,12 +996,15 @@ class _Sandbox(_Object, type_prefix="sb"):
990
996
  if wait_resp.result.status != api_pb2.GenericResult.GENERIC_STATUS_SUCCESS:
991
997
  raise ExecutionError(wait_resp.result.exception)
992
998
 
993
- async def _load(self: _SandboxSnapshot, resolver: Resolver, existing_object_id: Optional[str]):
999
+ async def _load(
1000
+ self: _SandboxSnapshot, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
1001
+ ):
994
1002
  # we eagerly hydrate the sandbox snapshot below
995
1003
  pass
996
1004
 
997
1005
  rep = "SandboxSnapshot()"
998
- obj = _SandboxSnapshot._from_loader(_load, rep, hydrate_lazily=True)
1006
+ # TODO: use ._new_hydrated instead
1007
+ obj = _SandboxSnapshot._from_loader(_load, rep, hydrate_lazily=True, load_context_overrides=LoadContext.empty())
999
1008
  obj._hydrate(snapshot_id, self._client, None)
1000
1009
 
1001
1010
  return obj
modal/secret.py CHANGED
@@ -10,6 +10,7 @@ from synchronicity import classproperty
10
10
 
11
11
  from modal_proto import api_pb2
12
12
 
13
+ from ._load_context import LoadContext
13
14
  from ._object import _get_environment_name, _Object, live_method
14
15
  from ._resolver import Resolver
15
16
  from ._runtime.execution_context import is_local
@@ -259,8 +260,10 @@ class _Secret(_Object, type_prefix="st"):
259
260
  if not all(isinstance(v, str) for v in env_dict_filtered.values()):
260
261
  raise InvalidError(ENV_DICT_WRONG_TYPE_ERR)
261
262
 
262
- async def _load(self: _Secret, resolver: Resolver, existing_object_id: Optional[str]):
263
- if resolver.app_id is not None:
263
+ async def _load(
264
+ self: _Secret, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
265
+ ):
266
+ if load_context.app_id is not None:
264
267
  object_creation_type = api_pb2.OBJECT_CREATION_TYPE_ANONYMOUS_OWNED_BY_APP
265
268
  else:
266
269
  object_creation_type = api_pb2.OBJECT_CREATION_TYPE_EPHEMERAL
@@ -268,21 +271,22 @@ class _Secret(_Object, type_prefix="st"):
268
271
  req = api_pb2.SecretGetOrCreateRequest(
269
272
  object_creation_type=object_creation_type,
270
273
  env_dict=env_dict_filtered,
271
- app_id=resolver.app_id,
272
- environment_name=resolver.environment_name,
274
+ app_id=load_context.app_id,
275
+ environment_name=load_context.environment_name,
273
276
  )
274
277
  try:
275
- resp = await resolver.client.stub.SecretGetOrCreate(req)
278
+ resp = await load_context.client.stub.SecretGetOrCreate(req)
276
279
  except GRPCError as exc:
277
280
  if exc.status == Status.INVALID_ARGUMENT:
278
281
  raise InvalidError(exc.message)
279
282
  if exc.status == Status.FAILED_PRECONDITION:
280
283
  raise InvalidError(exc.message)
281
284
  raise
282
- self._hydrate(resp.secret_id, resolver.client, resp.metadata)
285
+ self._hydrate(resp.secret_id, load_context.client, resp.metadata)
283
286
 
284
287
  rep = f"Secret.from_dict([{', '.join(env_dict.keys())}])"
285
- return _Secret._from_loader(_load, rep, hydrate_lazily=True)
288
+ # TODO: scoping - these should probably not be lazily hydrated without having an app and/or sandbox association
289
+ return _Secret._from_loader(_load, rep, hydrate_lazily=True, load_context_overrides=LoadContext.empty())
286
290
 
287
291
  @staticmethod
288
292
  def from_local_environ(
@@ -330,7 +334,9 @@ class _Secret(_Object, type_prefix="st"):
330
334
  ```
331
335
  """
332
336
 
333
- async def _load(self: _Secret, resolver: Resolver, existing_object_id: Optional[str]):
337
+ async def _load(
338
+ self: _Secret, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
339
+ ):
334
340
  try:
335
341
  from dotenv import dotenv_values, find_dotenv
336
342
  from dotenv.main import _walk_to_root
@@ -359,13 +365,15 @@ class _Secret(_Object, type_prefix="st"):
359
365
  req = api_pb2.SecretGetOrCreateRequest(
360
366
  object_creation_type=api_pb2.OBJECT_CREATION_TYPE_ANONYMOUS_OWNED_BY_APP,
361
367
  env_dict=env_dict,
362
- app_id=resolver.app_id,
368
+ app_id=load_context.app_id, # TODO: what if app_id isn't set here (e.g. .hydrate())
363
369
  )
364
- resp = await resolver.client.stub.SecretGetOrCreate(req)
370
+ resp = await load_context.client.stub.SecretGetOrCreate(req)
365
371
 
366
- self._hydrate(resp.secret_id, resolver.client, resp.metadata)
372
+ self._hydrate(resp.secret_id, load_context.client, resp.metadata)
367
373
 
368
- return _Secret._from_loader(_load, "Secret.from_dotenv()", hydrate_lazily=True)
374
+ return _Secret._from_loader(
375
+ _load, "Secret.from_dotenv()", hydrate_lazily=True, load_context_overrides=LoadContext.empty()
376
+ )
369
377
 
370
378
  @staticmethod
371
379
  def from_name(
@@ -376,6 +384,7 @@ class _Secret(_Object, type_prefix="st"):
376
384
  required_keys: list[
377
385
  str
378
386
  ] = [], # Optionally, a list of required environment variables (will be asserted server-side)
387
+ client: Optional[_Client] = None,
379
388
  ) -> "_Secret":
380
389
  """Reference a Secret by its name.
381
390
 
@@ -393,23 +402,31 @@ class _Secret(_Object, type_prefix="st"):
393
402
  """
394
403
  warn_if_passing_namespace(namespace, "modal.Secret.from_name")
395
404
 
396
- async def _load(self: _Secret, resolver: Resolver, existing_object_id: Optional[str]):
405
+ async def _load(
406
+ self: _Secret, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
407
+ ):
397
408
  req = api_pb2.SecretGetOrCreateRequest(
398
409
  deployment_name=name,
399
- environment_name=_get_environment_name(environment_name, resolver),
410
+ environment_name=load_context.environment_name,
400
411
  required_keys=required_keys,
401
412
  )
402
413
  try:
403
- response = await resolver.client.stub.SecretGetOrCreate(req)
414
+ response = await load_context.client.stub.SecretGetOrCreate(req)
404
415
  except GRPCError as exc:
405
416
  if exc.status == Status.NOT_FOUND:
406
417
  raise NotFoundError(exc.message)
407
418
  else:
408
419
  raise
409
- self._hydrate(response.secret_id, resolver.client, response.metadata)
420
+ self._hydrate(response.secret_id, load_context.client, response.metadata)
410
421
 
411
422
  rep = _Secret._repr(name, environment_name)
412
- return _Secret._from_loader(_load, rep, hydrate_lazily=True, name=name)
423
+ return _Secret._from_loader(
424
+ _load,
425
+ rep,
426
+ hydrate_lazily=True,
427
+ name=name,
428
+ load_context_overrides=LoadContext(environment_name=environment_name, client=client),
429
+ )
413
430
 
414
431
  @staticmethod
415
432
  async def create_deployed(
modal/secret.pyi CHANGED
@@ -423,7 +423,12 @@ class _Secret(modal._object._Object):
423
423
 
424
424
  @staticmethod
425
425
  def from_name(
426
- name: str, *, namespace=None, environment_name: typing.Optional[str] = None, required_keys: list[str] = []
426
+ name: str,
427
+ *,
428
+ namespace=None,
429
+ environment_name: typing.Optional[str] = None,
430
+ required_keys: list[str] = [],
431
+ client: typing.Optional[modal.client._Client] = None,
427
432
  ) -> _Secret:
428
433
  """Reference a Secret by its name.
429
434
 
@@ -543,7 +548,12 @@ class Secret(modal.object.Object):
543
548
 
544
549
  @staticmethod
545
550
  def from_name(
546
- name: str, *, namespace=None, environment_name: typing.Optional[str] = None, required_keys: list[str] = []
551
+ name: str,
552
+ *,
553
+ namespace=None,
554
+ environment_name: typing.Optional[str] = None,
555
+ required_keys: list[str] = [],
556
+ client: typing.Optional[modal.client.Client] = None,
547
557
  ) -> Secret:
548
558
  """Reference a Secret by its name.
549
559
 
modal/serving.py CHANGED
@@ -4,13 +4,13 @@ import platform
4
4
  from collections.abc import AsyncGenerator
5
5
  from multiprocessing.context import SpawnProcess
6
6
  from multiprocessing.synchronize import Event
7
- from typing import TYPE_CHECKING, Optional, TypeVar
7
+ from typing import TYPE_CHECKING, Optional
8
8
 
9
9
  from synchronicity.async_wrap import asynccontextmanager
10
10
 
11
11
  from modal._output import OutputManager
12
12
 
13
- from ._utils.async_utils import TaskContext, asyncify, synchronize_api, synchronizer
13
+ from ._utils.async_utils import TaskContext, asyncify, synchronize_api
14
14
  from ._utils.logger import logger
15
15
  from ._watcher import watch
16
16
  from .cli.import_refs import ImportRef, import_app_from_ref
@@ -20,20 +20,16 @@ from .output import _get_output_manager, enable_output
20
20
  from .runner import _run_app, serve_update
21
21
 
22
22
  if TYPE_CHECKING:
23
- from .app import _App
24
- else:
25
- _App = TypeVar("_App")
23
+ import modal.app
26
24
 
27
25
 
28
26
  def _run_serve(
29
27
  import_ref: ImportRef, existing_app_id: str, is_ready: Event, environment_name: str, show_progress: bool
30
28
  ):
31
- # subprocess entrypoint
32
- _app = import_app_from_ref(import_ref, base_cmd="modal serve")
33
- blocking_app = synchronizer._translate_out(_app)
29
+ app = import_app_from_ref(import_ref, base_cmd="modal serve")
34
30
 
35
31
  with enable_output(show_progress=show_progress):
36
- serve_update(blocking_app, existing_app_id, is_ready, environment_name)
32
+ serve_update(app, existing_app_id, is_ready, environment_name)
37
33
 
38
34
 
39
35
  async def _restart_serve(
@@ -97,12 +93,12 @@ async def _run_watch_loop(
97
93
 
98
94
  @asynccontextmanager
99
95
  async def _serve_app(
100
- app: "_App",
96
+ app: "modal.app._App",
101
97
  import_ref: ImportRef,
102
98
  *,
103
99
  _watcher: Optional[AsyncGenerator[set[str], None]] = None, # for testing
104
100
  environment_name: Optional[str] = None,
105
- ) -> AsyncGenerator["_App", None]:
101
+ ) -> AsyncGenerator["modal.app._App", None]:
106
102
  if environment_name is None:
107
103
  environment_name = config.get("environment")
108
104
 
modal/serving.pyi CHANGED
@@ -1,4 +1,5 @@
1
1
  import collections.abc
2
+ import modal.app
2
3
  import modal.cli.import_refs
3
4
  import multiprocessing.context
4
5
  import multiprocessing.synchronize
@@ -6,8 +7,6 @@ import synchronicity.combined_types
6
7
  import typing
7
8
  import typing_extensions
8
9
 
9
- _App = typing.TypeVar("_App")
10
-
11
10
  def _run_serve(
12
11
  import_ref: modal.cli.import_refs.ImportRef,
13
12
  existing_app_id: str,
@@ -27,31 +26,31 @@ async def _run_watch_loop(
27
26
  environment_name: str,
28
27
  ): ...
29
28
  def _serve_app(
30
- app: _App,
29
+ app: modal.app._App,
31
30
  import_ref: modal.cli.import_refs.ImportRef,
32
31
  *,
33
32
  _watcher: typing.Optional[collections.abc.AsyncGenerator[set[str], None]] = None,
34
33
  environment_name: typing.Optional[str] = None,
35
- ) -> typing.AsyncContextManager[_App]: ...
34
+ ) -> typing.AsyncContextManager[modal.app._App]: ...
36
35
 
37
36
  class __serve_app_spec(typing_extensions.Protocol):
38
37
  def __call__(
39
38
  self,
40
39
  /,
41
- app: _App,
40
+ app: modal.app.App,
42
41
  import_ref: modal.cli.import_refs.ImportRef,
43
42
  *,
44
43
  _watcher: typing.Optional[typing.Generator[set[str], None, None]] = None,
45
44
  environment_name: typing.Optional[str] = None,
46
- ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[_App]: ...
45
+ ) -> synchronicity.combined_types.AsyncAndBlockingContextManager[modal.app.App]: ...
47
46
  def aio(
48
47
  self,
49
48
  /,
50
- app: _App,
49
+ app: modal.app.App,
51
50
  import_ref: modal.cli.import_refs.ImportRef,
52
51
  *,
53
52
  _watcher: typing.Optional[collections.abc.AsyncGenerator[set[str], None]] = None,
54
53
  environment_name: typing.Optional[str] = None,
55
- ) -> typing.AsyncContextManager[_App]: ...
54
+ ) -> typing.AsyncContextManager[modal.app.App]: ...
56
55
 
57
56
  serve_app: __serve_app_spec
modal/snapshot.py CHANGED
@@ -3,6 +3,7 @@ from typing import Optional
3
3
 
4
4
  from modal_proto import api_pb2
5
5
 
6
+ from ._load_context import LoadContext
6
7
  from ._object import _Object
7
8
  from ._resolver import Resolver
8
9
  from ._utils.async_utils import synchronize_api
@@ -23,14 +24,19 @@ class _SandboxSnapshot(_Object, type_prefix="sn"):
23
24
  """
24
25
  Construct a `SandboxSnapshot` object from a sandbox snapshot ID.
25
26
  """
26
- if client is None:
27
- client = await _Client.from_env()
27
+ # TODO: remove this - from_id constructor should not do io:
28
+ client = client or await _Client.from_env()
28
29
 
29
- async def _load(self: _SandboxSnapshot, resolver: Resolver, existing_object_id: Optional[str]):
30
- await client.stub.SandboxSnapshotGet(api_pb2.SandboxSnapshotGetRequest(snapshot_id=sandbox_snapshot_id))
30
+ async def _load(
31
+ self: _SandboxSnapshot, resolver: Resolver, load_context: LoadContext, existing_object_id: Optional[str]
32
+ ):
33
+ await load_context.client.stub.SandboxSnapshotGet(
34
+ api_pb2.SandboxSnapshotGetRequest(snapshot_id=sandbox_snapshot_id)
35
+ )
31
36
 
32
37
  rep = "SandboxSnapshot()"
33
- obj = _SandboxSnapshot._from_loader(_load, rep)
38
+ obj = _SandboxSnapshot._from_loader(_load, rep, load_context_overrides=LoadContext(client=client))
39
+ # TODO: should this be a _Object._new_hydrated instead?
34
40
  obj._hydrate(sandbox_snapshot_id, client, None)
35
41
 
36
42
  return obj