modal 0.62.115__py3-none-any.whl → 0.72.13__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 (220) hide show
  1. modal/__init__.py +13 -9
  2. modal/__main__.py +41 -3
  3. modal/_clustered_functions.py +80 -0
  4. modal/_clustered_functions.pyi +22 -0
  5. modal/_container_entrypoint.py +402 -398
  6. modal/_ipython.py +3 -13
  7. modal/_location.py +17 -10
  8. modal/_output.py +243 -99
  9. modal/_pty.py +2 -2
  10. modal/_resolver.py +55 -60
  11. modal/_resources.py +26 -7
  12. modal/_runtime/__init__.py +1 -0
  13. modal/_runtime/asgi.py +519 -0
  14. modal/_runtime/container_io_manager.py +1025 -0
  15. modal/{execution_context.py → _runtime/execution_context.py} +11 -2
  16. modal/_runtime/telemetry.py +169 -0
  17. modal/_runtime/user_code_imports.py +356 -0
  18. modal/_serialization.py +123 -6
  19. modal/_traceback.py +47 -187
  20. modal/_tunnel.py +50 -14
  21. modal/_tunnel.pyi +19 -36
  22. modal/_utils/app_utils.py +3 -17
  23. modal/_utils/async_utils.py +386 -104
  24. modal/_utils/blob_utils.py +157 -186
  25. modal/_utils/bytes_io_segment_payload.py +97 -0
  26. modal/_utils/deprecation.py +89 -0
  27. modal/_utils/docker_utils.py +98 -0
  28. modal/_utils/function_utils.py +299 -98
  29. modal/_utils/grpc_testing.py +47 -34
  30. modal/_utils/grpc_utils.py +54 -21
  31. modal/_utils/hash_utils.py +51 -10
  32. modal/_utils/http_utils.py +39 -9
  33. modal/_utils/logger.py +2 -1
  34. modal/_utils/mount_utils.py +34 -16
  35. modal/_utils/name_utils.py +58 -0
  36. modal/_utils/package_utils.py +14 -1
  37. modal/_utils/pattern_utils.py +205 -0
  38. modal/_utils/rand_pb_testing.py +3 -3
  39. modal/_utils/shell_utils.py +15 -49
  40. modal/_vendor/a2wsgi_wsgi.py +62 -72
  41. modal/_vendor/cloudpickle.py +1 -1
  42. modal/_watcher.py +12 -10
  43. modal/app.py +561 -323
  44. modal/app.pyi +474 -262
  45. modal/call_graph.py +7 -6
  46. modal/cli/_download.py +22 -6
  47. modal/cli/_traceback.py +200 -0
  48. modal/cli/app.py +203 -42
  49. modal/cli/config.py +12 -5
  50. modal/cli/container.py +61 -13
  51. modal/cli/dict.py +128 -0
  52. modal/cli/entry_point.py +26 -13
  53. modal/cli/environment.py +40 -9
  54. modal/cli/import_refs.py +21 -48
  55. modal/cli/launch.py +28 -14
  56. modal/cli/network_file_system.py +57 -21
  57. modal/cli/profile.py +1 -1
  58. modal/cli/programs/run_jupyter.py +34 -9
  59. modal/cli/programs/vscode.py +58 -8
  60. modal/cli/queues.py +131 -0
  61. modal/cli/run.py +199 -96
  62. modal/cli/secret.py +5 -4
  63. modal/cli/token.py +7 -2
  64. modal/cli/utils.py +74 -8
  65. modal/cli/volume.py +97 -56
  66. modal/client.py +248 -144
  67. modal/client.pyi +156 -124
  68. modal/cloud_bucket_mount.py +43 -30
  69. modal/cloud_bucket_mount.pyi +32 -25
  70. modal/cls.py +528 -141
  71. modal/cls.pyi +189 -145
  72. modal/config.py +32 -15
  73. modal/container_process.py +177 -0
  74. modal/container_process.pyi +82 -0
  75. modal/dict.py +50 -54
  76. modal/dict.pyi +120 -164
  77. modal/environments.py +106 -5
  78. modal/environments.pyi +77 -25
  79. modal/exception.py +30 -43
  80. modal/experimental.py +62 -2
  81. modal/file_io.py +537 -0
  82. modal/file_io.pyi +235 -0
  83. modal/file_pattern_matcher.py +196 -0
  84. modal/functions.py +846 -428
  85. modal/functions.pyi +446 -387
  86. modal/gpu.py +57 -44
  87. modal/image.py +943 -417
  88. modal/image.pyi +584 -245
  89. modal/io_streams.py +434 -0
  90. modal/io_streams.pyi +122 -0
  91. modal/mount.py +223 -90
  92. modal/mount.pyi +241 -243
  93. modal/network_file_system.py +85 -86
  94. modal/network_file_system.pyi +151 -110
  95. modal/object.py +66 -36
  96. modal/object.pyi +166 -143
  97. modal/output.py +63 -0
  98. modal/parallel_map.py +73 -47
  99. modal/parallel_map.pyi +51 -63
  100. modal/partial_function.py +272 -107
  101. modal/partial_function.pyi +219 -120
  102. modal/proxy.py +15 -12
  103. modal/proxy.pyi +3 -8
  104. modal/queue.py +96 -72
  105. modal/queue.pyi +210 -135
  106. modal/requirements/2024.04.txt +2 -1
  107. modal/requirements/2024.10.txt +16 -0
  108. modal/requirements/README.md +21 -0
  109. modal/requirements/base-images.json +22 -0
  110. modal/retries.py +45 -4
  111. modal/runner.py +325 -203
  112. modal/runner.pyi +124 -110
  113. modal/running_app.py +27 -4
  114. modal/sandbox.py +509 -231
  115. modal/sandbox.pyi +396 -169
  116. modal/schedule.py +2 -2
  117. modal/scheduler_placement.py +20 -3
  118. modal/secret.py +41 -25
  119. modal/secret.pyi +62 -42
  120. modal/serving.py +39 -49
  121. modal/serving.pyi +37 -43
  122. modal/stream_type.py +15 -0
  123. modal/token_flow.py +5 -3
  124. modal/token_flow.pyi +37 -32
  125. modal/volume.py +123 -137
  126. modal/volume.pyi +228 -221
  127. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
  128. modal-0.72.13.dist-info/RECORD +174 -0
  129. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
  130. modal_docs/gen_reference_docs.py +3 -1
  131. modal_docs/mdmd/mdmd.py +0 -1
  132. modal_docs/mdmd/signatures.py +1 -2
  133. modal_global_objects/images/base_images.py +28 -0
  134. modal_global_objects/mounts/python_standalone.py +2 -2
  135. modal_proto/__init__.py +1 -1
  136. modal_proto/api.proto +1231 -531
  137. modal_proto/api_grpc.py +750 -430
  138. modal_proto/api_pb2.py +2102 -1176
  139. modal_proto/api_pb2.pyi +8859 -0
  140. modal_proto/api_pb2_grpc.py +1329 -675
  141. modal_proto/api_pb2_grpc.pyi +1416 -0
  142. modal_proto/modal_api_grpc.py +149 -0
  143. modal_proto/modal_options_grpc.py +3 -0
  144. modal_proto/options_pb2.pyi +20 -0
  145. modal_proto/options_pb2_grpc.pyi +7 -0
  146. modal_proto/py.typed +0 -0
  147. modal_version/__init__.py +1 -1
  148. modal_version/_version_generated.py +2 -2
  149. modal/_asgi.py +0 -370
  150. modal/_container_exec.py +0 -128
  151. modal/_container_io_manager.py +0 -646
  152. modal/_container_io_manager.pyi +0 -412
  153. modal/_sandbox_shell.py +0 -49
  154. modal/app_utils.py +0 -20
  155. modal/app_utils.pyi +0 -17
  156. modal/execution_context.pyi +0 -37
  157. modal/shared_volume.py +0 -23
  158. modal/shared_volume.pyi +0 -24
  159. modal-0.62.115.dist-info/RECORD +0 -207
  160. modal_global_objects/images/conda.py +0 -15
  161. modal_global_objects/images/debian_slim.py +0 -15
  162. modal_global_objects/images/micromamba.py +0 -15
  163. test/__init__.py +0 -1
  164. test/aio_test.py +0 -12
  165. test/async_utils_test.py +0 -279
  166. test/blob_test.py +0 -67
  167. test/cli_imports_test.py +0 -149
  168. test/cli_test.py +0 -674
  169. test/client_test.py +0 -203
  170. test/cloud_bucket_mount_test.py +0 -22
  171. test/cls_test.py +0 -636
  172. test/config_test.py +0 -149
  173. test/conftest.py +0 -1485
  174. test/container_app_test.py +0 -50
  175. test/container_test.py +0 -1405
  176. test/cpu_test.py +0 -23
  177. test/decorator_test.py +0 -85
  178. test/deprecation_test.py +0 -34
  179. test/dict_test.py +0 -51
  180. test/e2e_test.py +0 -68
  181. test/error_test.py +0 -7
  182. test/function_serialization_test.py +0 -32
  183. test/function_test.py +0 -791
  184. test/function_utils_test.py +0 -101
  185. test/gpu_test.py +0 -159
  186. test/grpc_utils_test.py +0 -82
  187. test/helpers.py +0 -47
  188. test/image_test.py +0 -814
  189. test/live_reload_test.py +0 -80
  190. test/lookup_test.py +0 -70
  191. test/mdmd_test.py +0 -329
  192. test/mount_test.py +0 -162
  193. test/mounted_files_test.py +0 -327
  194. test/network_file_system_test.py +0 -188
  195. test/notebook_test.py +0 -66
  196. test/object_test.py +0 -41
  197. test/package_utils_test.py +0 -25
  198. test/queue_test.py +0 -115
  199. test/resolver_test.py +0 -59
  200. test/retries_test.py +0 -67
  201. test/runner_test.py +0 -85
  202. test/sandbox_test.py +0 -191
  203. test/schedule_test.py +0 -15
  204. test/scheduler_placement_test.py +0 -57
  205. test/secret_test.py +0 -89
  206. test/serialization_test.py +0 -50
  207. test/stub_composition_test.py +0 -10
  208. test/stub_test.py +0 -361
  209. test/test_asgi_wrapper.py +0 -234
  210. test/token_flow_test.py +0 -18
  211. test/traceback_test.py +0 -135
  212. test/tunnel_test.py +0 -29
  213. test/utils_test.py +0 -88
  214. test/version_test.py +0 -14
  215. test/volume_test.py +0 -397
  216. test/watcher_test.py +0 -58
  217. test/webhook_test.py +0 -145
  218. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
  219. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
  220. {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/entry_points.txt +0 -0
modal/file_io.pyi ADDED
@@ -0,0 +1,235 @@
1
+ import _typeshed
2
+ import enum
3
+ import modal.client
4
+ import modal_proto.api_pb2
5
+ import typing
6
+ import typing_extensions
7
+
8
+ T = typing.TypeVar("T")
9
+
10
+ async def _delete_bytes(
11
+ file: _FileIO, start: typing.Optional[int] = None, end: typing.Optional[int] = None
12
+ ) -> None: ...
13
+ async def _replace_bytes(
14
+ file: _FileIO, data: bytes, start: typing.Optional[int] = None, end: typing.Optional[int] = None
15
+ ) -> None: ...
16
+
17
+ class FileWatchEventType(enum.Enum):
18
+ Unknown = "Unknown"
19
+ Access = "Access"
20
+ Create = "Create"
21
+ Modify = "Modify"
22
+ Remove = "Remove"
23
+
24
+ class FileWatchEvent:
25
+ paths: list[str]
26
+ type: FileWatchEventType
27
+
28
+ def __init__(self, paths: list[str], type: FileWatchEventType) -> None: ...
29
+ def __repr__(self): ...
30
+ def __eq__(self, other): ...
31
+
32
+ class _FileIO(typing.Generic[T]):
33
+ _task_id: str
34
+ _file_descriptor: str
35
+ _client: modal.client._Client
36
+ _watch_output_buffer: list[typing.Optional[bytes]]
37
+
38
+ def __init__(self, client: modal.client._Client, task_id: str) -> None: ...
39
+ def _validate_mode(self, mode: str) -> None: ...
40
+ def _handle_error(self, error: modal_proto.api_pb2.SystemErrorMessage) -> None: ...
41
+ def _consume_output(self, exec_id: str) -> typing.AsyncIterator[typing.Optional[bytes]]: ...
42
+ async def _consume_watch_output(self, exec_id: str) -> None: ...
43
+ async def _parse_watch_output(self, event: bytes) -> typing.Optional[FileWatchEvent]: ...
44
+ async def _wait(self, exec_id: str) -> bytes: ...
45
+ def _validate_type(self, data: typing.Union[bytes, str]) -> None: ...
46
+ async def _open_file(self, path: str, mode: str) -> None: ...
47
+ @classmethod
48
+ async def create(
49
+ cls,
50
+ path: str,
51
+ mode: typing.Union[_typeshed.OpenTextMode, _typeshed.OpenBinaryMode],
52
+ client: modal.client._Client,
53
+ task_id: str,
54
+ ) -> _FileIO: ...
55
+ async def _make_read_request(self, n: typing.Optional[int]) -> bytes: ...
56
+ async def read(self, n: typing.Optional[int] = None) -> T: ...
57
+ async def readline(self) -> T: ...
58
+ async def readlines(self) -> typing.Sequence[T]: ...
59
+ async def write(self, data: typing.Union[bytes, str]) -> None: ...
60
+ async def flush(self) -> None: ...
61
+ def _get_whence(self, whence: int): ...
62
+ async def seek(self, offset: int, whence: int = 0) -> None: ...
63
+ @classmethod
64
+ async def ls(cls, path: str, client: modal.client._Client, task_id: str) -> list[str]: ...
65
+ @classmethod
66
+ async def mkdir(cls, path: str, client: modal.client._Client, task_id: str, parents: bool = False) -> None: ...
67
+ @classmethod
68
+ async def rm(cls, path: str, client: modal.client._Client, task_id: str, recursive: bool = False) -> None: ...
69
+ @classmethod
70
+ def watch(
71
+ cls,
72
+ path: str,
73
+ client: modal.client._Client,
74
+ task_id: str,
75
+ filter: typing.Optional[list[FileWatchEventType]] = None,
76
+ recursive: bool = False,
77
+ timeout: typing.Optional[int] = None,
78
+ ) -> typing.AsyncIterator[FileWatchEvent]: ...
79
+ async def _close(self) -> None: ...
80
+ async def close(self) -> None: ...
81
+ def _check_writable(self) -> None: ...
82
+ def _check_readable(self) -> None: ...
83
+ def _check_closed(self) -> None: ...
84
+ async def __aenter__(self) -> _FileIO: ...
85
+ async def __aexit__(self, exc_type, exc_value, traceback) -> None: ...
86
+
87
+ class __delete_bytes_spec(typing_extensions.Protocol):
88
+ def __call__(self, file: FileIO, start: typing.Optional[int] = None, end: typing.Optional[int] = None) -> None: ...
89
+ async def aio(self, file: FileIO, start: typing.Optional[int] = None, end: typing.Optional[int] = None) -> None: ...
90
+
91
+ delete_bytes: __delete_bytes_spec
92
+
93
+ class __replace_bytes_spec(typing_extensions.Protocol):
94
+ def __call__(
95
+ self, file: FileIO, data: bytes, start: typing.Optional[int] = None, end: typing.Optional[int] = None
96
+ ) -> None: ...
97
+ async def aio(
98
+ self, file: FileIO, data: bytes, start: typing.Optional[int] = None, end: typing.Optional[int] = None
99
+ ) -> None: ...
100
+
101
+ replace_bytes: __replace_bytes_spec
102
+
103
+ T_INNER = typing.TypeVar("T_INNER", covariant=True)
104
+
105
+ class FileIO(typing.Generic[T]):
106
+ _task_id: str
107
+ _file_descriptor: str
108
+ _client: modal.client.Client
109
+ _watch_output_buffer: list[typing.Optional[bytes]]
110
+
111
+ def __init__(self, client: modal.client.Client, task_id: str) -> None: ...
112
+ def _validate_mode(self, mode: str) -> None: ...
113
+ def _handle_error(self, error: modal_proto.api_pb2.SystemErrorMessage) -> None: ...
114
+
115
+ class ___consume_output_spec(typing_extensions.Protocol):
116
+ def __call__(self, exec_id: str) -> typing.Iterator[typing.Optional[bytes]]: ...
117
+ def aio(self, exec_id: str) -> typing.AsyncIterator[typing.Optional[bytes]]: ...
118
+
119
+ _consume_output: ___consume_output_spec
120
+
121
+ class ___consume_watch_output_spec(typing_extensions.Protocol):
122
+ def __call__(self, exec_id: str) -> None: ...
123
+ async def aio(self, exec_id: str) -> None: ...
124
+
125
+ _consume_watch_output: ___consume_watch_output_spec
126
+
127
+ class ___parse_watch_output_spec(typing_extensions.Protocol):
128
+ def __call__(self, event: bytes) -> typing.Optional[FileWatchEvent]: ...
129
+ async def aio(self, event: bytes) -> typing.Optional[FileWatchEvent]: ...
130
+
131
+ _parse_watch_output: ___parse_watch_output_spec
132
+
133
+ class ___wait_spec(typing_extensions.Protocol):
134
+ def __call__(self, exec_id: str) -> bytes: ...
135
+ async def aio(self, exec_id: str) -> bytes: ...
136
+
137
+ _wait: ___wait_spec
138
+
139
+ def _validate_type(self, data: typing.Union[bytes, str]) -> None: ...
140
+
141
+ class ___open_file_spec(typing_extensions.Protocol):
142
+ def __call__(self, path: str, mode: str) -> None: ...
143
+ async def aio(self, path: str, mode: str) -> None: ...
144
+
145
+ _open_file: ___open_file_spec
146
+
147
+ @classmethod
148
+ def create(
149
+ cls,
150
+ path: str,
151
+ mode: typing.Union[_typeshed.OpenTextMode, _typeshed.OpenBinaryMode],
152
+ client: modal.client.Client,
153
+ task_id: str,
154
+ ) -> FileIO: ...
155
+
156
+ class ___make_read_request_spec(typing_extensions.Protocol):
157
+ def __call__(self, n: typing.Optional[int]) -> bytes: ...
158
+ async def aio(self, n: typing.Optional[int]) -> bytes: ...
159
+
160
+ _make_read_request: ___make_read_request_spec
161
+
162
+ class __read_spec(typing_extensions.Protocol[T_INNER]):
163
+ def __call__(self, n: typing.Optional[int] = None) -> T_INNER: ...
164
+ async def aio(self, n: typing.Optional[int] = None) -> T_INNER: ...
165
+
166
+ read: __read_spec[T]
167
+
168
+ class __readline_spec(typing_extensions.Protocol[T_INNER]):
169
+ def __call__(self) -> T_INNER: ...
170
+ async def aio(self) -> T_INNER: ...
171
+
172
+ readline: __readline_spec[T]
173
+
174
+ class __readlines_spec(typing_extensions.Protocol[T_INNER]):
175
+ def __call__(self) -> typing.Sequence[T_INNER]: ...
176
+ async def aio(self) -> typing.Sequence[T_INNER]: ...
177
+
178
+ readlines: __readlines_spec[T]
179
+
180
+ class __write_spec(typing_extensions.Protocol):
181
+ def __call__(self, data: typing.Union[bytes, str]) -> None: ...
182
+ async def aio(self, data: typing.Union[bytes, str]) -> None: ...
183
+
184
+ write: __write_spec
185
+
186
+ class __flush_spec(typing_extensions.Protocol):
187
+ def __call__(self) -> None: ...
188
+ async def aio(self) -> None: ...
189
+
190
+ flush: __flush_spec
191
+
192
+ def _get_whence(self, whence: int): ...
193
+
194
+ class __seek_spec(typing_extensions.Protocol):
195
+ def __call__(self, offset: int, whence: int = 0) -> None: ...
196
+ async def aio(self, offset: int, whence: int = 0) -> None: ...
197
+
198
+ seek: __seek_spec
199
+
200
+ @classmethod
201
+ def ls(cls, path: str, client: modal.client.Client, task_id: str) -> list[str]: ...
202
+ @classmethod
203
+ def mkdir(cls, path: str, client: modal.client.Client, task_id: str, parents: bool = False) -> None: ...
204
+ @classmethod
205
+ def rm(cls, path: str, client: modal.client.Client, task_id: str, recursive: bool = False) -> None: ...
206
+ @classmethod
207
+ def watch(
208
+ cls,
209
+ path: str,
210
+ client: modal.client.Client,
211
+ task_id: str,
212
+ filter: typing.Optional[list[FileWatchEventType]] = None,
213
+ recursive: bool = False,
214
+ timeout: typing.Optional[int] = None,
215
+ ) -> typing.Iterator[FileWatchEvent]: ...
216
+
217
+ class ___close_spec(typing_extensions.Protocol):
218
+ def __call__(self) -> None: ...
219
+ async def aio(self) -> None: ...
220
+
221
+ _close: ___close_spec
222
+
223
+ class __close_spec(typing_extensions.Protocol):
224
+ def __call__(self) -> None: ...
225
+ async def aio(self) -> None: ...
226
+
227
+ close: __close_spec
228
+
229
+ def _check_writable(self) -> None: ...
230
+ def _check_readable(self) -> None: ...
231
+ def _check_closed(self) -> None: ...
232
+ def __enter__(self) -> FileIO: ...
233
+ async def __aenter__(self) -> FileIO: ...
234
+ def __exit__(self, exc_type, exc_value, traceback) -> None: ...
235
+ async def __aexit__(self, exc_type, exc_value, traceback) -> None: ...
@@ -0,0 +1,196 @@
1
+ # Copyright Modal Labs 2024
2
+ """Pattern matching library ported from https://github.com/moby/patternmatcher.
3
+
4
+ This is the same pattern-matching logic used by Docker, except it is written in
5
+ Python rather than Go. Also, the original Go library has a couple deprecated
6
+ functions that we don't implement in this port.
7
+
8
+ The main way to use this library is by constructing a `FilePatternMatcher` object,
9
+ then asking it whether file paths match any of its patterns.
10
+ """
11
+
12
+ import os
13
+ from abc import abstractmethod
14
+ from pathlib import Path
15
+ from typing import Callable, Optional, Sequence, Union
16
+
17
+ from ._utils.pattern_utils import Pattern
18
+
19
+
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):
63
+ """
64
+ Allows matching file Path objects against a list of patterns.
65
+
66
+ **Usage:**
67
+ ```python
68
+ from pathlib import Path
69
+ from modal import FilePatternMatcher
70
+
71
+ matcher = FilePatternMatcher("*.py")
72
+
73
+ assert matcher(Path("foo.py"))
74
+
75
+ # You can also negate the matcher.
76
+ negated_matcher = ~matcher
77
+
78
+ assert not negated_matcher(Path("foo.py"))
79
+ ```
80
+ """
81
+
82
+ patterns: list[Pattern]
83
+ _delayed_init: Callable[[], None] = None
84
+
85
+ def _set_patterns(self, patterns: Sequence[str]) -> None:
86
+ self.patterns = []
87
+ for pattern in list(patterns):
88
+ pattern = pattern.strip()
89
+ if not pattern:
90
+ continue
91
+ pattern = os.path.normpath(pattern)
92
+ new_pattern = Pattern()
93
+ if pattern[0] == "!":
94
+ if len(pattern) == 1:
95
+ raise ValueError('Illegal exclusion pattern: "!"')
96
+ new_pattern.exclusion = True
97
+ pattern = pattern[1:]
98
+ # In Python, we can proceed without explicit syntax checking
99
+ new_pattern.cleaned_pattern = pattern
100
+ new_pattern.dirs = pattern.split(os.path.sep)
101
+ self.patterns.append(new_pattern)
102
+
103
+ def __init__(self, *pattern: str) -> None:
104
+ """Initialize a new FilePatternMatcher instance.
105
+
106
+ Args:
107
+ pattern (str): One or more pattern strings.
108
+
109
+ Raises:
110
+ ValueError: If an illegal exclusion pattern is provided.
111
+ """
112
+ self._set_patterns(pattern)
113
+
114
+ @classmethod
115
+ def from_file(cls, file_path: Union[str, Path]) -> "FilePatternMatcher":
116
+ """Initialize a new FilePatternMatcher instance from a file.
117
+
118
+ The patterns in the file will be read lazily when the matcher is first used.
119
+
120
+ Args:
121
+ file_path (Path): The path to the file containing patterns.
122
+
123
+ **Usage:**
124
+ ```python
125
+ from modal import FilePatternMatcher
126
+
127
+ matcher = FilePatternMatcher.from_file("/path/to/ignorefile")
128
+ ```
129
+
130
+ """
131
+ uninitialized = cls.__new__(cls)
132
+
133
+ def _delayed_init():
134
+ uninitialized._set_patterns(Path(file_path).read_text("utf8").splitlines())
135
+ uninitialized._delayed_init = None
136
+
137
+ uninitialized._delayed_init = _delayed_init
138
+ return uninitialized
139
+
140
+ def _matches(self, file_path: str) -> bool:
141
+ """Check if the file path or any of its parent directories match the patterns.
142
+
143
+ This is equivalent to `MatchesOrParentMatches()` in the original Go
144
+ library. The reason is that `Matches()` in the original library is
145
+ deprecated due to buggy behavior.
146
+ """
147
+
148
+ matched = False
149
+ file_path = os.path.normpath(file_path)
150
+ if file_path == ".":
151
+ # Don't let them exclude everything; kind of silly.
152
+ return False
153
+ parent_path = os.path.dirname(file_path)
154
+ if parent_path == "":
155
+ parent_path = "."
156
+ parent_path_dirs = parent_path.split(os.path.sep)
157
+
158
+ for pattern in self.patterns:
159
+ # Skip evaluation based on current match status and pattern exclusion
160
+ if pattern.exclusion != matched:
161
+ continue
162
+
163
+ match = pattern.match(file_path)
164
+
165
+ if not match and parent_path != ".":
166
+ # Check if the pattern matches any of the parent directories
167
+ for i in range(len(parent_path_dirs)):
168
+ dir_path = os.path.sep.join(parent_path_dirs[: i + 1])
169
+ if pattern.match(dir_path):
170
+ match = True
171
+ break
172
+
173
+ if match:
174
+ matched = not pattern.exclusion
175
+
176
+ return matched
177
+
178
+ def __call__(self, file_path: Path) -> bool:
179
+ if self._delayed_init:
180
+ self._delayed_init()
181
+ return self._matches(str(file_path))
182
+
183
+
184
+ # _with_repr allows us to use this matcher as a default value in a function signature
185
+ # and get a nice repr in the docs and auto-generated type stubs:
186
+ NON_PYTHON_FILES = (~FilePatternMatcher("**/*.py"))._with_repr(f"{__name__}.NON_PYTHON_FILES")
187
+ _NOTHING = (~FilePatternMatcher())._with_repr(f"{__name__}._NOTHING") # match everything = ignore nothing
188
+
189
+
190
+ def _ignore_fn(ignore: Union[Sequence[str], Callable[[Path], bool]]) -> Callable[[Path], bool]:
191
+ # if a callable is passed, return it
192
+ # otherwise, treat input as a sequence of patterns and return a callable pattern matcher for those
193
+ if callable(ignore):
194
+ return ignore
195
+
196
+ return FilePatternMatcher(*ignore)