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.
- modal/__init__.py +13 -9
- modal/__main__.py +41 -3
- modal/_clustered_functions.py +80 -0
- modal/_clustered_functions.pyi +22 -0
- modal/_container_entrypoint.py +402 -398
- modal/_ipython.py +3 -13
- modal/_location.py +17 -10
- modal/_output.py +243 -99
- modal/_pty.py +2 -2
- modal/_resolver.py +55 -60
- modal/_resources.py +26 -7
- modal/_runtime/__init__.py +1 -0
- modal/_runtime/asgi.py +519 -0
- modal/_runtime/container_io_manager.py +1025 -0
- modal/{execution_context.py → _runtime/execution_context.py} +11 -2
- modal/_runtime/telemetry.py +169 -0
- modal/_runtime/user_code_imports.py +356 -0
- modal/_serialization.py +123 -6
- modal/_traceback.py +47 -187
- modal/_tunnel.py +50 -14
- modal/_tunnel.pyi +19 -36
- modal/_utils/app_utils.py +3 -17
- modal/_utils/async_utils.py +386 -104
- modal/_utils/blob_utils.py +157 -186
- modal/_utils/bytes_io_segment_payload.py +97 -0
- modal/_utils/deprecation.py +89 -0
- modal/_utils/docker_utils.py +98 -0
- modal/_utils/function_utils.py +299 -98
- modal/_utils/grpc_testing.py +47 -34
- modal/_utils/grpc_utils.py +54 -21
- modal/_utils/hash_utils.py +51 -10
- modal/_utils/http_utils.py +39 -9
- modal/_utils/logger.py +2 -1
- modal/_utils/mount_utils.py +34 -16
- modal/_utils/name_utils.py +58 -0
- modal/_utils/package_utils.py +14 -1
- modal/_utils/pattern_utils.py +205 -0
- modal/_utils/rand_pb_testing.py +3 -3
- modal/_utils/shell_utils.py +15 -49
- modal/_vendor/a2wsgi_wsgi.py +62 -72
- modal/_vendor/cloudpickle.py +1 -1
- modal/_watcher.py +12 -10
- modal/app.py +561 -323
- modal/app.pyi +474 -262
- modal/call_graph.py +7 -6
- modal/cli/_download.py +22 -6
- modal/cli/_traceback.py +200 -0
- modal/cli/app.py +203 -42
- modal/cli/config.py +12 -5
- modal/cli/container.py +61 -13
- modal/cli/dict.py +128 -0
- modal/cli/entry_point.py +26 -13
- modal/cli/environment.py +40 -9
- modal/cli/import_refs.py +21 -48
- modal/cli/launch.py +28 -14
- modal/cli/network_file_system.py +57 -21
- modal/cli/profile.py +1 -1
- modal/cli/programs/run_jupyter.py +34 -9
- modal/cli/programs/vscode.py +58 -8
- modal/cli/queues.py +131 -0
- modal/cli/run.py +199 -96
- modal/cli/secret.py +5 -4
- modal/cli/token.py +7 -2
- modal/cli/utils.py +74 -8
- modal/cli/volume.py +97 -56
- modal/client.py +248 -144
- modal/client.pyi +156 -124
- modal/cloud_bucket_mount.py +43 -30
- modal/cloud_bucket_mount.pyi +32 -25
- modal/cls.py +528 -141
- modal/cls.pyi +189 -145
- modal/config.py +32 -15
- modal/container_process.py +177 -0
- modal/container_process.pyi +82 -0
- modal/dict.py +50 -54
- modal/dict.pyi +120 -164
- modal/environments.py +106 -5
- modal/environments.pyi +77 -25
- modal/exception.py +30 -43
- modal/experimental.py +62 -2
- modal/file_io.py +537 -0
- modal/file_io.pyi +235 -0
- modal/file_pattern_matcher.py +196 -0
- modal/functions.py +846 -428
- modal/functions.pyi +446 -387
- modal/gpu.py +57 -44
- modal/image.py +943 -417
- modal/image.pyi +584 -245
- modal/io_streams.py +434 -0
- modal/io_streams.pyi +122 -0
- modal/mount.py +223 -90
- modal/mount.pyi +241 -243
- modal/network_file_system.py +85 -86
- modal/network_file_system.pyi +151 -110
- modal/object.py +66 -36
- modal/object.pyi +166 -143
- modal/output.py +63 -0
- modal/parallel_map.py +73 -47
- modal/parallel_map.pyi +51 -63
- modal/partial_function.py +272 -107
- modal/partial_function.pyi +219 -120
- modal/proxy.py +15 -12
- modal/proxy.pyi +3 -8
- modal/queue.py +96 -72
- modal/queue.pyi +210 -135
- modal/requirements/2024.04.txt +2 -1
- modal/requirements/2024.10.txt +16 -0
- modal/requirements/README.md +21 -0
- modal/requirements/base-images.json +22 -0
- modal/retries.py +45 -4
- modal/runner.py +325 -203
- modal/runner.pyi +124 -110
- modal/running_app.py +27 -4
- modal/sandbox.py +509 -231
- modal/sandbox.pyi +396 -169
- modal/schedule.py +2 -2
- modal/scheduler_placement.py +20 -3
- modal/secret.py +41 -25
- modal/secret.pyi +62 -42
- modal/serving.py +39 -49
- modal/serving.pyi +37 -43
- modal/stream_type.py +15 -0
- modal/token_flow.py +5 -3
- modal/token_flow.pyi +37 -32
- modal/volume.py +123 -137
- modal/volume.pyi +228 -221
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/METADATA +5 -5
- modal-0.72.13.dist-info/RECORD +174 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/top_level.txt +0 -1
- modal_docs/gen_reference_docs.py +3 -1
- modal_docs/mdmd/mdmd.py +0 -1
- modal_docs/mdmd/signatures.py +1 -2
- modal_global_objects/images/base_images.py +28 -0
- modal_global_objects/mounts/python_standalone.py +2 -2
- modal_proto/__init__.py +1 -1
- modal_proto/api.proto +1231 -531
- modal_proto/api_grpc.py +750 -430
- modal_proto/api_pb2.py +2102 -1176
- modal_proto/api_pb2.pyi +8859 -0
- modal_proto/api_pb2_grpc.py +1329 -675
- modal_proto/api_pb2_grpc.pyi +1416 -0
- modal_proto/modal_api_grpc.py +149 -0
- modal_proto/modal_options_grpc.py +3 -0
- modal_proto/options_pb2.pyi +20 -0
- modal_proto/options_pb2_grpc.pyi +7 -0
- modal_proto/py.typed +0 -0
- modal_version/__init__.py +1 -1
- modal_version/_version_generated.py +2 -2
- modal/_asgi.py +0 -370
- modal/_container_exec.py +0 -128
- modal/_container_io_manager.py +0 -646
- modal/_container_io_manager.pyi +0 -412
- modal/_sandbox_shell.py +0 -49
- modal/app_utils.py +0 -20
- modal/app_utils.pyi +0 -17
- modal/execution_context.pyi +0 -37
- modal/shared_volume.py +0 -23
- modal/shared_volume.pyi +0 -24
- modal-0.62.115.dist-info/RECORD +0 -207
- modal_global_objects/images/conda.py +0 -15
- modal_global_objects/images/debian_slim.py +0 -15
- modal_global_objects/images/micromamba.py +0 -15
- test/__init__.py +0 -1
- test/aio_test.py +0 -12
- test/async_utils_test.py +0 -279
- test/blob_test.py +0 -67
- test/cli_imports_test.py +0 -149
- test/cli_test.py +0 -674
- test/client_test.py +0 -203
- test/cloud_bucket_mount_test.py +0 -22
- test/cls_test.py +0 -636
- test/config_test.py +0 -149
- test/conftest.py +0 -1485
- test/container_app_test.py +0 -50
- test/container_test.py +0 -1405
- test/cpu_test.py +0 -23
- test/decorator_test.py +0 -85
- test/deprecation_test.py +0 -34
- test/dict_test.py +0 -51
- test/e2e_test.py +0 -68
- test/error_test.py +0 -7
- test/function_serialization_test.py +0 -32
- test/function_test.py +0 -791
- test/function_utils_test.py +0 -101
- test/gpu_test.py +0 -159
- test/grpc_utils_test.py +0 -82
- test/helpers.py +0 -47
- test/image_test.py +0 -814
- test/live_reload_test.py +0 -80
- test/lookup_test.py +0 -70
- test/mdmd_test.py +0 -329
- test/mount_test.py +0 -162
- test/mounted_files_test.py +0 -327
- test/network_file_system_test.py +0 -188
- test/notebook_test.py +0 -66
- test/object_test.py +0 -41
- test/package_utils_test.py +0 -25
- test/queue_test.py +0 -115
- test/resolver_test.py +0 -59
- test/retries_test.py +0 -67
- test/runner_test.py +0 -85
- test/sandbox_test.py +0 -191
- test/schedule_test.py +0 -15
- test/scheduler_placement_test.py +0 -57
- test/secret_test.py +0 -89
- test/serialization_test.py +0 -50
- test/stub_composition_test.py +0 -10
- test/stub_test.py +0 -361
- test/test_asgi_wrapper.py +0 -234
- test/token_flow_test.py +0 -18
- test/traceback_test.py +0 -135
- test/tunnel_test.py +0 -29
- test/utils_test.py +0 -88
- test/version_test.py +0 -14
- test/volume_test.py +0 -397
- test/watcher_test.py +0 -58
- test/webhook_test.py +0 -145
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/LICENSE +0 -0
- {modal-0.62.115.dist-info → modal-0.72.13.dist-info}/WHEEL +0 -0
- {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)
|