Skip to content

Commit 875e625

Browse files
Feature:4005 Docker settings usage warnings
- Warn when used without containerized orchestrator in place
1 parent 1bf2d9e commit 875e625

File tree

7 files changed

+245
-4
lines changed

7 files changed

+245
-4
lines changed

src/zenml/config/compiler.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
from zenml import __version__
3030
from zenml.config.base_settings import BaseSettings, ConfigurationLevel
31+
from zenml.config.constants import DOCKER_SETTINGS_KEY
3132
from zenml.config.pipeline_configurations import PipelineConfiguration
3233
from zenml.config.pipeline_run_configuration import PipelineRunConfiguration
3334
from zenml.config.pipeline_spec import OutputSpec, PipelineSpec
@@ -43,6 +44,7 @@
4344
from zenml.models import PipelineSnapshotBase
4445
from zenml.pipelines.run_utils import get_default_run_name
4546
from zenml.utils import pydantic_utils, secret_utils, settings_utils
47+
from zenml.utils.warnings import WARNING_CONTROLLER, WarningCodes
4648

4749
if TYPE_CHECKING:
4850
from zenml.pipelines.pipeline_definition import Pipeline
@@ -369,6 +371,37 @@ def _verify_upstream_steps(
369371
f"steps in this pipeline: {available_steps}."
370372
)
371373

