Skip to content

Commit acc1871

Browse files
Feature:4005 Docker settings usage warnings
- Warn when used without containerized orchestrator in place
1 parent 8e14213 commit acc1871

File tree

7 files changed

+366
-4
lines changed

7 files changed

+366
-4
lines changed

src/zenml/config/compiler.py

Lines changed: 50 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,46 @@ def _verify_upstream_steps(
369371
f"steps in this pipeline: {available_steps}."
370372
)
371373

374+
@staticmethod
375+
def _validate_docker_settings_usage(
376+
stack: "Stack",
377+
docker_settings: "BaseSettings | None" = None,
378+
) -> None:
379+
"""Validates that docker settings are used with a proper stack.
380+
381+
Generates warning for improper docker settings usage or returns.
382+
383+
Args:
384+
docker_settings: The docker settings specified for the step/pipeline.
385+
stack: The stack the settings are validated against.
386+
387+
"""
388+
from zenml.orchestrators import (
389+
ContainerizedOrchestrator,
390+
LocalOrchestrator,
391+
)
392+
393+
if not docker_settings or isinstance(
394+
stack.orchestrator, ContainerizedOrchestrator
395+
):
396+
return
397+
398+
warning_message = (
399+
"You are specifying docker settings but you are not using a"
400+
f"containerized orchestrator: {stack.orchestrator.__class__.__name__}."
401+
f"Consider switching stack or using a containerized orchestrator, otherwise"
402+
f"your docker settings will be ignored."
403+
)
404+
405+
if isinstance(stack.orchestrator, LocalOrchestrator):
406+
WARNING_CONTROLLER.info(
407+
warning_code=WarningCodes.ZML002, message=warning_message
408+
)
409+
else:
410+
WARNING_CONTROLLER.warn(
411+
warning_code=WarningCodes.ZML002, message=warning_message
412+
)
413+
372414
def _filter_and_validate_settings(
373415
self,
374416
settings: Dict[str, "BaseSettings"],
@@ -397,9 +439,9 @@ def _filter_and_validate_settings(
397439
try:
398440
settings_instance = resolver.resolve(stack=stack)
399441
except KeyError:
400-
logger.info(
401-
"Not including stack component settings with key `%s`.",
402-
key,
442+
WARNING_CONTROLLER.info(
443+
warning_code=WarningCodes.ZML001.value,
444+
message=f"Not including stack component settings with key {key}",
403445
)
404446
continue
405447

@@ -429,6 +471,11 @@ def _filter_and_validate_settings(
429471

430472
validated_settings[key] = settings_instance
431473

474+
self._validate_docker_settings_usage(
475+
stack=stack,
476+
docker_settings=settings.get(DOCKER_SETTINGS_KEY),
477+
)
478+
432479
return validated_settings
433480

434481
def _get_step_spec(

src/zenml/logger.py

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

362+
# logging capture warnings
363+
logging.captureWarnings(True)
364+
362365
if not has_console_handler:
363366
console_handler = logging.StreamHandler(sys.stdout)
364367
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.create()
24+
25+
__all__ = [
26+
"WARNING_CONTROLLER",
27+
"WarningCodes"
28+
]

src/zenml/utils/warnings/base.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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+
"""Warning configuration class and helper enums."""
15+
16+
from enum import Enum
17+
18+
from pydantic import BaseModel, Field
19+
20+
21+
class WarningSeverity(str, Enum):
22+
"""Enum class describing the warning severity."""
23+
24+
LOW = "low"
25+
MEDIUM = "medium"
26+
HIGH = "high"
27+
28+
29+
class WarningCategory(str, Enum):
30+
"""Enum class describing the warning category."""
31+
32+
USAGE = "USAGE"
33+
34+
35+
class WarningVerbosity(str, Enum):
36+
"""Enum class describing the warning verbosity."""
37+
38+
LOW = "low"
39+
MEDIUM = "medium"
40+
HIGH = "high"
41+
42+
43+
class WarningConfig(BaseModel, use_enum_values=True):
44+
"""Warning config class describing how warning messages should be displayed."""
45+
46+
description: str = Field(description="Description of the warning message")
47+
severity: WarningSeverity = WarningSeverity.MEDIUM
48+
category: WarningCategory
49+
code: str
50+
is_throttled: bool = Field(
51+
description="If the warning is throttled. Throttled warnings should be displayed once per session.",
52+
default=False,
53+
)
54+
verbosity: WarningVerbosity = Field(
55+
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.",
56+
default=WarningVerbosity.MEDIUM,
57+
)
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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+
"""Module for centralized WarningController implementation."""
15+
16+
import logging
17+
from collections import defaultdict
18+
from typing import Any
19+
20+
from zenml.enums import LoggingLevels
21+
from zenml.utils.singleton import SingletonMetaClass
22+
from zenml.utils.warnings.base import WarningConfig, WarningVerbosity
23+
24+
logger = logging.getLogger(__name__)
25+
26+
27+
class WarningController(metaclass=SingletonMetaClass):
28+
"""Class responsible for centralized handling of warning messages."""
29+
30+
def __init__(self) -> None:
31+
"""WarningController constructor."""
32+
self._warning_configs: dict[str, WarningConfig] = {}
33+
self._warning_statistics: dict[str, int] = defaultdict(int)
34+
35+
def register(self, warning_configs: dict[str, WarningConfig]) -> None:
36+
"""Register a warning config collection to the controller.
37+
38+
Args:
39+
warning_configs: Configs to be registered. Key should be the warning code.
40+
"""
41+
self._warning_configs.update(warning_configs)
42+
43+
@staticmethod
44+
def _resolve_call_details() -> tuple[str, int]:
45+
import inspect
46+
47+
frame = inspect.stack()[3] # public methods -> _log -> _resolve
48+
module = inspect.getmodule(frame[0])
49+
module_name = module.__name__ if module else "<unknown module>"
50+
line_number = frame.lineno
51+
52+
return module_name, line_number
53+
54+
@staticmethod
55+
def _get_display_message(
56+
message: str,
57+
module_name: str,
58+
line_number: int,
59+
config: WarningConfig,
60+
) -> str:
61+
"""Helper method to build the warning message string.
62+
63+
Args:
64+
message: The warning message.
65+
module_name: The module that the warning call originated from.
66+
line_number: The line number that the warning call originated from.
67+
config: The warning configuration.
68+
69+
Returns: A warning message containing extra fields/info based on warning config.
70+
"""
71+
display = f"[{config.code}]({config.category}) - {message}"
72+
73+
if config.verbosity == WarningVerbosity.MEDIUM:
74+
display = f"{module_name}:{line_number} {display}"
75+
76+
if config.verbosity == WarningVerbosity.HIGH:
77+
display = f"{display}\n{config.description}"
78+
79+
return display
80+
81+
def _log(
82+
self,
83+
warning_code: str,
84+
message: str,
85+
level: LoggingLevels,
86+
**kwargs: dict[str, Any],
87+
) -> None:
88+
"""Core function for warning handling.
89+
90+
Args:
91+
warning_code: The code of the warning configuration.
92+
message: The warning message.
93+
level: The level of the warning.
94+
**kwargs: Keyword arguments (for formatted messages).
95+
96+
"""
97+
warning_config = self._warning_configs.get(warning_code)
98+
99+
# resolves the module and line number of the warning call.
100+
module_name, line_number = self._resolve_call_details()
101+
102+
if not warning_config:
103+
# If no config is available just follow default behavior:
104+
logger.warning(f"{module_name}:{line_number} - {message}")
105+
return
106+
107+
if warning_config.is_throttled:
108+
if warning_code in self._warning_statistics:
109+
# Throttled code has already appeared - skip.
110+
return
111+
112+
display_message = self._get_display_message(
113+
message=message,
114+
module_name=module_name,
115+
line_number=line_number,
116+
config=warning_config,
117+
)
118+
119+
self._warning_statistics[warning_code] += 1
120+
121+
if level == LoggingLevels.INFO:
122+
logger.info(display_message.format(**kwargs))
123+
else:
124+
# Assumes warning level is the default if an invalid option is passed.
125+
logger.warning(display_message.format(**kwargs))
126+
127+
def warn(
128+
self, *, warning_code: str, message: str, **kwargs: dict[str, Any]
129+
) -> None:
130+
"""Method to execute warning handling logic with warning log level.
131+
132+
Args:
133+
warning_code: The code of the warning (see WarningCodes enum)
134+
message: The message to display.
135+
**kwargs: Keyword arguments (for formatted messages).
136+
"""
137+
self._log(warning_code, message, LoggingLevels.WARNING, **kwargs)
138+
139+
def info(
140+
self, *, warning_code: str, message: str, **kwargs: dict[str, Any]
141+
) -> None:
142+
"""Method to execute warning handling logic with info log level.
143+
144+
Args:
145+
warning_code: The code of the warning (see WarningCodes enum)
146+
message: The message to display.
147+
**kwargs: Keyword arguments (for formatted messages).
148+
"""
149+
self._log(warning_code, message, LoggingLevels.INFO, **kwargs)
150+
151+
@staticmethod
152+
def create() -> "WarningController":
153+
"""Factory function for WarningController.
154+
155+
Creates a new warning controller and registers system warning configs.
156+
157+
Returns:
158+
A warning controller instance.
159+
"""
160+
from zenml.utils.warnings.registry import WARNING_CONFIG_REGISTRY
161+
162+
registry = WarningController()
163+
164+
registry.register(warning_configs=WARNING_CONFIG_REGISTRY)
165+
166+
return registry

0 commit comments

Comments
 (0)