modal 0.67.42__py3-none-any.whl → 0.68.4__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.
@@ -415,6 +415,9 @@ def main(container_args: api_pb2.ContainerArguments, client: Client):
415
415
 
416
416
  _client: _Client = synchronizer._translate_in(client) # TODO(erikbern): ugly
417
417
 
418
+ # Call ContainerHello - currently a noop but might be used later for things
419
+ container_io_manager.hello()
420
+
418
421
  with container_io_manager.heartbeats(is_snapshotting_function), UserCodeEventLoop() as event_loop:
419
422
  # If this is a serialized function, fetch the definition from the server
420
423
  if function_def.definition_type == api_pb2.Function.DEFINITION_TYPE_SERIALIZED:
@@ -335,6 +335,9 @@ class _ContainerIOManager:
335
335
  """Only used for tests."""
336
336
  cls._singleton = None
337
337
 
338
+ async def hello(self):
339
+ await self._client.stub.ContainerHello(Empty())
340
+
338
341
  async def _run_heartbeat_loop(self):
339
342
  while 1:
340
343
  t0 = time.monotonic()
modal/_traceback.py CHANGED
@@ -1,16 +1,21 @@
1
1
  # Copyright Modal Labs 2022
2
- """Helper functions related to operating on traceback objects.
2
+ """Helper functions related to operating on exceptions, warnings, and traceback objects.
3
3
 
4
4
  Functions related to *displaying* tracebacks should go in `modal/cli/_traceback.py`
5
5
  so that Rich is not a dependency of the container Client.
