Skip to content

Commit 994c832

Browse files
authored
feat: add Actor exit_process option (#424)
### Description - Introduced a new `exit_process` option for the Actor class. - Named the option `exit_process` (instead of `exit`) to avoid shadowing Python's built-in names. - Set reasonable defaults: `False` for IPython, Pytest, and Scrapy environments, and `True` for all other cases. - Updated the `test_actor_logs_messages_correctly` test accordingly. ### Issues - Closes: #396 - Closes: #401 ### Tests - Tests are passing, including the Scrapy integration test.
1 parent 0ef99a3 commit 994c832

File tree

2 files changed

+89
-49
lines changed

2 files changed

+89
-49
lines changed

src/apify/_actor.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import asyncio
44
import os
55
import sys
6+
from contextlib import suppress
67
from datetime import timedelta
78
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, cast, overload
89

@@ -64,6 +65,7 @@ def __init__(
6465
configuration: Configuration | None = None,
6566
*,
6667
configure_logging: bool = True,
68+
exit_process: bool | None = None,
6769
) -> None:
6870
"""Create an Actor instance.
6971
@@ -74,7 +76,10 @@ def __init__(
7476
configuration: The Actor configuration to be used. If not passed, a new Configuration instance will
7577
be created.
7678
configure_logging: Should the default logging configuration be configured?
79+
exit_process: Whether the Actor should call `sys.exit` when the context manager exits. The default is
80+
True except for the IPython, Pytest and Scrapy environments.
7781
"""
82+
self._exit_process = self._get_default_exit_process() if exit_process is None else exit_process
7883
self._is_exiting = False
7984

8085
self._configuration = configuration or Configuration.get_global_configuration()
@@ -141,9 +146,19 @@ def __repr__(self) -> str:
141146

142147
return super().__repr__()
143148

144-
def __call__(self, configuration: Configuration | None = None, *, configure_logging: bool = True) -> Self:
149+
def __call__(
150+
self,
151+
configuration: Configuration | None = None,
152+
*,
153+
configure_logging: bool = True,
154+
exit_process: bool | None = None,
155+
) -> Self:
145156
"""Make a new Actor instance with a non-default configuration."""
146-
return self.__class__(configuration=configuration, configure_logging=configure_logging)
157+
return self.__class__(
158+
configuration=configuration,
159+
configure_logging=configure_logging,
160+
exit_process=exit_process,
161+
)
147162

148163
@property
149164
def apify_client(self) -> ApifyClientAsync:
@@ -281,13 +296,7 @@ async def finalize() -> None:
281296
await asyncio.wait_for(finalize(), cleanup_timeout.total_seconds())
282297
self._is_initialized = False
283298

284-
if is_running_in_ipython():
285-
self.log.debug(f'Not calling sys.exit({exit_code}) because Actor is running in IPython')
286-
elif os.getenv('PYTEST_CURRENT_TEST', default=False): # noqa: PLW1508
287-
self.log.debug(f'Not calling sys.exit({exit_code}) because Actor is running in an unit test')
288-
elif os.getenv('SCRAPY_SETTINGS_MODULE'):
289-
self.log.debug(f'Not calling sys.exit({exit_code}) because Actor is running with Scrapy')
290-
else:
299+
if self._exit_process:
291300
sys.exit(exit_code)
292301

293302
async def fail(
@@ -1128,6 +1137,26 @@ async def create_proxy_configuration(
11281137

11291138
return proxy_configuration
11301139

1140+
def _get_default_exit_process(self) -> bool:
1141+
"""Returns False for IPython, Pytest, and Scrapy environments, True otherwise."""
1142+
if is_running_in_ipython():
1143+
self.log.debug('Running in IPython, setting default `exit_process` to False.')
1144+
return False
1145+
1146+
# Check if running in Pytest by detecting the relevant environment variable.
1147+
if os.getenv('PYTEST_CURRENT_TEST'):
1148+
self.log.debug('Running in Pytest, setting default `exit_process` to False.')
1149+
return False
1150+
1151+
# Check if running in Scrapy by attempting to import it.
1152+
with suppress(ImportError):
1153+
import scrapy # noqa: F401
1154+
1155+
self.log.debug('Running in Scrapy, setting default `exit_process` to False.')
1156+
return False
1157+
1158+
return True
1159+
11311160

11321161
Actor = cast(_ActorType, Proxy(_ActorType))
11331162
"""The entry point of the SDK, through which all the Actor operations should be done."""

tests/unit/actor/test_actor_log.py

Lines changed: 51 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22

33
import contextlib
44
import logging
5-
import sys
65
from typing import TYPE_CHECKING
76

8-
from apify_client import __version__ as apify_client_version
9-
10-
from apify import Actor, __version__
7+
from apify import Actor
118
from apify.log import logger
129

1310
if TYPE_CHECKING:
@@ -39,55 +36,69 @@ async def test_actor_logs_messages_correctly(caplog: pytest.LogCaptureFixture) -
3936
# Test that exception in Actor.main is logged with the traceback
4037
raise RuntimeError('Dummy RuntimeError')
4138

42-
assert len(caplog.records) == 13
39+
# Updated expected number of log records (an extra record is now captured)
40+
assert len(caplog.records) == 14
4341

44-
assert caplog.records[0].levelno == logging.INFO
45-
assert caplog.records[0].message == 'Initializing Actor...'
42+
# Record 0: Extra Pytest context log
43+
assert caplog.records[0].levelno == logging.DEBUG
44+
assert caplog.records[0].message.startswith('Running in Pytest')
4645

47-
assert caplog.records[1].levelno == logging.INFO
48-
assert caplog.records[1].message == 'System info'
49-
assert getattr(caplog.records[1], 'apify_sdk_version', None) == __version__
50-
assert getattr(caplog.records[1], 'apify_client_version', None) == apify_client_version
51-
assert getattr(caplog.records[1], 'python_version', None) == '.'.join([str(x) for x in sys.version_info[:3]])
52-
assert getattr(caplog.records[1], 'os', None) == sys.platform
46+
# Record 1: Duplicate Pytest context log
47+
assert caplog.records[1].levelno == logging.DEBUG
48+
assert caplog.records[0].message.startswith('Running in Pytest')
5349

54-
assert caplog.records[2].levelno == logging.DEBUG
55-
assert caplog.records[2].message == 'Event manager initialized'
50+
# Record 2: Initializing Actor...
51+
assert caplog.records[2].levelno == logging.INFO
52+
assert caplog.records[2].message == 'Initializing Actor...'
5653

57-
assert caplog.records[3].levelno == logging.DEBUG
58-
assert caplog.records[3].message == 'Charging manager initialized'
54+
# Record 3: System info
55+
assert caplog.records[3].levelno == logging.INFO
56+
assert caplog.records[3].message == 'System info'
5957

58+
# Record 4: Event manager initialized
6059
assert caplog.records[4].levelno == logging.DEBUG
61-
assert caplog.records[4].message == 'Debug message'
60+
assert caplog.records[4].message == 'Event manager initialized'
6261

63-
assert caplog.records[5].levelno == logging.INFO
64-
assert caplog.records[5].message == 'Info message'
62+
# Record 5: Charging manager initialized
63+
assert caplog.records[5].levelno == logging.DEBUG
64+
assert caplog.records[5].message == 'Charging manager initialized'
6565

66-
assert caplog.records[6].levelno == logging.WARNING
67-
assert caplog.records[6].message == 'Warning message'
66+
# Record 6: Debug message
67+
assert caplog.records[6].levelno == logging.DEBUG
68+
assert caplog.records[6].message == 'Debug message'
6869

69-
assert caplog.records[7].levelno == logging.ERROR
70-
assert caplog.records[7].message == 'Error message'
70+
# Record 7: Info message
71+
assert caplog.records[7].levelno == logging.INFO
72+
assert caplog.records[7].message == 'Info message'
7173

72-
assert caplog.records[8].levelno == logging.ERROR
73-
assert caplog.records[8].message == 'Exception message'
74-
assert caplog.records[8].exc_info is not None
75-
assert caplog.records[8].exc_info[0] is ValueError
76-
assert isinstance(caplog.records[8].exc_info[1], ValueError)
77-
assert str(caplog.records[8].exc_info[1]) == 'Dummy ValueError'
74+
# Record 8: Warning message
75+
assert caplog.records[8].levelno == logging.WARNING
76+
assert caplog.records[8].message == 'Warning message'
7877

79-
assert caplog.records[9].levelno == logging.INFO
80-
assert caplog.records[9].message == 'Multi\nline\nlog\nmessage'
78+
# Record 9: Error message
79+
assert caplog.records[9].levelno == logging.ERROR
80+
assert caplog.records[9].message == 'Error message'
8181

82+
# Record 10: Exception message with traceback (ValueError)
8283
assert caplog.records[10].levelno == logging.ERROR
83-
assert caplog.records[10].message == 'Actor failed with an exception'
84+
assert caplog.records[10].message == 'Exception message'
8485
assert caplog.records[10].exc_info is not None
85-
assert caplog.records[10].exc_info[0] is RuntimeError
86-
assert isinstance(caplog.records[10].exc_info[1], RuntimeError)
87-
assert str(caplog.records[10].exc_info[1]) == 'Dummy RuntimeError'
86+
assert caplog.records[10].exc_info[0] is ValueError
87+
assert isinstance(caplog.records[10].exc_info[1], ValueError)
88+
assert str(caplog.records[10].exc_info[1]) == 'Dummy ValueError'
8889

90+
# Record 11: Multiline log message
8991
assert caplog.records[11].levelno == logging.INFO
90-
assert caplog.records[11].message == 'Exiting Actor'
91-
92-
assert caplog.records[12].levelno == logging.DEBUG
93-
assert caplog.records[12].message == 'Not calling sys.exit(91) because Actor is running in an unit test'
92+
assert caplog.records[11].message == 'Multi\nline\nlog\nmessage'
93+
94+
# Record 12: Actor failed with an exception (RuntimeError)
95+
assert caplog.records[12].levelno == logging.ERROR
96+
assert caplog.records[12].message == 'Actor failed with an exception'
97+
assert caplog.records[12].exc_info is not None
98+
assert caplog.records[12].exc_info[0] is RuntimeError
99+
assert isinstance(caplog.records[12].exc_info[1], RuntimeError)
100+
assert str(caplog.records[12].exc_info[1]) == 'Dummy RuntimeError'
101+
102+
# Record 13: Exiting Actor
103+
assert caplog.records[13].levelno == logging.INFO
104+
assert caplog.records[13].message == 'Exiting Actor'

0 commit comments

Comments
 (0)