From 3e77ccaef20e991222c5146c82a5a13621c9fa0c Mon Sep 17 00:00:00 2001 From: Jan Buchar Date: Thu, 13 Feb 2025 16:49:03 +0100 Subject: [PATCH 1/5] fix: Make sure that Actor instances with non-default configurations are also accessible through the global Actor proxy after initialization --- src/apify/_actor.py | 3 +++ tests/unit/actor/test_actor_lifecycle.py | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index d675f1bd..412bfbff 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -200,6 +200,9 @@ async def init(self) -> None: if self._is_initialized: raise RuntimeError('The Actor was already initialized!') + # Make sure that the currently initialized instance is also available through the global `Actor` proxy + cast(Proxy, Actor).__wrapped__ = self + self._is_exiting = False self._was_final_persist_state_emitted = False diff --git a/tests/unit/actor/test_actor_lifecycle.py b/tests/unit/actor/test_actor_lifecycle.py index 33af45e6..0c2c5ecd 100644 --- a/tests/unit/actor/test_actor_lifecycle.py +++ b/tests/unit/actor/test_actor_lifecycle.py @@ -16,6 +16,7 @@ import apify._actor from apify import Actor from apify._actor import _ActorType +from apify._configuration import Configuration async def test_actor_properly_init_with_async() -> None: @@ -35,6 +36,13 @@ async def test_actor_init() -> None: assert my_actor._is_initialized is False +async def test_actor_global_works() -> None: + non_default_configuration = Configuration(actor_full_name='Actor McActorson, esq.') + + async with Actor(configuration=non_default_configuration): + assert Actor.configuration is non_default_configuration + + async def test_double_init_raises_error(prepare_test_env: Callable) -> None: async with Actor: assert Actor._is_initialized From b993088471819fe3c57c4a3b475473fef21628f2 Mon Sep 17 00:00:00 2001 From: Jan Buchar Date: Thu, 13 Feb 2025 16:56:55 +0100 Subject: [PATCH 2/5] Remove unnecessary variable declarations --- src/apify/_actor.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index 412bfbff..e095c6b1 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -55,9 +55,6 @@ class _ActorType: """The class of `Actor`. Only make a new instance if you're absolutely sure you need to.""" - _apify_client: ApifyClientAsync - _configuration: Configuration - _is_exiting = False _is_rebooting = False def __init__( @@ -76,6 +73,8 @@ def __init__( be created. configure_logging: Should the default logging configuration be configured? """ + self._is_exiting = False + self._configuration = configuration or Configuration.get_global_configuration() self._configure_logging = configure_logging self._apify_client = self.new_client() From 7509d412ca12b1c4af8c0103de2f5b557efbff01 Mon Sep 17 00:00:00 2001 From: Jan Buchar Date: Thu, 13 Feb 2025 17:08:48 +0100 Subject: [PATCH 3/5] Make _is_rebooting class-scoped --- src/apify/_actor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index e095c6b1..e22437f7 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -900,11 +900,11 @@ async def reboot( self.log.error('Actor.reboot() is only supported when running on the Apify platform.') return - if self._is_rebooting: + if _ActorType._is_rebooting: self.log.debug('Actor is already rebooting, skipping the additional reboot call.') return - self._is_rebooting = True + _ActorType._is_rebooting = True if not custom_after_sleep: custom_after_sleep = self._configuration.metamorph_after_sleep From 127f31b4bd96b1db4b9b0beff557bea9f5709f0b Mon Sep 17 00:00:00 2001 From: Jan Buchar Date: Thu, 13 Feb 2025 17:15:10 +0100 Subject: [PATCH 4/5] Move test --- tests/unit/actor/test_actor_lifecycle.py | 8 -------- tests/unit/actor/test_actor_non_default_instance.py | 7 +++++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/unit/actor/test_actor_lifecycle.py b/tests/unit/actor/test_actor_lifecycle.py index 0c2c5ecd..33af45e6 100644 --- a/tests/unit/actor/test_actor_lifecycle.py +++ b/tests/unit/actor/test_actor_lifecycle.py @@ -16,7 +16,6 @@ import apify._actor from apify import Actor from apify._actor import _ActorType -from apify._configuration import Configuration async def test_actor_properly_init_with_async() -> None: @@ -36,13 +35,6 @@ async def test_actor_init() -> None: assert my_actor._is_initialized is False -async def test_actor_global_works() -> None: - non_default_configuration = Configuration(actor_full_name='Actor McActorson, esq.') - - async with Actor(configuration=non_default_configuration): - assert Actor.configuration is non_default_configuration - - async def test_double_init_raises_error(prepare_test_env: Callable) -> None: async with Actor: assert Actor._is_initialized diff --git a/tests/unit/actor/test_actor_non_default_instance.py b/tests/unit/actor/test_actor_non_default_instance.py index 6a51be23..3b3b5ccd 100644 --- a/tests/unit/actor/test_actor_non_default_instance.py +++ b/tests/unit/actor/test_actor_non_default_instance.py @@ -10,3 +10,10 @@ async def test_actor_with_non_default_config() -> None: async with Actor(config) as actor: assert actor.config.internal_timeout == timedelta(minutes=111) + + +async def test_actor_global_works() -> None: + non_default_configuration = Configuration(actor_full_name='Actor McActorson, esq.') + + async with Actor(configuration=non_default_configuration): + assert Actor.configuration is non_default_configuration From 4cd537b13f1469f2c72142fe3cfbf5004d9e7fbd Mon Sep 17 00:00:00 2001 From: Jan Buchar Date: Thu, 13 Feb 2025 17:24:32 +0100 Subject: [PATCH 5/5] Print warning when multiple Actor inits take place --- src/apify/_actor.py | 5 +++++ tests/unit/conftest.py | 1 + 2 files changed, 6 insertions(+) diff --git a/src/apify/_actor.py b/src/apify/_actor.py index e22437f7..8c6969b0 100644 --- a/src/apify/_actor.py +++ b/src/apify/_actor.py @@ -56,6 +56,7 @@ class _ActorType: """The class of `Actor`. Only make a new instance if you're absolutely sure you need to.""" _is_rebooting = False + _is_any_instance_initialized = False def __init__( self, @@ -199,6 +200,9 @@ async def init(self) -> None: if self._is_initialized: raise RuntimeError('The Actor was already initialized!') + if _ActorType._is_any_instance_initialized: + self.log.warning('Repeated Actor initialization detected - this is non-standard usage, proceed with care') + # Make sure that the currently initialized instance is also available through the global `Actor` proxy cast(Proxy, Actor).__wrapped__ = self @@ -225,6 +229,7 @@ async def init(self) -> None: await self._event_manager.__aenter__() self._is_initialized = True + _ActorType._is_any_instance_initialized = True async def exit( self, diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 87a4dfb3..6f336cd6 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -39,6 +39,7 @@ def prepare_test_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Callabl def _prepare_test_env() -> None: delattr(apify._actor.Actor, '__wrapped__') + apify._actor._ActorType._is_any_instance_initialized = False # Set the environment variable for the local storage directory to the temporary path. monkeypatch.setenv(ApifyEnvVars.LOCAL_STORAGE_DIR, str(tmp_path))