From 8e97e241968240be89f9095d06ece225be1c5f55 Mon Sep 17 00:00:00 2001 From: Jinni Gu Date: Fri, 3 Apr 2026 01:09:38 -0700 Subject: [PATCH] feat: honor ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS Closes #5114 --- src/google/adk/a2a/experimental.py | 5 +- src/google/adk/features/_feature_registry.py | 7 + src/google/adk/utils/env_utils.py | 43 ++++ src/google/adk/utils/feature_decorator.py | 32 +-- .../a2a/integration/test_client_server.py | 222 ++++++++++++++++++ tests/unittests/a2a/test_experimental.py | 62 +++++ .../features/test_feature_decorator.py | 32 ++- .../features/test_feature_registry.py | 49 +++- tests/unittests/utils/test_env_utils.py | 37 +++ 9 files changed, 472 insertions(+), 17 deletions(-) create mode 100644 tests/unittests/a2a/test_experimental.py diff --git a/src/google/adk/a2a/experimental.py b/src/google/adk/a2a/experimental.py index dadc3791d1..b5e237a18a 100644 --- a/src/google/adk/a2a/experimental.py +++ b/src/google/adk/a2a/experimental.py @@ -27,7 +27,10 @@ "themselves not experimental. Once it's stable enough the experimental " "mode will be removed. Your feedback is welcome." ), - bypass_env_var="ADK_SUPPRESS_A2A_EXPERIMENTAL_FEATURE_WARNINGS", + bypass_env_var=( + "ADK_SUPPRESS_A2A_EXPERIMENTAL_FEATURE_WARNINGS", + "ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS", + ), ) """Mark a class or function as experimental A2A feature. diff --git a/src/google/adk/features/_feature_registry.py b/src/google/adk/features/_feature_registry.py index 4ae26a98ab..7776c4c1c4 100644 --- a/src/google/adk/features/_feature_registry.py +++ b/src/google/adk/features/_feature_registry.py @@ -20,6 +20,7 @@ from typing import Generator import warnings +from ..utils.env_utils import is_experimental_warning_suppressed from ..utils.env_utils import is_env_enabled @@ -300,6 +301,12 @@ def _emit_non_stable_warning_once( feature_name: The feature name. feature_stage: The feature stage. """ + if ( + feature_stage == FeatureStage.EXPERIMENTAL + and is_experimental_warning_suppressed() + ): + return + if feature_name not in _WARNED_FEATURES: _WARNED_FEATURES.add(feature_name) full_message = ( diff --git a/src/google/adk/utils/env_utils.py b/src/google/adk/utils/env_utils.py index 802a7a30c4..c09b5a3007 100644 --- a/src/google/adk/utils/env_utils.py +++ b/src/google/adk/utils/env_utils.py @@ -22,6 +22,11 @@ import os +_EXTENDED_TRUTHY_ENV_VALUES = frozenset({'1', 'true', 'yes', 'on'}) +_EXPERIMENTAL_WARNING_SUPPRESSION_ENV_VAR = ( + 'ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS' +) + def is_env_enabled(env_var_name: str, default: str = '0') -> bool: """Check if an environment variable is enabled. @@ -57,3 +62,41 @@ def is_env_enabled(env_var_name: str, default: str = '0') -> bool: True """ return os.environ.get(env_var_name, default).lower() in ['true', '1'] + + +def is_env_truthy(env_var_name: str) -> bool: + """Check if an environment variable uses ADK's permissive truthy values. + + This helper preserves the historical parsing used for experimental warning + suppression and accepts ``1``, ``true``, ``yes``, and ``on``. + + Args: + env_var_name: The name of the environment variable to check. + + Returns: + True if the environment variable is set to a truthy value, False otherwise. + """ + value = os.environ.get(env_var_name) + if value is None: + return False + return value.strip().lower() in _EXTENDED_TRUTHY_ENV_VALUES + + +def is_experimental_warning_suppressed( + *additional_env_var_names: str, +) -> bool: + """Check whether experimental warnings should be suppressed. + + Args: + *additional_env_var_names: Optional warning-specific env vars that should + also suppress experimental warnings. + + Returns: + True if the general experimental warning suppression env var, or any + provided additional env var, is truthy. + """ + env_var_names = ( + _EXPERIMENTAL_WARNING_SUPPRESSION_ENV_VAR, + *additional_env_var_names, + ) + return any(is_env_truthy(env_var_name) for env_var_name in env_var_names) diff --git a/src/google/adk/utils/feature_decorator.py b/src/google/adk/utils/feature_decorator.py index 7dbbc3bd99..82ef08bdd3 100644 --- a/src/google/adk/utils/feature_decorator.py +++ b/src/google/adk/utils/feature_decorator.py @@ -16,21 +16,26 @@ from collections.abc import Callable import functools -import os from typing import Any from typing import cast -from typing import Optional from typing import TypeVar import warnings +from .env_utils import is_env_truthy + T = TypeVar("T") -def _is_truthy_env(var_name: str) -> bool: - value = os.environ.get(var_name) - if value is None: +def _should_bypass_warning( + bypass_env_var: str | tuple[str, ...] | None, +) -> bool: + if bypass_env_var is None: return False - return value.strip().lower() in ("1", "true", "yes", "on") + + env_var_names = ( + (bypass_env_var,) if isinstance(bypass_env_var, str) else bypass_env_var + ) + return any(is_env_truthy(env_var_name) for env_var_name in env_var_names) def _make_feature_decorator( @@ -38,7 +43,7 @@ def _make_feature_decorator( label: str, default_message: str, block_usage: bool = False, - bypass_env_var: Optional[str] = None, + bypass_env_var: str | tuple[str, ...] | None = None, ) -> Callable[..., Any]: def decorator_factory(message_or_obj: Any = None) -> Any: # Case 1: Used as @decorator without parentheses @@ -61,7 +66,10 @@ def decorator_factory(message_or_obj: Any = None) -> Any: def _create_decorator( - message: str, label: str, block_usage: bool, bypass_env_var: Optional[str] + message: str, + label: str, + block_usage: bool, + bypass_env_var: str | tuple[str, ...] | None, ) -> Callable[[T], T]: def decorator(obj: T) -> T: obj_name = getattr(obj, "__name__", type(obj).__name__) @@ -74,9 +82,7 @@ def decorator(obj: T) -> T: @functools.wraps(orig_init) def new_init(self: Any, *args: Any, **kwargs: Any) -> Any: # Check if usage should be bypassed via environment variable at call time - should_bypass = bypass_env_var is not None and _is_truthy_env( - bypass_env_var - ) + should_bypass = _should_bypass_warning(bypass_env_var) if should_bypass: # Bypass completely - no warning, no error @@ -96,9 +102,7 @@ def new_init(self: Any, *args: Any, **kwargs: Any) -> Any: @functools.wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: # Check if usage should be bypassed via environment variable at call time - should_bypass = bypass_env_var is not None and _is_truthy_env( - bypass_env_var - ) + should_bypass = _should_bypass_warning(bypass_env_var) if should_bypass: # Bypass completely - no warning, no error diff --git a/tests/unittests/a2a/integration/test_client_server.py b/tests/unittests/a2a/integration/test_client_server.py index 3318efb84e..683ec50f11 100644 --- a/tests/unittests/a2a/integration/test_client_server.py +++ b/tests/unittests/a2a/integration/test_client_server.py @@ -14,17 +14,30 @@ """Integration tests for A2A client-server interaction.""" +import json +from pathlib import Path +import textwrap +import warnings + +from a2a.client.client import ClientConfig as A2AClientConfig +from a2a.client.client_factory import ClientFactory as A2AClientFactory +from a2a.types import AgentCapabilities +from a2a.types import AgentCard from a2a.types import Message as A2AMessage from a2a.types import Part as A2APart from a2a.types import Task from a2a.types import TaskState from a2a.types import TextPart +from a2a.types import TransportProtocol as A2ATransport from google.adk.agents.remote_a2a_agent import A2A_METADATA_PREFIX +from google.adk.agents.remote_a2a_agent import RemoteA2aAgent +from google.adk.cli.fast_api import get_fast_api_app from google.adk.events.event import Event from google.adk.platform import uuid as platform_uuid from google.adk.runners import Runner from google.adk.sessions.in_memory_session_service import InMemorySessionService from google.genai import types +import httpx import pytest from .client import create_a2a_client @@ -32,6 +45,215 @@ from .server import create_server_app +def _write_fast_api_a2a_agent( + agents_dir: Path, *, app_name: str, response_text: str +) -> AgentCard: + """Writes a minimal A2A-enabled agent package for FastAPI tests.""" + agent_dir = agents_dir / app_name + agent_dir.mkdir(parents=True) + + (agent_dir / "__init__.py").write_text( + "from . import agent\n", + encoding="utf-8", + ) + (agent_dir / "agent.py").write_text( + textwrap.dedent( + f"""\ + # Copyright 2026 Google LLC + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. + + from __future__ import annotations + + from typing import AsyncGenerator + + from google.adk.agents.base_agent import BaseAgent + from google.adk.events.event import Event + from google.genai import types + + + class TestA2AAgent(BaseAgent): + + async def _run_async_impl( + self, ctx + ) -> AsyncGenerator[Event, None]: + yield Event( + invocation_id=ctx.invocation_id, + author=self.name, + content=types.Content( + role="model", + parts=[types.Part(text={response_text!r})], + ), + ) + + + root_agent = TestA2AAgent(name={app_name!r}) + """ + ), + encoding="utf-8", + ) + + agent_card = AgentCard( + name=app_name, + url=f"http://test/a2a/{app_name}", + description="Test A2A agent", + capabilities=AgentCapabilities( + streaming=True, + extensions=[ + {"uri": "https://a2a-adk/a2a-extension/new-integration"} + ], + ), + version="0.0.1", + default_input_modes=["text/plain"], + default_output_modes=["text/plain"], + skills=[], + ) + (agent_dir / "agent.json").write_text( + json.dumps(agent_card.model_dump(by_alias=True, exclude_none=True)), + encoding="utf-8", + ) + return agent_card + + +def _create_fast_api_remote_client( + app, agent_card: AgentCard +) -> tuple[RemoteA2aAgent, httpx.AsyncClient]: + """Creates a RemoteA2aAgent backed by an in-process FastAPI app.""" + httpx_client = httpx.AsyncClient( + transport=httpx.ASGITransport(app=app), + base_url="http://test", + ) + client_config = A2AClientConfig( + httpx_client=httpx_client, + streaming=False, + polling=False, + supported_transports=[A2ATransport.jsonrpc], + ) + factory = A2AClientFactory(config=client_config) + agent = RemoteA2aAgent( + name="remote_agent", + agent_card=agent_card, + a2a_client_factory=factory, + use_legacy=False, + ) + return agent, httpx_client + + +async def _run_fast_api_a2a_round_trip( + *, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + app_name: str, + response_text: str, + suppress_warnings: bool, +) -> tuple[list[str], list[str]]: + """Runs a real A2A request through get_fast_api_app().""" + agents_dir = tmp_path / f"{app_name}_workspace" + agents_dir.mkdir() + agent_card = _write_fast_api_a2a_agent( + agents_dir, + app_name=app_name, + response_text=response_text, + ) + + monkeypatch.delenv( + "ADK_SUPPRESS_A2A_EXPERIMENTAL_FEATURE_WARNINGS", raising=False + ) + if suppress_warnings: + monkeypatch.setenv("ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS", "1") + else: + monkeypatch.delenv( + "ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS", raising=False + ) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + app = get_fast_api_app(agents_dir=str(agents_dir), web=True, a2a=True) + agent, httpx_client = _create_fast_api_remote_client(app, agent_card) + try: + session_service = InMemorySessionService() + await session_service.create_session( + app_name="ClientApp", + user_id="test_user", + session_id="test_session", + ) + client_runner = Runner( + app_name="ClientApp", + agent=agent, + session_service=session_service, + ) + new_message = types.Content( + parts=[types.Part(text="Hi")], role="user" + ) + + texts = [] + async for event in client_runner.run_async( + user_id="test_user", + session_id="test_session", + new_message=new_message, + ): + if event.content and event.content.parts: + for part in event.content.parts: + if part.text: + texts.append(part.text) + finally: + await httpx_client.aclose() + + experimental_warnings = [ + str(warning.message) + for warning in caught + if "[EXPERIMENTAL]" in str(warning.message) + ] + return texts, experimental_warnings + + +@pytest.mark.asyncio +async def test_fast_api_a2a_request_warns_by_default(tmp_path, monkeypatch): + """A real FastAPI A2A request still emits the request-converter warning.""" + response_text = "Hello from unsuppressed FastAPI A2A" + texts, experimental_warnings = await _run_fast_api_a2a_round_trip( + tmp_path=tmp_path, + monkeypatch=monkeypatch, + app_name="unsuppressed_a2a_agent", + response_text=response_text, + suppress_warnings=False, + ) + + assert texts == [response_text] + assert any( + "convert_a2a_request_to_agent_run_request" in warning + for warning in experimental_warnings + ) + + +@pytest.mark.asyncio +async def test_fast_api_a2a_request_suppresses_experimental_warnings( + tmp_path, monkeypatch +): + """The general suppression env var silences the full FastAPI A2A path.""" + response_text = "Hello from suppressed FastAPI A2A" + texts, experimental_warnings = await _run_fast_api_a2a_round_trip( + tmp_path=tmp_path, + monkeypatch=monkeypatch, + app_name="suppressed_a2a_agent", + response_text=response_text, + suppress_warnings=True, + ) + + assert texts == [response_text] + assert not experimental_warnings + + def create_streaming_mock_run_async(received_requests: list): """Creates a mock_run_async that streams multiple chunks.""" diff --git a/tests/unittests/a2a/test_experimental.py b/tests/unittests/a2a/test_experimental.py new file mode 100644 index 0000000000..b5dc114256 --- /dev/null +++ b/tests/unittests/a2a/test_experimental.py @@ -0,0 +1,62 @@ +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import warnings + +from google.adk.a2a.experimental import a2a_experimental +import pytest + + +@a2a_experimental +class A2aExperimentalClass: + + def run(self): + return "running" + + +@pytest.fixture(autouse=True) +def clear_suppression_env_vars(monkeypatch): + monkeypatch.delenv("ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS", raising=False) + monkeypatch.delenv( + "ADK_SUPPRESS_A2A_EXPERIMENTAL_FEATURE_WARNINGS", raising=False + ) + + +def test_a2a_experimental_class_warns_by_default(): + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + feature = A2aExperimentalClass() + assert feature.run() == "running" + assert len(w) == 1 + assert "[EXPERIMENTAL] A2aExperimentalClass:" in str(w[0].message) + + +def test_a2a_experimental_warning_suppressed_by_general_env_var(monkeypatch): + monkeypatch.setenv("ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS", "yes") + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + feature = A2aExperimentalClass() + assert feature.run() == "running" + assert not w + + +def test_a2a_experimental_warning_suppressed_by_a2a_env_var(monkeypatch): + monkeypatch.setenv("ADK_SUPPRESS_A2A_EXPERIMENTAL_FEATURE_WARNINGS", "on") + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + feature = A2aExperimentalClass() + assert feature.run() == "running" + assert not w diff --git a/tests/unittests/features/test_feature_decorator.py b/tests/unittests/features/test_feature_decorator.py index 5405bf3eca..fb2cfc399f 100644 --- a/tests/unittests/features/test_feature_decorator.py +++ b/tests/unittests/features/test_feature_decorator.py @@ -68,7 +68,11 @@ def reset_env_and_registry(monkeypatch): """Reset environment variables and registry before each test.""" # Clean up environment variables for key in list(os.environ.keys()): - if key.startswith("ADK_ENABLE_") or key.startswith("ADK_DISABLE_"): + if ( + key.startswith("ADK_ENABLE_") + or key.startswith("ADK_DISABLE_") + or key == "ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS" + ): monkeypatch.delenv(key, raising=False) # Add an existing feature to the registry @@ -168,6 +172,20 @@ def test_disabled_experimental_class_bypass_with_env_var(monkeypatch): ) +def test_disabled_experimental_class_suppresses_warning_with_general_env_var( + monkeypatch, +): + """General suppression env var silences decorator-backed class warnings.""" + monkeypatch.setenv("ADK_ENABLE_EXPERIMENTAL_CLASS", "true") + monkeypatch.setenv("ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS", "yes") + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + feature = ExperimentalClass() + assert feature.run() == "running" + assert not w + + def test_enabled_experimental_function_does_not_raise_error(): """Test that enabled experimental function does not raise error.""" @@ -179,6 +197,18 @@ def test_enabled_experimental_function_does_not_raise_error(): ) +def test_enabled_experimental_function_suppresses_warning_with_general_env_var( + monkeypatch, +): + """General suppression env var silences decorator-backed function warnings.""" + monkeypatch.setenv("ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS", "on") + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + assert experimental_function() == "executing" + assert not w + + def test_enabled_experimental_function_disabled_by_env_var(monkeypatch): """Test that enabled experimental function can be disabled by env var.""" diff --git a/tests/unittests/features/test_feature_registry.py b/tests/unittests/features/test_feature_registry.py index 1dad00c903..3b83fae84f 100644 --- a/tests/unittests/features/test_feature_registry.py +++ b/tests/unittests/features/test_feature_registry.py @@ -43,7 +43,11 @@ def reset_env_and_registry(monkeypatch): """Reset environment variables, registry and overrides before each test.""" # Clean up environment variables for key in list(os.environ.keys()): - if key.startswith("ADK_ENABLE_") or key.startswith("ADK_DISABLE_"): + if ( + key.startswith("ADK_ENABLE_") + or key.startswith("ADK_DISABLE_") + or key == "ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS" + ): monkeypatch.delenv(key, raising=False) # Reset warned features set @@ -118,6 +122,49 @@ def test_experimental_enabled_feature(self): w[0].message ) + def test_experimental_warning_suppressed_by_general_env_var(self, monkeypatch): + """General suppression env var silences registry experimental warnings.""" + _register_feature("SUPPRESSED_EXP", FEATURE_CONFIG_EXPERIMENTAL_ENABLED) + monkeypatch.setenv("ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS", "yes") + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + assert is_feature_enabled("SUPPRESSED_EXP") + assert not w + + def test_enable_env_warning_suppressed_by_general_env_var(self, monkeypatch): + """Suppression also applies when an experimental feature is env-enabled.""" + _register_feature("ENV_ENABLED_EXP", FEATURE_CONFIG_EXPERIMENTAL_DISABLED) + monkeypatch.setenv("ADK_ENABLE_ENV_ENABLED_EXP", "true") + monkeypatch.setenv("ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS", "on") + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + assert is_feature_enabled("ENV_ENABLED_EXP") + assert not w + + def test_override_warning_suppressed_by_general_env_var(self, monkeypatch): + """Suppression also applies to programmatic overrides.""" + _register_feature("OVERRIDE_ENABLED_EXP", FEATURE_CONFIG_EXPERIMENTAL_DISABLED) + override_feature_enabled("OVERRIDE_ENABLED_EXP", True) + monkeypatch.setenv("ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS", "1") + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + assert is_feature_enabled("OVERRIDE_ENABLED_EXP") + assert not w + + def test_wip_warning_not_suppressed_by_general_env_var(self, monkeypatch): + """The experimental suppression flag must not suppress WIP warnings.""" + _register_feature("SUPPRESSED_WIP", FeatureConfig(FeatureStage.WIP, True)) + monkeypatch.setenv("ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS", "true") + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + assert is_feature_enabled("SUPPRESSED_WIP") + assert len(w) == 1 + assert "[WIP] feature SUPPRESSED_WIP is enabled." in str(w[0].message) + def test_stable_feature_enabled(self): """Stable features are enabled.""" _register_feature("STABLE_FEATURE", FEATURE_CONFIG_STABLE) diff --git a/tests/unittests/utils/test_env_utils.py b/tests/unittests/utils/test_env_utils.py index d2635392a9..540a33ece7 100644 --- a/tests/unittests/utils/test_env_utils.py +++ b/tests/unittests/utils/test_env_utils.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from google.adk.utils.env_utils import is_env_truthy +from google.adk.utils.env_utils import is_experimental_warning_suppressed from google.adk.utils.env_utils import is_env_enabled import pytest @@ -47,3 +49,38 @@ def test_is_env_enabled_with_defaults(monkeypatch, default, expected): """Test is_env_enabled when env var is not set with different defaults.""" monkeypatch.delenv('TEST_FLAG', raising=False) assert is_env_enabled('TEST_FLAG', default=default) is expected + + +@pytest.mark.parametrize( + 'env_value,expected', + [ + ('true', True), + (' YES ', True), + ('on', True), + ('1', True), + ('false', False), + ('off', False), + ('0', False), + ('', False), + ], +) +def test_is_env_truthy(monkeypatch, env_value, expected): + """Test the permissive truthy parsing used by warning suppression.""" + monkeypatch.setenv('TEST_FLAG', env_value) + assert is_env_truthy('TEST_FLAG') is expected + + +def test_is_experimental_warning_suppressed_checks_general_env(monkeypatch): + """General suppression env var disables experimental warnings.""" + monkeypatch.setenv('ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS', 'yes') + assert is_experimental_warning_suppressed() + + +def test_is_experimental_warning_suppressed_checks_additional_env(monkeypatch): + """Additional env vars can opt warning surfaces into the same suppression.""" + monkeypatch.delenv('ADK_SUPPRESS_EXPERIMENTAL_FEATURE_WARNINGS', raising=False) + monkeypatch.setenv('ADK_SUPPRESS_A2A_EXPERIMENTAL_FEATURE_WARNINGS', 'on') + + assert is_experimental_warning_suppressed( + 'ADK_SUPPRESS_A2A_EXPERIMENTAL_FEATURE_WARNINGS' + )