modal 0.68.24__py3-none-any.whl → 0.68.42__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.
Files changed (51) hide show
  1. modal/_traceback.py +6 -2
  2. modal/_utils/deprecation.py +89 -0
  3. modal/app.py +15 -34
  4. modal/app.pyi +6 -7
  5. modal/cli/app.py +1 -1
  6. modal/cli/dict.py +6 -2
  7. modal/cli/network_file_system.py +1 -1
  8. modal/cli/run.py +1 -0
  9. modal/cli/volume.py +1 -1
  10. modal/client.pyi +2 -2
  11. modal/cls.py +15 -9
  12. modal/cls.pyi +5 -5
  13. modal/config.py +2 -1
  14. modal/container_process.py +2 -1
  15. modal/dict.py +12 -17
  16. modal/dict.pyi +8 -12
  17. modal/environments.py +10 -7
  18. modal/environments.pyi +6 -6
  19. modal/exception.py +0 -54
  20. modal/file_io.py +54 -7
  21. modal/file_io.pyi +18 -8
  22. modal/file_pattern_matcher.py +48 -15
  23. modal/functions.py +11 -13
  24. modal/functions.pyi +18 -12
  25. modal/image.py +21 -20
  26. modal/image.pyi +16 -28
  27. modal/mount.py +7 -4
  28. modal/mount.pyi +4 -4
  29. modal/network_file_system.py +13 -19
  30. modal/network_file_system.pyi +8 -12
  31. modal/partial_function.py +2 -30
  32. modal/queue.py +12 -17
  33. modal/queue.pyi +8 -12
  34. modal/runner.py +2 -7
  35. modal/sandbox.py +25 -13
  36. modal/sandbox.pyi +21 -0
  37. modal/secret.py +7 -4
  38. modal/secret.pyi +5 -5
  39. modal/serving.py +1 -1
  40. modal/volume.py +12 -17
  41. modal/volume.pyi +8 -12
  42. {modal-0.68.24.dist-info → modal-0.68.42.dist-info}/METADATA +2 -2
  43. {modal-0.68.24.dist-info → modal-0.68.42.dist-info}/RECORD +51 -50
  44. modal_proto/api.proto +1 -1
  45. modal_proto/api_pb2.py +750 -750
  46. modal_proto/api_pb2.pyi +4 -4
  47. modal_version/_version_generated.py +1 -1
  48. {modal-0.68.24.dist-info → modal-0.68.42.dist-info}/LICENSE +0 -0
  49. {modal-0.68.24.dist-info → modal-0.68.42.dist-info}/WHEEL +0 -0
  50. {modal-0.68.24.dist-info → modal-0.68.42.dist-info}/entry_points.txt +0 -0
  51. {modal-0.68.24.dist-info → modal-0.68.42.dist-info}/top_level.txt +0 -0
modal/environments.py CHANGED
@@ -10,6 +10,7 @@ from modal_proto import api_pb2
10
10
 
11
11
  from ._resolver import Resolver
12
12
  from ._utils.async_utils import synchronize_api, synchronizer
13
+ from ._utils.deprecation import renamed_parameter
13
14
  from ._utils.grpc_utils import retry_transient_errors
14
15
  from ._utils.name_utils import check_object_name
15
16
  from .client import _Client
@@ -52,21 +53,22 @@ class _Environment(_Object, type_prefix="en"):
52
53
  )
53
54
 
54
55
  @staticmethod
56
+ @renamed_parameter((2024, 12, 18), "label", "name")
55
57
  async def from_name(
56
- label: str,
58
+ name: str,
57
59
  create_if_missing: bool = False,
58
60
  ):
59
- if label:
60
- # Allow null labels for the case where we want to look up the "default" environment,
61
+ if name:
62
+ # Allow null names for the case where we want to look up the "default" environment,
61
63
  # which is defined by the server. It feels messy to have "from_name" without a name, though?
62
64
  # We're adding this mostly for internal use right now. We could consider an environment-only
63
65
  # alternate constructor, like `Environment.get_default`, rather than exposing "unnamed"
64
66
  # environments as part of public API when we make this class more useful.
65
- check_object_name(label, "Environment")
67
+ check_object_name(name, "Environment")
66
68
 
67
69
  async def _load(self: _Environment, resolver: Resolver, existing_object_id: Optional[str]):