6
6
  """
7
+
7
8
  import re
8
9
  import sys
9
10
  import traceback
11
+ import warnings
10
12
  from types import TracebackType
11
- from typing import Any, Optional
13
+ from typing import Any, Iterable, Optional
14
+
15
+ from modal_proto import api_pb2
12
16
 
13
17
  from ._vendor.tblib import Traceback as TBLibTraceback
18
+ from .exception import ServerWarning
14
19
 
15
20
  TBDictType = dict[str, Any]
16
21
  LineCacheType = dict[tuple[str, str], str]
@@ -109,3 +114,12 @@ def print_exception(exc: Optional[type[BaseException]], value: Optional[BaseExce
109
114
  if sys.version_info < (3, 11) and value is not None:
110
115
  notes = getattr(value, "__notes__", [])
111
116
  print(*notes, sep="\n", file=sys.stderr)
117
+
118
+
119
+ def print_server_warnings(server_warnings: Iterable[api_pb2.Warning]):
120
+ """Issue a warning originating from the server with empty metadata about local origin.
121
+
122
+ When using the Modal CLI, these warnings should get caught and coerced into Rich panels.
123
+ """
124
+ for warning in server_warnings:
125
+ warnings.warn_explicit(warning.message, ServerWarning, "<modal-server>", 0)
modal/cli/_traceback.py CHANGED
@@ -1,5 +1,6 @@
1
1
  # Copyright Modal Labs 2024
2
2
  """Helper functions related to displaying tracebacks in the CLI."""
3
+
3
4
  import functools
4
5
  import re
5
6
  import warnings
@@ -11,7 +12,7 @@ from rich.syntax import Syntax
11
12
  from rich.text import Text
12
13
  from rich.traceback import PathHighlighter, Stack, Traceback, install
13
14
 
14
- from ..exception import DeprecationError, PendingDeprecationError
15
+ from ..exception import DeprecationError, PendingDeprecationError, ServerWarning
15
16
 
16
17
 
17
18
  @group()
@@ -165,7 +166,7 @@ def highlight_modal_deprecation_warnings() -> None:
165
166
  base_showwarning = warnings.showwarning
166
167
 
167
168
  def showwarning(warning, category, filename, lineno, file=None, line=None):
168
- if issubclass(category, (DeprecationError, PendingDeprecationError)):
169
+ if issubclass(category, (DeprecationError, PendingDeprecationError, ServerWarning)):
169
170
  content = str(warning)
170
171
  if re.match(r"^\d{4}-\d{2}-\d{2}", content):
171
172
  date = content[:10]
@@ -180,10 +181,16 @@ def highlight_modal_deprecation_warnings() -> None:
180
181
  except OSError:
181
182
  # e.g., when filename is "<unknown>"; raises FileNotFoundError on posix but OSError on windows
182
183
  pass
184
+ if issubclass(category, ServerWarning):
185
+ title = "Modal Warning"
186
+ else:
187
+ title = "Modal Deprecation Warning"
188
+ if date:
189
+ title += f" ({date})"
183
190
  panel = Panel(
184
191
  message,
185
- style="yellow",
186
- title=f"Modal Deprecation Warning ({date})" if date else "Modal Deprecation Warning",
192
+ border_style="yellow",
193
+ title=title,
187
194
  title_align="left",
188
195
  )
189
196
  Console().print(panel)
modal/cli/container.py CHANGED
@@ -1,5 +1,4 @@
1
1
  # Copyright Modal Labs 2022
2
-
3
2
  from typing import Optional, Union
4
3
 
5
4
  import typer
@@ -8,12 +7,13 @@ from rich.text import Text
8
7
  from modal._pty import get_pty_info
9
8
  from modal._utils.async_utils import synchronizer
10
9
  from modal._utils.grpc_utils import retry_transient_errors
11
- from modal.cli.utils import ENV_OPTION, display_table, stream_app_logs, timestamp_to_local
10
+ from modal.cli.utils import ENV_OPTION, display_table, is_tty, stream_app_logs, timestamp_to_local
12
11
  from modal.client import _Client
13
12
  from modal.config import config
14
13
  from modal.container_process import _ContainerProcess
15
14
  from modal.environments import ensure_env
16
15
  from modal.object import _get_environment_name
16
+ from modal.stream_type import StreamType
17
17
  from modal_proto import api_pb2
18
18
 
19
19
  container_cli = typer.Typer(name="container", help="Manage and connect to running containers.", no_args_is_help=True)
@@ -55,12 +55,19 @@ def logs(container_id: str = typer.Argument(help="Container ID")):
55
55
  @container_cli.command("exec")
56
56
  @synchronizer.create_blocking
57
57
  async def exec(
58
+ pty: Optional[bool] = typer.Option(default=None, help="Run the command using a PTY."),
58
59
  container_id: str = typer.Argument(help="Container ID"),
59
- command: list[str] = typer.Argument(help="A command to run inside the container."),
60
- pty: bool = typer.Option(default=True, help="Run the command using a PTY."),
60
+ command: list[str] = typer.Argument(
61
+ help="A command to run inside the container.\n\n"
62
+ "To pass command-line flags or options, add `--` before the start of your commands. "
63
+ "For example: `modal container exec <id> -- /bin/bash -c 'echo hi'`"
64
+ ),
61
65
  ):
62
66
  """Execute a command in a container."""
63
67
 
68
+ if pty is None:
69
+ pty = is_tty()
70
+
64
71
  client = await _Client.from_env()
65
72
 
66
73
  req = api_pb2.ContainerExecRequest(
@@ -71,7 +78,11 @@ async def exec(
71
78
  )
72
79
  res: api_pb2.ContainerExecResponse = await client.stub.ContainerExec(req)
73
80
 
74
- await _ContainerProcess(res.exec_id, client).attach(pty=pty)
81
+ if pty:
82
+ await _ContainerProcess(res.exec_id, client).attach()
83
+ else:
84
+ # TODO: redirect stderr to its own stream?
85
+ await _ContainerProcess(res.exec_id, client, stdout=StreamType.STDOUT, stderr=StreamType.STDOUT).wait()
75
86
 
76
87
 
77
88
  @container_cli.command("stop")
modal/cli/run.py CHANGED
@@ -13,8 +13,6 @@ from typing import Any, Callable, Optional, get_type_hints
13
13
 
14
14
  import click
15
15
  import typer
16
- from rich.console import Console
17
- from rich.panel import Panel
18
16
  from typing_extensions import TypedDict
19
17
 
20
18
  from .. import Cls
@@ -29,7 +27,7 @@ from ..runner import deploy_app, interactive_shell, run_app
29
27
  from ..serving import serve_app
30
28
  from ..volume import Volume
31
29
  from .import_refs import import_app, import_function
32
- from .utils import ENV_OPTION, ENV_OPTION_HELP, stream_app_logs
30
+ from .utils import ENV_OPTION, ENV_OPTION_HELP, is_tty, stream_app_logs
33
31
 
34
32
 
35
33
  class ParameterMetadata(TypedDict):
@@ -306,14 +304,7 @@ def deploy(
306
304
  if name is None:
307
305
  name = app.name
308
306
 
309
- with enable_output():
310
- res = deploy_app(app, name=name, environment_name=env or "", tag=tag)
311
- if res.warnings:
312
- console = Console()
313
- for warning in res.warnings:
314
- panel = Panel(warning, title="Warning", title_align="left", border_style="yellow")
315
- console.print(panel, highlight=False)
316
-
307
+ res = deploy_app(app, name=name, environment_name=env or "", tag=tag)
317
308
  if stream_logs:
318
309
  stream_app_logs(app_id=res.app_id, app_logs_url=res.app_logs_url)
319
310
 
@@ -392,40 +383,47 @@ def shell(
392
383
  "Can be a single region or a comma-separated list to choose from (if not using REF)."
393
384
  ),
394
385
  ),
386
+ pty: Optional[bool] = typer.Option(default=None, help="Run the command using a PTY."),
395
387
  ):
396
- """Run an interactive shell inside a Modal container.
388
+ """Run a command or interactive shell inside a Modal container.
397
389
 
398
- **Examples:**
390
+ \b**Examples:**
399
391
 
400
- Start a shell inside the default Debian-based image:
392
+ \bStart an interactive shell inside the default Debian-based image:
401
393
 
402
- ```
394
+ \b```
403
395
  modal shell
404
396
  ```
405
397
 
406
- Start a bash shell using the spec for `my_function` in your App:
398
+ \bStart an interactive shell with the spec for `my_function` in your App
399
+ (uses the same image, volumes, mounts, etc.):
407
400
 
408
- ```
401
+ \b```
409
402
  modal shell hello_world.py::my_function
410
403
  ```
411
404
 
412
- Or, if you're using a [modal.Cls](/docs/reference/modal.Cls), you can refer to a `@modal.method` directly:
405
+ \bOr, if you're using a [modal.Cls](/docs/reference/modal.Cls), you can refer to a `@modal.method` directly:
413
406
 
414
- ```
407
+ \b```
415
408
  modal shell hello_world.py::MyClass.my_method
416
409
  ```
417
410
 
418
411
  Start a `python` shell:
419
412
 
420
- ```
413
+ \b```
421
414
  modal shell hello_world.py --cmd=python
422
415
  ```
416
+
417
+ \bRun a command with your function's spec and pipe the output to a file:
418
+
419
+ \b```
420
+ modal shell hello_world.py -c 'uv pip list' > env.txt
421
+ ```
423
422
  """
