Skip to content

Commit b609955

Browse files
authored
refactor: consolidate empty-string env-var validators in Configuration (#965)
## Description A few `Configuration` fields were already guarded against empty-string env vars with ad-hoc `BeforeValidator` lambdas. The Apify platform sometimes sets an env var to an empty string instead of leaving it unset (see #303, or this [Discord thread](https://discord.com/channels/801163717915574323/1020281045205123102/threads/1294774068528156817)), and for these fields `''` cannot be parsed into the target type, so without the guard validation would raise and crash `Actor.init()`. This PR is a pure refactor: it consolidates those repeated lambdas into a single shared `_default_if_empty(default=...)` validator. There is no behavior change, and the empty-string handling is **not** extended to any new fields. ### Changes - Added a shared `_default_if_empty(default=...)` validator. - Replaced the existing ad-hoc lambdas with it on the four fields that already handled `''`: - `max_paid_dataset_items` (→ `None`) - `max_total_charge_usd` (→ `None`) - `timeout_at` (→ `None`) - `user_is_paying` (→ `False`) - Added a parametrized regression test covering those four fields.
1 parent b5f026f commit b609955

2 files changed

Lines changed: 40 additions & 5 deletions

File tree

src/apify/_configuration.py

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from decimal import Decimal
66
from logging import getLogger
77
from pathlib import Path
8-
from typing import Annotated, Any, Self
8+
from typing import TYPE_CHECKING, Annotated, Any, Self
99

1010
from pydantic import AliasChoices, BeforeValidator, Field, model_validator
1111
from typing_extensions import TypedDict
@@ -23,6 +23,9 @@
2323
)
2424
from apify._utils import docs_group
2525

26+
if TYPE_CHECKING:
27+
from collections.abc import Callable
28+
2629
logger = getLogger(__name__)
2730

2831

@@ -34,6 +37,20 @@ def _transform_to_list(value: Any) -> list[str] | None:
3437
return value if isinstance(value, list) else str(value).split(',')
3538

3639

40+
def _default_if_empty(*, default: Any) -> Callable[[Any], Any]:
41+
"""Build a validator that substitutes `default` for an empty-string env var.
42+
43+
The Apify platform sometimes sets an env var to an empty string instead of leaving it unset. For fields whose
44+
target type cannot parse `''` (datetimes, numbers, booleans, ...), passing the value straight through would crash
45+
validation and, in turn, `Actor.init()`. Treat `''` as "not provided" and fall back to the field default instead.
46+
"""
47+
48+
def transform(value: Any) -> Any:
49+
return default if value == '' else value
50+
51+
return transform
52+
53+
3754
class ActorStorages(TypedDict):
3855
"""Mapping of storage aliases to their IDs, grouped by storage type.
3956
@@ -294,7 +311,7 @@ class Configuration(CrawleeConfiguration):
294311
alias='actor_max_paid_dataset_items',
295312
description='For paid-per-result Actors, the user-set limit on returned results. Do not exceed this limit',
296313
),
297-
BeforeValidator(lambda val: val if val != '' else None),
314+
BeforeValidator(_default_if_empty(default=None)),
298315
] = None
299316

300317
max_total_charge_usd: Annotated[
@@ -303,7 +320,7 @@ class Configuration(CrawleeConfiguration):
303320
alias='actor_max_total_charge_usd',
304321
description='For pay-per-event Actors, the user-set limit on total charges. Do not exceed this limit',
305322
),
306-
BeforeValidator(lambda val: val if val != '' else None),
323+
BeforeValidator(_default_if_empty(default=None)),
307324
] = None
308325

309326
test_pay_per_event: Annotated[
@@ -382,7 +399,7 @@ class Configuration(CrawleeConfiguration):
382399
),
383400
description='Date when the Actor will time out',
384401
),
385-
BeforeValidator(lambda val: val if val != '' else None), # We should accept empty environment variables as well
402+
BeforeValidator(_default_if_empty(default=None)),
386403
] = None
387404

388405
standby_url: Annotated[
@@ -416,7 +433,7 @@ class Configuration(CrawleeConfiguration):
416433
alias='apify_user_is_paying',
417434
description='True if the user calling the Actor is paying user',
418435
),
419-
BeforeValidator(lambda val: False if val == '' else val),
436+
BeforeValidator(_default_if_empty(default=False)),
420437
] = False
421438

422439
web_server_port: Annotated[

tests/unit/actor/test_configuration.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,3 +392,21 @@ def test_actor_storage_json_env_var(monkeypatch: pytest.MonkeyPatch) -> None:
392392
assert config.actor_storages['datasets'] == datasets
393393
assert config.actor_storages['request_queues'] == request_queues
394394
assert config.actor_storages['key_value_stores'] == key_value_stores
395+
396+
397+
@pytest.mark.parametrize(
398+
('env_var', 'attr', 'expected'),
399+
[
400+
('APIFY_TIMEOUT_AT', 'timeout_at', None),
401+
('ACTOR_MAX_PAID_DATASET_ITEMS', 'max_paid_dataset_items', None),
402+
('ACTOR_MAX_TOTAL_CHARGE_USD', 'max_total_charge_usd', None),
403+
('APIFY_USER_IS_PAYING', 'user_is_paying', False),
404+
],
405+
)
406+
def test_typed_env_var_empty_string_falls_back_to_default(
407+
monkeypatch: pytest.MonkeyPatch, env_var: str, attr: str, expected: object
408+
) -> None:
409+
"""Platform may set a typed env var to '' instead of leaving it unset; that must not crash `Actor.init()`."""
410+
monkeypatch.setenv(env_var, '')
411+
config = ApifyConfiguration()
412+
assert getattr(config, attr) == expected

0 commit comments

Comments
 (0)