Skip to content

Commit abe1f12

Browse files
Merge pull request #4011 from zenml-io/feature/4005-docker-settings-usage-warnings
Feature:4005 Docker settings usage warnings
2 parents b32d8aa + 58a9abd commit abe1f12

File tree

8 files changed

+599
-4
lines changed

8 files changed

+599
-4
lines changed

src/zenml/config/compiler.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from zenml.models import PipelineSnapshotBase
4444
from zenml.pipelines.run_utils import get_default_run_name
4545
from zenml.utils import pydantic_utils, secret_utils, settings_utils
46+
from zenml.utils.warnings import WARNING_CONTROLLER, WarningCodes
4647

4748
if TYPE_CHECKING:
4849
from zenml.pipelines.pipeline_definition import Pipeline
@@ -369,6 +370,37 @@ def _verify_upstream_steps(
369370
f"steps in this pipeline: {available_steps}."
370371
)
371372

373+
@staticmethod
374+
def _validate_docker_settings_usage(
375+
docker_settings: "BaseSettings | None",
376+
stack: "Stack",
377+
) -> None:
378+
"""Validates that docker settings are used with a proper stack.
379+
380+
Generates warning for improper docker settings usage or returns.
381+
382+
Args:
383+
docker_settings: The docker settings specified for the step/pipeline.
384+
stack: The stack the settings are validated against.
385+
386+
"""
387+
from zenml.orchestrators import LocalOrchestrator
388+
389+
if not docker_settings:
390+
return
391+
392+
warning_message = (
393+
"You are specifying docker settings but the orchestrator"
394+
" you are using (LocalOrchestrator) will not make use of them. "
395+
"Consider switching stacks, removing the settings, or using a "
396+
"different orchestrator."
397+
)
398+
399+
if isinstance(stack.orchestrator, LocalOrchestrator):
400+
WARNING_CONTROLLER.info(
401+
warning_code=WarningCodes.ZML002, message=warning_message
402+
)
403+
372404
def _filter_and_validate_settings(
373405
self,
374406
settings: Dict[str, "BaseSettings"],
@@ -390,16 +422,19 @@ def _filter_and_validate_settings(
390422
Returns:
391423
The filtered settings.
392424
"""
425+
from zenml.config.constants import DOCKER_SETTINGS_KEY
426+
393427
validated_settings = {}
394428

395429
for key, settings_instance in settings.items():
396430
resolver = SettingsResolver(key=key, settings=settings_instance)
397431
try:
398432
settings_instance = resolver.resolve(stack=stack)
399433
except KeyError:
400-
logger.info(
401-
"Not including stack component settings with key `%s`.",
402-
key,
434+
WARNING_CONTROLLER.info(
435+
warning_code=WarningCodes.ZML001,
436+
message="Not including stack component settings with key {key}",
437+
key=key,
403438
)
404439
continue
405440

@@ -429,6 +464,11 @@ def _filter_and_validate_settings(
429464

430465
validated_settings[key] = settings_instance
431466

467+
self._validate_docker_settings_usage(
468+
stack=stack,
469+
docker_settings=settings.get(DOCKER_SETTINGS_KEY),
470+
)
471+
432472
return validated_settings
433473

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

0 commit comments

Comments
 (0)