424
423
  env = ensure_env(env)
425
424
 
426
- console = Console()
427
- if not console.is_terminal:
428
- raise click.UsageError("`modal shell` can only be run from a terminal.")
425
+ if pty is None:
426
+ pty = is_tty()
429
427
 
430
428
  if platform.system() == "Windows":
431
429
  raise InvalidError("`modal shell` is currently not supported on Windows")
@@ -441,7 +439,7 @@ def shell(
441
439
  ):
442
440
  from .container import exec
443
441
 
444
- exec(container_id=container_or_function, command=shlex.split(cmd), pty=True)
442
+ exec(container_id=container_or_function, command=shlex.split(cmd))
445
443
  return
446
444
 
447
445
  function = import_function(
@@ -461,6 +459,7 @@ def shell(
461
459
  memory=function_spec.memory,
462
460
  volumes=function_spec.volumes,
463
461
  region=function_spec.scheduler_placement.proto.regions if function_spec.scheduler_placement else None,
462
+ pty=pty,
464
463
  )
465
464
  else:
466
465
  modal_image = Image.from_registry(image, add_python=add_python) if image else None
@@ -474,6 +473,7 @@ def shell(
474
473
  cloud=cloud,
475
474
  volumes=volumes,
476
475
  region=region.split(",") if region else [],
476
+ pty=pty,
477
477
  )
478
478
 
479
479
  # NB: invoking under bash makes --cmd a lot more flexible.
modal/cli/utils.py CHANGED
@@ -77,6 +77,10 @@ def _plain(text: Union[Text, str]) -> str:
77
77
  return text.plain if isinstance(text, Text) else text
78
78
 
79
79
 
80
+ def is_tty() -> bool:
81
+ return Console().is_terminal
82
+
83
+
80
84
  def display_table(
81
85
  columns: Sequence[Union[Column, str]],
82
86
  rows: Sequence[Sequence[Union[Text, str]]],
modal/client.py CHANGED
@@ -16,23 +16,21 @@ from typing import (
16
16
  import grpclib.client
17
17
  from google.protobuf import empty_pb2
18
18
  from google.protobuf.message import Message
19
- from grpclib import GRPCError, Status
20
19
  from synchronicity.async_wrap import asynccontextmanager
21
20
 
22
21
  from modal._utils.async_utils import synchronizer
23
22
  from modal_proto import api_grpc, api_pb2, modal_api_grpc
24
23
  from modal_version import __version__
25
24
 
25
+ from ._traceback import print_server_warnings
26
26
  from ._utils import async_utils
27
27
  from ._utils.async_utils import TaskContext, synchronize_api
28
28
  from ._utils.grpc_utils import connect_channel, create_channel, retry_transient_errors
29
29
  from .config import _check_config, _is_remote, config, logger
30
- from .exception import AuthError, ClientClosed, ConnectionError, DeprecationError, VersionError
30
+ from .exception import AuthError, ClientClosed, ConnectionError
31
31
 
32
32
  HEARTBEAT_INTERVAL: float = config.get("heartbeat_interval")
33
33
  HEARTBEAT_TIMEOUT: float = HEARTBEAT_INTERVAL + 0.1
34
- CLIENT_CREATE_ATTEMPT_TIMEOUT: float = 4.0
35
- CLIENT_CREATE_TOTAL_TIMEOUT: float = 15.0
36
34
 
37
35
 
38
36
  def _get_metadata(client_type: int, credentials: Optional[tuple[str, str]], version: str) -> dict[str, str]:
@@ -137,32 +135,11 @@ class _Client:
137
135
  async def hello(self):
138
136
  """Connect to server and retrieve version information; raise appropriate error for various failures."""
139
137
  logger.debug(f"Client ({id(self)}): Starting")
140
- try:
141
- req = empty_pb2.Empty()
142
- resp = await retry_transient_errors(
143
- self.stub.ClientHello,
144
- req,
145
- attempt_timeout=CLIENT_CREATE_ATTEMPT_TIMEOUT,
146
- total_timeout=CLIENT_CREATE_TOTAL_TIMEOUT,
147
- )
148
- if resp.warning:
149
- ALARM_EMOJI = chr(0x1F6A8)
150
- warnings.warn_explicit(f"{ALARM_EMOJI} {resp.warning} {ALARM_EMOJI}", DeprecationError, "<unknown>", 0)
151
- except GRPCError as exc:
152
- if exc.status == Status.FAILED_PRECONDITION:
153
- raise VersionError(
154
- f"The client version ({self.version}) is too old. Please update (pip install --upgrade modal)."
155
- )
156
- else:
157
- raise exc
138
+ resp = await retry_transient_errors(self.stub.ClientHello, empty_pb2.Empty())
139
+ print_server_warnings(resp.server_warnings)
158
140
 
159
141
  async def __aenter__(self):
160
142
  await self._open()
161
- try:
162
- await self.hello()
163
- except BaseException:
164
- await self._close()
165
- raise
166
143
  return self
167
144
 
168
145
  async def __aexit__(self, exc_type, exc, tb):
@@ -178,7 +155,6 @@ class _Client:
178
155
  client = cls(server_url, api_pb2.CLIENT_TYPE_CLIENT, credentials=None)
179
156
  try:
180
157
  await client._open()
181
- # Skip client.hello
182
158
  yield client
183
159
  finally:
184
160
  await client._close()
@@ -229,7 +205,6 @@ class _Client:
229
205
  client = _Client(server_url, client_type, credentials)
230
206
  await client._open()
231
207
  async_utils.on_shutdown(client._close())
232
- await client.hello()
233
208
  cls._client_from_env = client
234
209
  return client
235
210
 
@@ -252,11 +227,6 @@ class _Client:
252
227
  credentials = (token_id, token_secret)
253
228
  client = _Client(server_url, client_type, credentials)
254
229
  await client._open()
255
- try:
256
- await client.hello()
257
- except BaseException:
258
- await client._close()
259
- raise
260
230
  async_utils.on_shutdown(client._close())
261
231
  return client
262
232
 
@@ -265,8 +235,8 @@ class _Client:
265
235
  """mdmd:hidden
266
236
  Check whether can the client can connect to this server with these credentials; raise if not.
267
237
  """
268
- async with cls(server_url, api_pb2.CLIENT_TYPE_CLIENT, credentials):
269
- pass # Will call ClientHello RPC and possibly raise AuthError or ConnectionError
238
+ async with cls(server_url, api_pb2.CLIENT_TYPE_CLIENT, credentials) as client:
239
+ client.hello() # Will call ClientHello RPC and possibly raise AuthError or ConnectionError
270
240
 
271
241
  @classmethod
272
242
  def set_env_client(cls, client: Optional["_Client"]):
@@ -316,7 +286,6 @@ class _Client:
316
286
  self.set_env_client(None)
317
287
  # TODO(elias): reset _cancellation_context in case ?
318
288
  await self._open()
319
- # intentionally not doing self.hello since we should already be authenticated etc.
320
289
 
321
290
  async def _get_grpclib_method(self, method_name: str) -> Any:
322
291
  # safely get grcplib method that is bound to a valid channel
modal/client.pyi CHANGED
@@ -26,7 +26,7 @@ class _Client:
26
26
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
27
27
 
28
28
  def __init__(
29
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.42"
29
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.4"
30
30
  ): ...
31
31
  def is_closed(self) -> bool: ...
32
32
  @property
@@ -81,7 +81,7 @@ class Client:
81
81
  _stub: typing.Optional[modal_proto.api_grpc.ModalClientStub]
82
82
 
83
83
  def __init__(
84
- self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.67.42"
84
+ self, server_url: str, client_type: int, credentials: typing.Optional[tuple[str, str]], version: str = "0.68.4"
85
85
  ): ...
86
86
  def is_closed(self) -> bool: ...
87
87
  @property
@@ -194,7 +194,3 @@ class UnaryStreamWrapper(typing.Generic[RequestType, ResponseType]):
194
194
  HEARTBEAT_INTERVAL: float
195
195
 
196
196
  HEARTBEAT_TIMEOUT: float
197
-
198
- CLIENT_CREATE_ATTEMPT_TIMEOUT: float
199
-
200
- CLIENT_CREATE_TOTAL_TIMEOUT: float
modal/cls.py CHANGED
@@ -14,15 +14,13 @@ from modal_proto import api_pb2
14
14
  from ._resolver import Resolver
15
15
  from ._resources import convert_fn_config_to_resources_config
16
16
  from ._serialization import check_valid_cls_constructor_arg
17
+ from ._traceback import print_server_warnings
17
18
  from ._utils.async_utils import synchronize_api, synchronizer
18
19
  from ._utils.grpc_utils import retry_transient_errors
19
20
  from ._utils.mount_utils import validate_volumes
20
21
  from .client import _Client
21
22
  from .exception import InvalidError, NotFoundError, VersionError
22
- from .functions import (
23
- _Function,
24
- _parse_retries,
25
- )
23
+ from .functions import _Function, _parse_retries
26
24
  from .gpu import GPU_T
27
25
  from .object import _get_environment_name, _Object
28
26
  from .partial_function import (
@@ -486,6 +484,8 @@ class _Cls(_Object, type_prefix="cs"):
486
484
  else:
487
485
  raise
488
486
 
487
+ print_server_warnings(response.server_warnings)
488
+
489
489
  class_function_tag = f"{tag}.*" # special name of the base service function for the class
490
490
 
491
491
  class_service_function = _Function.from_name(
@@ -9,7 +9,7 @@ from ._utils.async_utils import TaskContext, synchronize_api
9
9
  from ._utils.grpc_utils import retry_transient_errors
10
10
  from ._utils.shell_utils import stream_from_stdin, write_to_fd
11
11
  from .client import _Client
12
- from .exception import InteractiveTimeoutError, InvalidError
12
+ from .exception import InteractiveTimeoutError, InvalidError, deprecation_error
13
13
  from .io_streams import _StreamReader, _StreamWriter
14
14
  from .stream_type import StreamType
15
15
 
@@ -114,11 +114,18 @@ class _ContainerProcess(Generic[T]):
114
114
  self._returncode = resp.exit_code
115
115
  return self._returncode
116
116
 
117
- async def attach(self, *, pty: bool):
117
+ async def attach(self, *, pty: Optional[bool] = None):
118
118
  if platform.system() == "Windows":
119
119
  print("interactive exec is not currently supported on Windows.")
120
120
  return
121
121
 
122
+ if pty is not None:
123
+ deprecation_error(
124
+ (2024, 12, 9),
125
+ "The `pty` argument to `modal.container_process.attach(pty=...)` is deprecated, "
126
+ "as only PTY mode is supported. Please remove the argument.",
127
+ )
128
+
122
129
  from rich.console import Console
123
130
 
124
131
  console = Console()
@@ -151,7 +158,7 @@ class _ContainerProcess(Generic[T]):
151
158
  # time out if we can't connect to the server fast enough
152
159
  await asyncio.wait_for(on_connect.wait(), timeout=60)
153
160
 
154
- async with stream_from_stdin(_handle_input, use_raw_terminal=pty):
161
+ async with stream_from_stdin(_handle_input, use_raw_terminal=True):
155
162
  await stdout_task
156
163
  await stderr_task
157
164
 
@@ -34,7 +34,7 @@ class _ContainerProcess(typing.Generic[T]):
34
34
  def returncode(self) -> int: ...
35
35
  async def poll(self) -> typing.Optional[int]: ...
36
36
  async def wait(self) -> int: ...
37
- async def attach(self, *, pty: bool): ...
37
+ async def attach(self, *, pty: typing.Optional[bool] = None): ...
38
38
 
39
39
  class ContainerProcess(typing.Generic[T]):
40
40
  _process_id: typing.Optional[str]
@@ -76,7 +76,7 @@ class ContainerProcess(typing.Generic[T]):
76
76
  wait: __wait_spec
77
77
 
78
78
  class __attach_spec(typing_extensions.Protocol):
79
- def __call__(self, *, pty: bool): ...
80
- async def aio(self, *, pty: bool): ...
79
+ def __call__(self, *, pty: typing.Optional[bool] = None): ...
80
+ async def aio(self, *, pty: typing.Optional[bool] = None): ...
81
81
 
82
82
  attach: __attach_spec
modal/exception.py CHANGED
@@ -4,6 +4,9 @@ import signal
4
4
  import sys
5
5
  import warnings
6
6
  from datetime import date
7
+ from typing import Iterable
8
+
9
+ from modal_proto import api_pb2
7
10
 
8
11
 
9
12
  class Error(Exception):
@@ -107,6 +110,10 @@ class PendingDeprecationError(UserWarning):
107
110
  """Soon to be deprecated feature. Only used intermittently because of multi-repo concerns."""
108
111
 
109
112
 
113
+ class ServerWarning(UserWarning):
114
+ """Warning originating from the Modal server and re-issued in client code."""
115
+
116
+
110
117
  class _CliUserExecutionError(Exception):
111
118
  """mdmd:hidden
112
119
  Private wrapper for exceptions during when importing or running stubs from the CLI.
@@ -213,3 +220,16 @@ class ModuleNotMountable(Exception):
213
220
 
214
221
  class ClientClosed(Error):
215
222
  pass
223
+
224
+
225
+ class FilesystemExecutionError(Error):
226
+ """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)