68
70
  request = api_pb2.EnvironmentGetOrCreateRequest(
69
- deployment_name=label,
71
+ deployment_name=name,
70
72
  object_creation_type=(
71
73
  api_pb2.OBJECT_CREATION_TYPE_CREATE_IF_MISSING
72
74
  if create_if_missing
@@ -81,12 +83,13 @@ class _Environment(_Object, type_prefix="en"):
81
83
  return _Environment._from_loader(_load, "Environment()", is_another_app=True, hydrate_lazily=True)
82
84
 
83
85
  @staticmethod
86
+ @renamed_parameter((2024, 12, 18), "label", "name")
84
87
  async def lookup(
85
- label: str,
88
+ name: str,
86
89
  client: Optional[_Client] = None,
87
90
  create_if_missing: bool = False,
88
91
  ):
89
- obj = await _Environment.from_name(label, create_if_missing=create_if_missing)
92
+ obj = await _Environment.from_name(name, create_if_missing=create_if_missing)
90
93
  if client is None:
91
94
  client = await _Client.from_env()
92
95
  resolver = Resolver(client=client)
modal/environments.pyi CHANGED
@@ -22,10 +22,10 @@ class _Environment(modal.object._Object):
22
22
  def __init__(self): ...
23
23
  def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
24
24
  @staticmethod
25
- async def from_name(label: str, create_if_missing: bool = False): ...
25
+ async def from_name(name: str, create_if_missing: bool = False): ...
26
26
  @staticmethod
27
27
  async def lookup(
28
- label: str, client: typing.Optional[modal.client._Client] = None, create_if_missing: bool = False
28
+ name: str, client: typing.Optional[modal.client._Client] = None, create_if_missing: bool = False
29
29
  ): ...
30
30
 
31
31
  class Environment(modal.object.Object):
@@ -35,17 +35,17 @@ class Environment(modal.object.Object):
35
35
  def _hydrate_metadata(self, metadata: google.protobuf.message.Message): ...
36
36
 
37
37
  class __from_name_spec(typing_extensions.Protocol):
38
- def __call__(self, label: str, create_if_missing: bool = False): ...
39
- async def aio(self, label: str, create_if_missing: bool = False): ...
38
+ def __call__(self, name: str, create_if_missing: bool = False): ...
39
+ async def aio(self, name: str, create_if_missing: bool = False): ...
40
40
 
41
41
  from_name: __from_name_spec
42
42
 
43
43
  class __lookup_spec(typing_extensions.Protocol):
44
44
  def __call__(
45
- self, label: str, client: typing.Optional[modal.client.Client] = None, create_if_missing: bool = False
45
+ self, name: str, client: typing.Optional[modal.client.Client] = None, create_if_missing: bool = False
46
46
  ): ...
47
47
  async def aio(
48
- self, label: str, client: typing.Optional[modal.client.Client] = None, create_if_missing: bool = False
48
+ self, name: str, client: typing.Optional[modal.client.Client] = None, create_if_missing: bool = False
49
49
  ): ...
50
50
 
51
51
  lookup: __lookup_spec
modal/exception.py CHANGED
@@ -1,12 +1,6 @@
1
1
  # Copyright Modal Labs 2022
2
2
  import random
3
3
  import signal
4
- import sys
5
- import warnings
6
- from datetime import date
7
- from typing import Iterable
8
-
9
- from modal_proto import api_pb2
10
4
 
11
5
 
12
6
  class Error(Exception):
@@ -129,45 +123,6 @@ class _CliUserExecutionError(Exception):
129
123
  self.user_source = user_source
130
124
 
131
125
 
132
- # TODO(erikbern): we have something similready in function_utils.py
133
- _INTERNAL_MODULES = ["modal", "synchronicity"]
134
-
135
-
136
- def _is_internal_frame(frame):
137
- module = frame.f_globals["__name__"].split(".")[0]
138
- return module in _INTERNAL_MODULES
139
-
140
-
141
- def deprecation_error(deprecated_on: tuple[int, int, int], msg: str):
142
- raise DeprecationError(f"Deprecated on {date(*deprecated_on)}: {msg}")
143
-
144
-
145
- def deprecation_warning(
146
- deprecated_on: tuple[int, int, int], msg: str, *, pending: bool = False, show_source: bool = True
147
- ) -> None:
148
- """Utility for getting the proper stack entry.
149
-
150
- See the implementation of the built-in [warnings.warn](https://docs.python.org/3/library/warnings.html#available-functions).
151
- """
152
- filename, lineno = "<unknown>", 0
153
- if show_source:
154
- # Find the last non-Modal line that triggered the warning
155
- try:
156
- frame = sys._getframe()
157
- while frame is not None and _is_internal_frame(frame):
158
- frame = frame.f_back
159
- filename = frame.f_code.co_filename
160
- lineno = frame.f_lineno
161
- except ValueError:
162
- # Use the defaults from above
163
- pass
164
-
165
- warning_cls: type = PendingDeprecationError if pending else DeprecationError
166
-
167
- # This is a lower-level function that warnings.warn uses
168
- warnings.warn_explicit(f"{date(*deprecated_on)}: {msg}", warning_cls, filename, lineno)
169
-
170
-
171
126
  def _simulate_preemption_interrupt(signum, frame):
172
127
  signal.alarm(30) # simulate a SIGKILL after 30s
173
128
  raise KeyboardInterrupt("Simulated preemption interrupt from modal-client!")
@@ -224,12 +179,3 @@ class ClientClosed(Error):
224
179
 
225
180
  class FilesystemExecutionError(Error):
226
181
  """Raised when an unknown error is thrown during a container filesystem operation."""
227
-
228
-
229
- def print_server_warnings(server_warnings: Iterable[api_pb2.Warning]):
230
- # TODO(erikbern): move this to modal._utils.deprecation
231
- for warning in server_warnings:
232
- if warning.type == api_pb2.Warning.WARNING_TYPE_CLIENT_DEPRECATION:
233
- warnings.warn_explicit(warning.message, DeprecationError, "<unknown>", 0)
234
- else:
235
- warnings.warn_explicit(warning.message, UserWarning, "<unknown>", 0)
modal/file_io.py CHANGED
@@ -6,6 +6,8 @@ from typing import TYPE_CHECKING, AsyncIterator, Generic, Optional, Sequence, Ty
6
6
  if TYPE_CHECKING:
7
7
  import _typeshed
8
8
 
9
+ import json
10
+
9
11
  from grpclib.exceptions import GRPCError, StreamTerminatedError
10
12
 
11
13
  from modal._utils.grpc_utils import retry_transient_errors
@@ -267,12 +269,12 @@ class _FileIO(Generic[T]):
267
269
  output = await self._make_read_request(None)
268
270
  if self._binary:
269
271
  lines_bytes = output.split(b"\n")
270
- output = [line + b"\n" for line in lines_bytes[:-1]] + ([lines_bytes[-1]] if lines_bytes[-1] else [])
271
- return cast(Sequence[T], output)
272
+ return_bytes = [line + b"\n" for line in lines_bytes[:-1]] + ([lines_bytes[-1]] if lines_bytes[-1] else [])
273
+ return cast(Sequence[T], return_bytes)
272
274
  else:
273
275
  lines = output.decode("utf-8").split("\n")
274
- output = [line + "\n" for line in lines[:-1]] + ([lines[-1]] if lines[-1] else [])
275
- return cast(Sequence[T], output)
276
+ return_strs = [line + "\n" for line in lines[:-1]] + ([lines[-1]] if lines[-1] else [])
277
+ return cast(Sequence[T], return_strs)
276
278
 
277
279
  async def write(self, data: Union[bytes, str]) -> None:
278
280
  """Write data to the current position.
@@ -337,6 +339,52 @@ class _FileIO(Generic[T]):
337
339
  )
338
340
  await self._wait(resp.exec_id)
339
341
 
342
+ @classmethod
343
+ async def ls(cls, path: str, client: _Client, task_id: str) -> list[str]:
344
+ """List the contents of the provided directory."""
345
+ self = cls.__new__(cls)
346
+ self._client = client
347
+ self._task_id = task_id
348
+ resp = await self._make_request(
349
+ api_pb2.ContainerFilesystemExecRequest(
350
+ file_ls_request=api_pb2.ContainerFileLsRequest(path=path),
351
+ task_id=task_id,
352
+ )
353
+ )
354
+ output = await self._wait(resp.exec_id)
355
+ try:
356
+ return json.loads(output.decode("utf-8"))["paths"]
357
+ except json.JSONDecodeError:
358
+ raise FilesystemExecutionError("failed to parse list output")
359
+
360
+ @classmethod
361
+ async def mkdir(cls, path: str, client: _Client, task_id: str, parents: bool = False) -> None:
362
+ """Create a new directory."""
363
+ self = cls.__new__(cls)
364
+ self._client = client
365
+ self._task_id = task_id
366
+ resp = await self._make_request(
367
+ api_pb2.ContainerFilesystemExecRequest(
368
+ file_mkdir_request=api_pb2.ContainerFileMkdirRequest(path=path, make_parents=parents),
369
+ task_id=self._task_id,
370
+ )
371
+ )
372
+ await self._wait(resp.exec_id)
373
+
374
+ @classmethod
375
+ async def rm(cls, path: str, client: _Client, task_id: str, recursive: bool = False) -> None:
376
+ """Remove a file or directory in the Sandbox."""
377
+ self = cls.__new__(cls)
378
+ self._client = client
379
+ self._task_id = task_id
380
+ resp = await self._make_request(
381
+ api_pb2.ContainerFilesystemExecRequest(
382
+ file_rm_request=api_pb2.ContainerFileRmRequest(path=path, recursive=recursive),
383
+ task_id=self._task_id,
384
+ )
385
+ )
386
+ await self._wait(resp.exec_id)
387
+
340
388
  async def _close(self) -> None:
341
389
  # Buffer is flushed by the runner on close
342
390
  resp = await self._make_request(
@@ -367,11 +415,10 @@ class _FileIO(Generic[T]):
367
415
  if self._closed:
368
416
  raise ValueError("I/O operation on closed file")
369
417
 
370
- def __enter__(self) -> "_FileIO":
371
- self._check_closed()
418
+ async def __aenter__(self) -> "_FileIO":
372
419
  return self
373
420
 
374
- async def __exit__(self, exc_type, exc_value, traceback) -> None:
421
+ async def __aexit__(self, exc_type, exc_value, traceback) -> None:
375
422
  await self._close()
376
423
 
377
424
 
modal/file_io.pyi CHANGED
@@ -43,13 +43,19 @@ class _FileIO(typing.Generic[T]):
43
43
  async def flush(self) -> None: ...
44
44
  def _get_whence(self, whence: int): ...
45
45
  async def seek(self, offset: int, whence: int = 0) -> None: ...
46
+ @classmethod
47
+ async def ls(cls, path: str, client: modal.client._Client, task_id: str) -> list[str]: ...
48
+ @classmethod
49
+ async def mkdir(cls, path: str, client: modal.client._Client, task_id: str, parents: bool = False) -> None: ...
50
+ @classmethod
51
+ async def rm(cls, path: str, client: modal.client._Client, task_id: str, recursive: bool = False) -> None: ...
46
52
  async def _close(self) -> None: ...
47
53
  async def close(self) -> None: ...
48
54
  def _check_writable(self) -> None: ...
49
55
  def _check_readable(self) -> None: ...
50
56
  def _check_closed(self) -> None: ...
51
- def __enter__(self) -> _FileIO: ...
52
- async def __exit__(self, exc_type, exc_value, traceback) -> None: ...
57
+ async def __aenter__(self) -> _FileIO: ...
58
+ async def __aexit__(self, exc_type, exc_value, traceback) -> None: ...
53
59
 
54
60
  class __delete_bytes_spec(typing_extensions.Protocol):
55
61
  def __call__(self, file: FileIO, start: typing.Optional[int] = None, end: typing.Optional[int] = None) -> None: ...
@@ -161,6 +167,13 @@ class FileIO(typing.Generic[T]):
161
167
 
162
168
  seek: __seek_spec
163
169
 
170
+ @classmethod
171
+ def ls(cls, path: str, client: modal.client.Client, task_id: str) -> list[str]: ...
172
+ @classmethod
173
+ def mkdir(cls, path: str, client: modal.client.Client, task_id: str, parents: bool = False) -> None: ...
174
+ @classmethod
175
+ def rm(cls, path: str, client: modal.client.Client, task_id: str, recursive: bool = False) -> None: ...
176
+
164
177
  class ___close_spec(typing_extensions.Protocol):
165
178
  def __call__(self) -> None: ...
166
179
  async def aio(self) -> None: ...
@@ -177,9 +190,6 @@ class FileIO(typing.Generic[T]):
177
190
  def _check_readable(self) -> None: ...
178
191
  def _check_closed(self) -> None: ...
179
192
  def __enter__(self) -> FileIO: ...
180
-
181
- class ____exit___spec(typing_extensions.Protocol):
182
- def __call__(self, exc_type, exc_value, traceback) -> None: ...
183
- async def aio(self, exc_type, exc_value, traceback) -> None: ...
184
-
185
- __exit__: ____exit___spec
193
+ async def __aenter__(self) -> FileIO: ...
194
+ def __exit__(self, exc_type, exc_value, traceback) -> None: ...
195
+ async def __aexit__(self, exc_type, exc_value, traceback) -> None: ...
@@ -10,13 +10,56 @@ then asking it whether file paths match any of its patterns.
10
10
  """
11
11
 
12
12
  import os
13
+ from abc import abstractmethod
13
14
  from pathlib import Path
14
- from typing import Callable
15
+ from typing import Callable, Optional
15
16
 
16
17
  from ._utils.pattern_utils import Pattern
17
18
 
18
19
 
19
- class FilePatternMatcher:
20
+ class _AbstractPatternMatcher:
21
+ _custom_repr: Optional[str] = None
22
+
23
+ def __invert__(self) -> "_AbstractPatternMatcher":
24
+ """Invert the filter. Returns a function that returns True if the path does not match any of the patterns.
25
+
26
+ Usage:
27
+ ```python
28
+ from pathlib import Path
29
+ from modal import FilePatternMatcher
30
+
31
+ inverted_matcher = ~FilePatternMatcher("**/*.py")
32
+
33
+ assert not inverted_matcher(Path("foo.py"))
34
+ ```
35
+ """
36
+ return _CustomPatternMatcher(lambda path: not self(path))
37
+
38
+ def with_repr(self, custom_repr) -> "_AbstractPatternMatcher":
39
+ # use to give an instance of a matcher a custom name - useful for visualizing default values in signatures
40
+ self._custom_repr = custom_repr
41
+ return self
42
+
43
+ def __repr__(self) -> str:
44
+ if self._custom_repr:
45
+ return self._custom_repr
46
+
47
+ return super().__repr__()
48
+
49
+ @abstractmethod
50
+ def __call__(self, path: Path) -> bool:
51
+ ...
52
+
53
+
54
+ class _CustomPatternMatcher(_AbstractPatternMatcher):
55
+ def __init__(self, predicate: Callable[[Path], bool]):
56
+ self._predicate = predicate
57
+
58
+ def __call__(self, path: Path) -> bool:
59
+ return self._predicate(path)
60
+
61
+
62
+ class FilePatternMatcher(_AbstractPatternMatcher):
20
63
  """Allows matching file paths against a list of patterns."""
21
64
 
22
65
  def __init__(self, *pattern: str) -> None:
@@ -105,17 +148,7 @@ class FilePatternMatcher:
105
148
  """
106
149
  return self._matches(str(file_path))
107
150
 
108
- def __invert__(self) -> Callable[[Path], bool]:
109
- """Invert the filter. Returns a function that returns True if the path does not match any of the patterns.
110
-
111
- Usage:
112
- ```python
113
- from pathlib import Path
114
- from modal import FilePatternMatcher
115
-
116
- inverted_matcher = ~FilePatternMatcher("**/*.py")
117
151
 
118
- assert not inverted_matcher(Path("foo.py"))
119
- ```
120
- """
121
- return lambda path: not self(path)
152
+ # with_repr allows us to use this matcher as a default value in a function signature
153
+ # and get a nice repr in the docs and auto-generated type stubs:
154
+ NON_PYTHON_FILES = (~FilePatternMatcher("**/*.py")).with_repr(f"{__name__}.NON_PYTHON_FILES")
modal/functions.py CHANGED
@@ -22,7 +22,6 @@ from grpclib import GRPCError, Status
22
22
  from synchronicity.combined_types import MethodWithAio
23
23
  from synchronicity.exceptions import UserCodeException
24
24
 
25
- from modal._utils.async_utils import aclosing
26
25
  from modal_proto import api_pb2
27
26
  from modal_proto.modal_api_grpc import ModalClientModal
28
27
 
@@ -35,12 +34,14 @@ from ._serialization import serialize, serialize_proto_params
35
34
  from ._traceback import print_server_warnings
36
35
  from ._utils.async_utils import (
37
36
  TaskContext,
37
+ aclosing,
38
38
  async_merge,
39
39
  callable_to_agen,
40
40
  synchronize_api,
41
41
  synchronizer,
42
42
  warn_if_generator_is_not_consumed,
43
43
  )
44
+ from ._utils.deprecation import deprecation_warning, renamed_parameter
44
45
  from ._utils.function_utils import (
45
46
  ATTEMPT_TIMEOUT_GRACE_PERIOD,
46
47
  OUTPUTS_TIMEOUT,
@@ -58,14 +59,7 @@ from .call_graph import InputInfo, _reconstruct_call_graph
58
59
  from .client import _Client
59
60
  from .cloud_bucket_mount import _CloudBucketMount, cloud_bucket_mounts_to_proto
60
61
  from .config import config
61
- from .exception import (
62
- ExecutionError,
63
- FunctionTimeoutError,
64
- InvalidError,
65
- NotFoundError,
66
- OutputExpiredError,
67
- deprecation_warning,
68
- )
62
+ from .exception import ExecutionError, FunctionTimeoutError, InvalidError, NotFoundError, OutputExpiredError
69
63
  from .gpu import GPU_T, parse_gpu_config
70
64
  from .image import _Image
71
65
  from .mount import _get_client_mount, _Mount, get_auto_mounts
@@ -352,6 +346,7 @@ class _FunctionSpec:
352
346
  memory: Optional[Union[int, tuple[int, int]]]
353
347
  ephemeral_disk: Optional[int]
354
348
  scheduler_placement: Optional[SchedulerPlacement]
349
+ proxy: Optional[_Proxy]
355
350
 
356
351
 
357
352
  P = typing_extensions.ParamSpec("P")
@@ -536,6 +531,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
536
531
  memory=memory,
537
532
  ephemeral_disk=ephemeral_disk,
538
533
  scheduler_placement=scheduler_placement,
534
+ proxy=proxy,
539
535
  )
540
536
 
541
537
  if info.user_cls and not is_auto_snapshot:
@@ -1028,10 +1024,11 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1028
1024
  await retry_transient_errors(self._client.stub.FunctionUpdateSchedulingParams, request)
1029
1025
 
1030
1026
  @classmethod
1027
+ @renamed_parameter((2024, 12, 18), "tag", "name")
1031
1028
  def from_name(
1032
1029
  cls: type["_Function"],
1033
1030
  app_name: str,
1034
- tag: str,
1031
+ name: str,
1035
1032
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
1036
1033
  environment_name: Optional[str] = None,
1037
1034
  ) -> "_Function":
@@ -1050,7 +1047,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1050
1047
  assert resolver.client and resolver.client.stub
1051
1048
  request = api_pb2.FunctionGetRequest(
1052
1049
  app_name=app_name,
1053
- object_tag=tag,
1050
+ object_tag=name,
1054
1051
  namespace=namespace,
1055
1052
  environment_name=_get_environment_name(environment_name, resolver) or "",
1056
1053
  )
@@ -1070,9 +1067,10 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1070
1067
  return cls._from_loader(_load_remote, rep, is_another_app=True, hydrate_lazily=True)
1071
1068
 
1072
1069
  @staticmethod
1070
+ @renamed_parameter((2024, 12, 18), "tag", "name")
1073
1071
  async def lookup(
1074
1072
  app_name: str,
1075
- tag: str,
1073
+ name: str,
1076
1074
  namespace=api_pb2.DEPLOYMENT_NAMESPACE_WORKSPACE,
1077
1075
  client: Optional[_Client] = None,
1078
1076
  environment_name: Optional[str] = None,
@@ -1086,7 +1084,7 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], _Object, type
1086
1084
  f = modal.Function.lookup("other-app", "function")
1087
1085
  ```
1088
1086
  """
1089
- obj = _Function.from_name(app_name, tag, namespace=namespace, environment_name=environment_name)
1087
+ obj = _Function.from_name(app_name, name, namespace=namespace, environment_name=environment_name)
1090
1088
  if client is None:
1091
1089
  client = await _Client.from_env()
1092
1090
  resolver = Resolver(client=client)
modal/functions.pyi CHANGED
@@ -100,6 +100,7 @@ class _FunctionSpec:
100
100
  memory: typing.Union[int, tuple[int, int], None]
101
101
  ephemeral_disk: typing.Optional[int]
102
102
  scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement]
103
+ proxy: typing.Optional[modal.proxy._Proxy]
103
104
 
104
105
  def __init__(
105
106
  self,
@@ -121,6 +122,7 @@ class _FunctionSpec:
121
122
  memory: typing.Union[int, tuple[int, int], None],
122
123
  ephemeral_disk: typing.Optional[int],
123
124
  scheduler_placement: typing.Optional[modal.scheduler_placement.SchedulerPlacement],
125
+ proxy: typing.Optional[modal.proxy._Proxy],
124
126
  ) -> None: ...
125
127
  def __repr__(self): ...
126
128
  def __eq__(self, other): ...
@@ -206,12 +208,12 @@ class _Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.
206
208
  async def keep_warm(self, warm_pool_size: int) -> None: ...
207
209
  @classmethod
208
210
  def from_name(
209
- cls: type[_Function], app_name: str, tag: str, namespace=1, environment_name: typing.Optional[str] = None
211
+ cls: type[_Function], app_name: str, name: str, namespace=1, environment_name: typing.Optional[str] = None
210
212
  ) -> _Function: ...
211
213
  @staticmethod
212
214
  async def lookup(
213
215
  app_name: str,
214
- tag: str,
216
+ name: str,
215
217
  namespace=1,
216
218
  client: typing.Optional[modal.client._Client] = None,
217
219
  environment_name: typing.Optional[str] = None,
@@ -381,14 +383,14 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
381
383
 
382
384
  @classmethod
383
385
  def from_name(
384
- cls: type[Function], app_name: str, tag: str, namespace=1, environment_name: typing.Optional[str] = None
386
+ cls: type[Function], app_name: str, name: str, namespace=1, environment_name: typing.Optional[str] = None
385
387
  ) -> Function: ...
386
388
 
387
389
  class __lookup_spec(typing_extensions.Protocol):
388
390
  def __call__(
389
391
  self,
390
392
  app_name: str,
391
- tag: str,
393
+ name: str,
392
394
  namespace=1,
393
395
  client: typing.Optional[modal.client.Client] = None,
394
396
  environment_name: typing.Optional[str] = None,
@@ -396,7 +398,7 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
396
398
  async def aio(
397
399
  self,
398
400
  app_name: str,
399
- tag: str,
401
+ name: str,
400
402
  namespace=1,
401
403
  client: typing.Optional[modal.client.Client] = None,
402
404
  environment_name: typing.Optional[str] = None,
@@ -448,7 +450,11 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
448
450
 
449
451
  _call_function_nowait: ___call_function_nowait_spec
450
452
 
451
- def _call_generator(self, args, kwargs): ...
453
+ class ___call_generator_spec(typing_extensions.Protocol):
454
+ def __call__(self, args, kwargs): ...
455
+ def aio(self, args, kwargs): ...
456
+
457
+ _call_generator: ___call_generator_spec
452
458
 
453
459
  class ___call_generator_nowait_spec(typing_extensions.Protocol):
454
460
  def __call__(self, args, kwargs): ...
@@ -456,11 +462,11 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
456
462
 
457
463
  _call_generator_nowait: ___call_generator_nowait_spec
458
464
 
459
- class __remote_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
465
+ class __remote_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
460
466
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
461
467
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> ReturnType_INNER: ...
462
468
 
463
- remote: __remote_spec[ReturnType, P]
469
+ remote: __remote_spec[P, ReturnType]
464
470
 
465
471
  class __remote_gen_spec(typing_extensions.Protocol):
466
472
  def __call__(self, *args, **kwargs) -> typing.Generator[typing.Any, None, None]: ...
@@ -473,17 +479,17 @@ class Function(typing.Generic[P, ReturnType, OriginalReturnType], modal.object.O
473
479
  def _get_obj(self) -> typing.Optional[modal.cls.Obj]: ...
474
480
  def local(self, *args: P.args, **kwargs: P.kwargs) -> OriginalReturnType: ...
475
481
 
476
- class ___experimental_spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
482
+ class ___experimental_spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
477
483
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
478
484
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
479
485
 
480
- _experimental_spawn: ___experimental_spawn_spec[ReturnType, P]
486
+ _experimental_spawn: ___experimental_spawn_spec[P, ReturnType]
481
487
 
482
- class __spawn_spec(typing_extensions.Protocol[ReturnType_INNER, P_INNER]):
488
+ class __spawn_spec(typing_extensions.Protocol[P_INNER, ReturnType_INNER]):
483
489
  def __call__(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
484
490
  async def aio(self, *args: P_INNER.args, **kwargs: P_INNER.kwargs) -> FunctionCall[ReturnType_INNER]: ...
485
491
 
486
- spawn: __spawn_spec[ReturnType, P]
492
+ spawn: __spawn_spec[P, ReturnType]
487
493
 
488
494
  def get_raw_f(self) -> typing.Callable[..., typing.Any]: ...
489
495
 
modal/image.py CHANGED
@@ -30,14 +30,15 @@ from ._resolver import Resolver
30
30
  from ._serialization import serialize
31
31
  from ._utils.async_utils import synchronize_api
32
32
  from ._utils.blob_utils import MAX_OBJECT_SIZE_BYTES
33
+ from ._utils.deprecation import deprecation_error, deprecation_warning
33
34
  from ._utils.function_utils import FunctionInfo
34
35
  from ._utils.grpc_utils import RETRYABLE_GRPC_STATUS_CODES, retry_transient_errors
35
36
  from .client import _Client
36
37
  from .cloud_bucket_mount import _CloudBucketMount
37
38
  from .config import config, logger, user_config_path
38
39
  from .environments import _get_environment_cached
39
- from .exception import InvalidError, NotFoundError, RemoteError, VersionError, deprecation_error, deprecation_warning
40
- from .file_pattern_matcher import FilePatternMatcher
40
+ from .exception import InvalidError, NotFoundError, RemoteError, VersionError
41
+ from .file_pattern_matcher import NON_PYTHON_FILES
41
42
  from .gpu import GPU_T, parse_gpu_config
42
43
  from .mount import _Mount, python_standalone_mount_name
43
44
  from .network_file_system import _NetworkFileSystem
@@ -570,21 +571,6 @@ class _Image(_Object, type_prefix="im"):
570
571
  obj.force_build = force_build
571
572
  return obj
572
573
 
573
- def extend(self, **kwargs) -> "_Image":
574
- """mdmd:hidden"""
575
- deprecation_error(
576
- (2024, 3, 7),
577
- "`Image.extend` is deprecated; please use a higher-level method, such as `Image.dockerfile_commands`.",
578
- )
579
-
580
- def build_dockerfile(version: ImageBuilderVersion) -> DockerfileSpec:
581
- return DockerfileSpec(
582
- commands=kwargs.pop("dockerfile_commands", []),
583
- context_files=kwargs.pop("context_files", {}),
584
- )
585
-
586
- return _Image._from_args(base_images={"base": self}, dockerfile_function=build_dockerfile, **kwargs)
587
-
588
574
  def copy_mount(self, mount: _Mount, remote_path: Union[str, Path] = ".") -> "_Image":
589
575
  """Copy the entire contents of a `modal.Mount` into an image.
590
576
  Useful when files only available locally are required during the image
@@ -720,7 +706,9 @@ class _Image(_Object, type_prefix="im"):
720
706
  context_mount=mount,
721
707
  )
722
708
 
723
- def add_local_python_source(self, *modules: str, copy: bool = False) -> "_Image":
709
+ def add_local_python_source(
710
+ self, *modules: str, copy: bool = False, ignore: Union[Sequence[str], Callable[[Path], bool]] = NON_PYTHON_FILES
711
+ ) -> "_Image":
724
712
  """Adds locally available Python packages/modules to containers
725
713
 
726
714
  Adds all files from the specified Python package or module to containers running the Image.
@@ -738,9 +726,22 @@ class _Image(_Object, type_prefix="im"):
738
726
  **Note:** This excludes all dot-prefixed subdirectories or files and all `.pyc`/`__pycache__` files.
739
727
  To add full directories with finer control, use `.add_local_dir()` instead and specify `/root` as
740
728
  the destination directory.
741
- """
742
729
 
743
- mount = _Mount.from_local_python_packages(*modules, ignore=~FilePatternMatcher("**/*.py"))
730
+ By default only includes `.py`-files in the source modules. Set the `ignore` argument to a list of patterns
731
+ or a callable to override this behavior, e.g.:
732
+
733
+ ```py
734
+ # includes everything except data.json
735
+ modal.Image.debian_slim().add_local_python_source("mymodule", ignore=["data.json"])
736
+
737
+ # exclude large files
738
+ modal.Image.debian_slim().add_local_python_source(
739
+ "mymodule",
740
+ ignore=lambda p: p.stat().st_size > 1e9
741
+ )
742
+ ```
743
+ """
744
+ mount = _Mount.from_local_python_packages(*modules, ignore=ignore)
744
745
  return self._add_mount_layer_or_copy(mount, copy=copy)
745
746
 
746
747
  def copy_local_dir(