Skip to content

Commit f18f13b

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

File tree

7 files changed

+339
-4
lines changed

7 files changed

+339
-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.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: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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+
display = f"[{config.code}]({config.category}) - {message}"
62+
63+
if config.verbosity == WarningVerbosity.MEDIUM:
64+
display = f"{module_name}:{line_number} {display}"
65+
66+
if config.verbosity == WarningVerbosity.HIGH:
67+
display = f"{display}\n{config.description}"
68+
69+
return display
70+
71+
def _log(
72+
self,
73+
warning_code: str,
74+
message: str,
75+
level: LoggingLevels,
76+
**kwargs: dict[str, Any],
77+
) -> None:
78+
warning_config = self._warning_configs.get(warning_code)
79+
80+
# resolves the module and line number of the warning call.
81+
module_name, line_number = self._resolve_call_details()
82+
83+
if not warning_config:
84+
# If no config is available just follow default behaviour:
85+
logger.warning(f"{module_name}:{line_number} - {message}")
86+
return
87+
88+
if warning_config.is_throttled:
89+
if warning_code in self._warning_statistics:
90+
# Throttled code has already appeared - skip.
91+
return
92+
93+
display_message = self._get_display_message(
94+
message=message,
95+
module_name=module_name,
96+
line_number=line_number,
97+
config=warning_config,
98+
)
99+
100+
self._warning_statistics[warning_code] += 1
101+
102+
if level == LoggingLevels.INFO:
103+
logger.info(display_message.format(**kwargs))
104+
else:
105+
# Assumes warning level is the default if an invalid option is passed.
106+
logger.warning(display_message.format(**kwargs))
107+
108+
def warn(
109+
self, *, warning_code: str, message: str, **kwargs: dict[str, Any]
110+
) -> None:
111+
"""Method to execute warning handling logic with warning log level.
112+
113+
Args:
114+
warning_code: The code of the warning (see WarningCodes enum)
115+
message: The message to display.
116+
**kwargs: Keyword arguments for message formatting (make sure keys correspond to message placeholders).
117+
"""
118+
self._log(warning_code, message, LoggingLevels.WARNING, **kwargs)
119+
120+
def info(
121+
self, *, warning_code: str, message: str, **kwargs: dict[str, Any]
122+
) -> None:
123+
"""Method to execute warning handling logic with info log level.
124+
125+
Args:
126+
warning_code: The code of the warning (see WarningCodes enum)
127+
message: The message to display.
128+
**kwargs: Keyword arguments for message formatting (make sure keys correspond to message placeholders).
129+
"""
130+
self._log(warning_code, message, LoggingLevels.INFO, **kwargs)
131+
132+
@staticmethod
133+
def create() -> "WarningController":
134+
"""Factory function for WarningController.
135+
136+
Creates a new warning controller and registers system warning configs.
137+
138+
Returns:
139+
A warning controller instance.
140+
"""
141+
from zenml.utils.warnings.registry import WARNING_CONFIG_REGISTRY
142+
143+
registry = WarningController.create()
144+
145+
registry.register(warning_configs=WARNING_CONFIG_REGISTRY)
146+
147+
return registry
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 warning configurations organization and resolution."""
15+
16+
from enum import Enum
17+
18+
from zenml.utils.warnings.base import (
19+
WarningCategory,
20+
WarningConfig,
21+
WarningSeverity,
22+
WarningVerbosity,
23+
)
24+
25+
26+
class WarningCodes(str, Enum):
27+
"""Enum class organizing the warning codes."""
28+
29+
ZML001 = "ZML001"
30+
ZML002 = "ZML002"
31+
32+
33+
WARNING_CONFIG_REGISTRY = {
34+
WarningCodes.ZML001.value: WarningConfig(
35+
code=WarningCodes.ZML001.value,
36+
category=WarningCategory.USAGE,
37+
description="""
38+
## Ignored settings warning
39+
40+
The user has provided a settings object without a valid key.
41+
""",
42+
verbosity=WarningVerbosity.LOW,
43+
severity=WarningSeverity.MEDIUM,
44+
is_throttled=False,
45+
),
46+
WarningCodes.ZML002.value: WarningConfig(
47+
code=WarningCodes.ZML002.value,
48+
category=WarningCategory.USAGE,
49+
description="""
50+
## Unused docker settings warning
51+
52+
The user provides a valid docker settings object but the stack
53+
does not have a containerised orchestrator and will not make use of it.
54+
""",
55+
verbosity=WarningVerbosity.LOW,
56+
severity=WarningSeverity.MEDIUM,
57+
is_throttled=True,
58+
),
59+
}

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)