374+
@staticmethod
375+
def _validate_docker_settings_usage(
376+
docker_settings: "BaseSettings",
377+
stack: "Stack",
378+
) -> None:
379+
from zenml.orchestrators import (
380+
ContainerizedOrchestrator,
381+
LocalOrchestrator,
382+
)
383+
384+
if not docker_settings or isinstance(
385+
stack.orchestrator, ContainerizedOrchestrator
386+
):
387+
return
388+
389+
warning_message = (
390+
"You are specifying docker settings but you are not using a"
391+
f"containerized orchestrator: {stack.orchestrator.__class__.__name__}."
392+
f"Consider switching stack or using a containerized orchestrator, otherwise"
393+
f"your docker settings will be ignored."
394+
)
395+
396+
if isinstance(stack.orchestrator, LocalOrchestrator):
397+
WARNING_CONTROLLER.info(
398+
warning_code=WarningCodes.ZML002, message=warning_message
399+
)
400+
else:
401+
WARNING_CONTROLLER.warn(
402+
warning_code=WarningCodes.ZML002, message=warning_message
403+
)
404+
372405
def _filter_and_validate_settings(
373406
self,
374407
settings: Dict[str, "BaseSettings"],
@@ -397,9 +430,10 @@ def _filter_and_validate_settings(
397430
try:
398431
settings_instance = resolver.resolve(stack=stack)
399432
except KeyError:
400-
logger.info(
401-
"Not including stack component settings with key `%s`.",
402-
key,
433+
WARNING_CONTROLLER.info(
434+
warning_code=WarningCodes.ZML001.value,
435+
message="Not including stack component settings with key {key}",
436+
key=key,
403437
)
404438
continue
405439

@@ -429,6 +463,11 @@ def _filter_and_validate_settings(
429463

430464
validated_settings[key] = settings_instance
431465

466+
self._validate_docker_settings_usage(
467+
stack=stack,
468+
docker_settings=settings.get(DOCKER_SETTINGS_KEY),
469+
)
470+
432471
return validated_settings
433472

434473
def _get_step_spec(

src/zenml/logger.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,9 @@ def init_logging() -> None:
357357
for handler in root_logger.handlers
358358
)
359359

360+
# logging capture warnings
361+
logging.captureWarnings(True)
362+
360363
if not has_console_handler:
361364
console_handler = logging.StreamHandler(sys.stdout)
362365
console_handler.setFormatter(get_formatter())
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Copyright (c) ZenML GmbH 2025. All Rights Reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at:
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12+
# or implied. See the License for the specific language governing
13+
# permissions and limitations under the License.
14+
"""Initialization of the warning utils module.
15+
16+
The `warnings` module contains utilities that help centralize
17+
and improve warning management.
18+
"""
19+
20+
from zenml.utils.warnings.controller import WarningController
21+
from zenml.utils.warnings.registry import WarningCodes
22+
23+
WARNING_CONTROLLER = WarningController()
24+
25+
__all__ = [
26+
"WARNING_CONTROLLER",
27+
"WarningCodes"
28+
]

src/zenml/utils/warnings/base.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from enum import Enum
2+
3+
from pydantic import BaseModel, Field
4+
5+
6+
class WarningSeverity(str, Enum):
7+
LOW = "low"
8+
MEDIUM = "medium"
9+
HIGH = "high"
10+
11+
12+
class WarningCategory(str, Enum):
13+
USAGE = "USAGE"
14+
15+
16+
class WarningVerbosity(str, Enum):
17+
LOW = "low"
18+
MEDIUM = "medium"
19+
HIGH = "high"
20+
21+
22+
class WarningConfig(BaseModel, use_enum_values=True):
23+
description: str = Field(description="Description of the warning message")
24+
severity: WarningSeverity = WarningSeverity.MEDIUM
25+
category: WarningCategory
26+
code: str
27+
is_throttled: bool = Field(
28+
description="If the warning is throttled. Throttled warnings should be displayed once per session.",
29+
default=False,
30+
)
31+
verbosity: WarningVerbosity = Field(
32+
description="Verbosity of the warning. Low verbosity displays basic details (code & message). Medium displays also call details like module & line number. High displays description and additional info.",
33+
default=WarningVerbosity.MEDIUM,
34+
)
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import logging
2+
from collections import defaultdict
3+
4+
from zenml.enums import LoggingLevels
5+
from zenml.utils.warnings.base import WarningConfig, WarningVerbosity
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
class WarningController:
11+
def __init__(self) -> None:
12+
self._warning_configs: dict[str, WarningConfig] = {}
13+
self._warning_statistics: dict[str, int] = defaultdict(int)
14+
15+
def register(self, warning_configs: dict[str, WarningConfig]) -> None:
16+
self._warning_configs.update(warning_configs)
17+
18+
@staticmethod
19+
def _resolve_call_details() -> tuple[str, int]:
20+
import inspect
21+
22+
frame = inspect.stack()[3] # public methods -> _log -> _resolve
23+
module = inspect.getmodule(frame[0])
24+
module_name = module.__name__ if module else "<unknown module>"
25+
line_number = frame.lineno
26+
27+
return module_name, line_number
28+
29+
@staticmethod
30+
def _get_display_message(
31+
message: str,
32+
module_name: str,
33+
line_number: int,
34+
config: WarningConfig,
35+
):
36+
display = f"[{config.code}]({config.category}) - {message}"
37+
38+
if config.verbosity == WarningVerbosity.MEDIUM:
39+
display = f"{module_name}:{line_number} {display}"
40+
41+
if config.verbosity == WarningVerbosity.HIGH:
42+
display = f"{display}\n{config.description}"
43+
44+
return display
45+
46+
def _log(
47+
self, warning_code: str, message: str, level: LoggingLevels, **kwargs
48+
) -> None:
49+
warning_config = self._warning_configs.get(warning_code)
50+
51+
# resolves the module and line number of the warning call.
52+
module_name, line_number = self._resolve_call_details()
53+
54+
if not warning_config:
55+
# If no config is available just follow default behaviour:
56+
logger.warning(f"{module_name}:{line_number} - {message}")
57+
return
58+
59+
if warning_config.is_throttled:
60+
if warning_code in self._warning_statistics:
61+
# Throttled code has already appeared - skip.
62+
return
63+
64+
display_message = self._get_display_message(
65+
message=message,
66+
module_name=module_name,
67+
line_number=line_number,
68+
config=warning_config,
69+
)
70+
71+
self._warning_statistics[warning_code] += 1
72+
73+
if level == LoggingLevels.INFO:
74+
logger.info(display_message.format(**kwargs))
75+
else:
76+
# Assumes warning level is the default if an invalid option is passed.
77+
logger.warning(display_message.format(**kwargs))
78+
79+
def warn(self, *, warning_code: str, message: str, **kwargs) -> None:
80+
self._log(warning_code, message, LoggingLevels.WARNING, **kwargs)
81+
82+
def info(self, *, warning_code: str, message: str, **kwargs) -> None:
83+
self._log(warning_code, message, LoggingLevels.INFO, **kwargs)
84+
85+
@staticmethod
86+
def create() -> "WarningController":
87+
from zenml.utils.warnings.registry import WARNING_CONFIG_REGISTRY
88+
89+
registry = WarningController()
90+
91+
registry.register(warning_configs=WARNING_CONFIG_REGISTRY)
92+
93+
return registry
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from enum import Enum
2+
3+
from zenml.utils.warnings.base import (
4+
WarningCategory,
5+
WarningConfig,
6+
WarningSeverity,
7+
WarningVerbosity,
8+
)
9+
10+
11+
class WarningCodes(str, Enum):
12+
ZML001 = "ZML001"
13+
ZML002 = "ZML002"
14+
15+
16+
WARNING_CONFIG_REGISTRY = {
17+
WarningCodes.ZML001.value: WarningConfig(
18+
code=WarningCodes.ZML001.value,
19+
category=WarningCategory.USAGE,
20+
description="""
21+
## Ignored settings warning
22+
23+
The user has provided a settings object without a valid key.
24+
""",
25+
verbosity=WarningVerbosity.LOW,
26+
severity=WarningSeverity.MEDIUM,
27+
is_throttled=False,
28+
),
29+
WarningCodes.ZML002.value: WarningConfig(
30+
code=WarningCodes.ZML002.value,
31+
category=WarningCategory.USAGE,
32+
description="""
33+
## Unused docker settings warning
34+
35+
The user provides a valid docker settings object but the stack
36+
does not have a containerised orchestrator and will not make use of it.
37+
""",
38+
verbosity=WarningVerbosity.LOW,
39+
severity=WarningSeverity.MEDIUM,
40+
is_throttled=True,
41+
),
42+
}

tests/unit/config/test_docker_settings.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
import pytest
1717
from pydantic import ValidationError
1818

19-
from zenml.config import DockerSettings
19+
from zenml.config.docker_settings import (
20+
DockerSettings,
21+
)
2022

2123

2224
def test_build_skipping():

0 commit comments

Comments
 (0)