Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 38 additions & 9 deletions src/apify/_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import asyncio
import os
import sys
from contextlib import suppress
from datetime import timedelta
from typing import TYPE_CHECKING, Any, Callable, Literal, TypeVar, cast, overload

Expand Down Expand Up @@ -64,6 +65,7 @@ def __init__(
configuration: Configuration | None = None,
*,
configure_logging: bool = True,
exit_process: bool | None = None,
) -> None:
"""Create an Actor instance.

Expand All @@ -74,7 +76,10 @@ def __init__(
configuration: The Actor configuration to be used. If not passed, a new Configuration instance will
be created.
configure_logging: Should the default logging configuration be configured?
exit_process: Whether the Actor should call `sys.exit` when the context manager exits. The default is
True except for the IPython, Pytest and Scrapy environments.
"""
self._exit_process = self._get_default_exit_process() if exit_process is None else exit_process
self._is_exiting = False

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

return super().__repr__()

def __call__(self, configuration: Configuration | None = None, *, configure_logging: bool = True) -> Self:
def __call__(
self,
configuration: Configuration | None = None,
*,
configure_logging: bool = True,
exit_process: bool | None = None,
) -> Self:
"""Make a new Actor instance with a non-default configuration."""
return self.__class__(configuration=configuration, configure_logging=configure_logging)
return self.__class__(
configuration=configuration,
configure_logging=configure_logging,
exit_process=exit_process,
)

@property
def apify_client(self) -> ApifyClientAsync:
Expand Down Expand Up @@ -281,13 +296,7 @@ async def finalize() -> None:
await asyncio.wait_for(finalize(), cleanup_timeout.total_seconds())
self._is_initialized = False

if is_running_in_ipython():
self.log.debug(f'Not calling sys.exit({exit_code}) because Actor is running in IPython')
elif os.getenv('PYTEST_CURRENT_TEST', default=False): # noqa: PLW1508
self.log.debug(f'Not calling sys.exit({exit_code}) because Actor is running in an unit test')
elif os.getenv('SCRAPY_SETTINGS_MODULE'):
self.log.debug(f'Not calling sys.exit({exit_code}) because Actor is running with Scrapy')
else:
if self._exit_process:
sys.exit(exit_code)

async def fail(
Expand Down Expand Up @@ -1128,6 +1137,26 @@ async def create_proxy_configuration(

return proxy_configuration

def _get_default_exit_process(self) -> bool:
"""Returns False for IPython, Pytest, and Scrapy environments, True otherwise."""
if is_running_in_ipython():
self.log.debug('Running in IPython, setting default `exit_process` to False.')
return False

# Check if running in Pytest by detecting the relevant environment variable.
if os.getenv('PYTEST_CURRENT_TEST'):
self.log.debug('Running in Pytest, setting default `exit_process` to False.')
return False

# Check if running in Scrapy by attempting to import it.
with suppress(ImportError):
import scrapy # noqa: F401

self.log.debug('Running in Scrapy, setting default `exit_process` to False.')
return False

return True


Actor = cast(_ActorType, Proxy(_ActorType))
"""The entry point of the SDK, through which all the Actor operations should be done."""
91 changes: 51 additions & 40 deletions tests/unit/actor/test_actor_log.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,9 @@

import contextlib
import logging
import sys
from typing import TYPE_CHECKING

from apify_client import __version__ as apify_client_version

from apify import Actor, __version__
from apify import Actor
from apify.log import logger

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

assert len(caplog.records) == 13
# Updated expected number of log records (an extra record is now captured)
assert len(caplog.records) == 14

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

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

assert caplog.records[2].levelno == logging.DEBUG
assert caplog.records[2].message == 'Event manager initialized'
# Record 2: Initializing Actor...
assert caplog.records[2].levelno == logging.INFO
assert caplog.records[2].message == 'Initializing Actor...'

assert caplog.records[3].levelno == logging.DEBUG
assert caplog.records[3].message == 'Charging manager initialized'
# Record 3: System info
assert caplog.records[3].levelno == logging.INFO
assert caplog.records[3].message == 'System info'

# Record 4: Event manager initialized
assert caplog.records[4].levelno == logging.DEBUG
assert caplog.records[4].message == 'Debug message'
assert caplog.records[4].message == 'Event manager initialized'

assert caplog.records[5].levelno == logging.INFO
assert caplog.records[5].message == 'Info message'
# Record 5: Charging manager initialized
assert caplog.records[5].levelno == logging.DEBUG
assert caplog.records[5].message == 'Charging manager initialized'

assert caplog.records[6].levelno == logging.WARNING
assert caplog.records[6].message == 'Warning message'
# Record 6: Debug message
assert caplog.records[6].levelno == logging.DEBUG
assert caplog.records[6].message == 'Debug message'

assert caplog.records[7].levelno == logging.ERROR
assert caplog.records[7].message == 'Error message'
# Record 7: Info message
assert caplog.records[7].levelno == logging.INFO
assert caplog.records[7].message == 'Info message'

assert caplog.records[8].levelno == logging.ERROR
assert caplog.records[8].message == 'Exception message'
assert caplog.records[8].exc_info is not None
assert caplog.records[8].exc_info[0] is ValueError
assert isinstance(caplog.records[8].exc_info[1], ValueError)
assert str(caplog.records[8].exc_info[1]) == 'Dummy ValueError'
# Record 8: Warning message
assert caplog.records[8].levelno == logging.WARNING
assert caplog.records[8].message == 'Warning message'

assert caplog.records[9].levelno == logging.INFO
assert caplog.records[9].message == 'Multi\nline\nlog\nmessage'
# Record 9: Error message
assert caplog.records[9].levelno == logging.ERROR
assert caplog.records[9].message == 'Error message'

# Record 10: Exception message with traceback (ValueError)
assert caplog.records[10].levelno == logging.ERROR
assert caplog.records[10].message == 'Actor failed with an exception'
assert caplog.records[10].message == 'Exception message'
assert caplog.records[10].exc_info is not None
assert caplog.records[10].exc_info[0] is RuntimeError
assert isinstance(caplog.records[10].exc_info[1], RuntimeError)
assert str(caplog.records[10].exc_info[1]) == 'Dummy RuntimeError'
assert caplog.records[10].exc_info[0] is ValueError
assert isinstance(caplog.records[10].exc_info[1], ValueError)
assert str(caplog.records[10].exc_info[1]) == 'Dummy ValueError'

# Record 11: Multiline log message
assert caplog.records[11].levelno == logging.INFO
assert caplog.records[11].message == 'Exiting Actor'

assert caplog.records[12].levelno == logging.DEBUG
assert caplog.records[12].message == 'Not calling sys.exit(91) because Actor is running in an unit test'
assert caplog.records[11].message == 'Multi\nline\nlog\nmessage'

# Record 12: Actor failed with an exception (RuntimeError)
assert caplog.records[12].levelno == logging.ERROR
assert caplog.records[12].message == 'Actor failed with an exception'
assert caplog.records[12].exc_info is not None
assert caplog.records[12].exc_info[0] is RuntimeError
assert isinstance(caplog.records[12].exc_info[1], RuntimeError)
assert str(caplog.records[12].exc_info[1]) == 'Dummy RuntimeError'

# Record 13: Exiting Actor
assert caplog.records[13].levelno == logging.INFO
assert caplog.records[13].message == 'Exiting Actor'