From 6adff8c99cee4e5daaeb3e433de54d53675c52ae Mon Sep 17 00:00:00 2001 From: Mike Alfare Date: Sat, 24 Aug 2024 17:44:41 -0400 Subject: [PATCH] update Behavior from SimpleNamespace to a custom class to support typing --- dbt_common/behavior_flags.py | 68 ++++++++++++++++--------------- tests/unit/test_behavior_flags.py | 14 +++---- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/dbt_common/behavior_flags.py b/dbt_common/behavior_flags.py index d237ad6..83e9eb3 100644 --- a/dbt_common/behavior_flags.py +++ b/dbt_common/behavior_flags.py @@ -1,5 +1,4 @@ import inspect -from types import SimpleNamespace from typing import Any, Dict, List, TypedDict try: @@ -12,6 +11,7 @@ from dbt_common.events.base_types import WarnLevel from dbt_common.events.functions import fire_event from dbt_common.events.types import BehaviorDeprecationEvent +from dbt_common.exceptions import DbtInternalError class BehaviorFlag: @@ -69,36 +69,40 @@ class RawBehaviorFlag(TypedDict): # this is effectively a dictionary that supports dot notation # it makes usage easy, e.g. adapter.behavior.my_flag -Behavior = SimpleNamespace - - -def register( - behavior_flags: List[RawBehaviorFlag], - user_overrides: Dict[str, Any], -) -> Behavior: - flags = {} - for raw_flag in behavior_flags: - flag = { - "name": raw_flag["name"], - "setting": raw_flag["default"], - } - - # specifically evaluate for `None` since `False` and `None` should be treated differently - if user_overrides.get(raw_flag["name"]) is not None: - flag["setting"] = user_overrides[raw_flag["name"]] - - event = BehaviorDeprecationEvent( - flag_name=raw_flag["name"], - flag_source=raw_flag.get("source", _default_source()), - deprecation_version=raw_flag.get("deprecation_version"), - deprecation_message=raw_flag.get("deprecation_message"), - docs_url=raw_flag.get("docs_url"), - ) - flag["deprecation_event"] = event - - flags[flag["name"]] = BehaviorFlag(**flag) # type: ignore - - return Behavior(**flags) # type: ignore +class Behavior: + _flags: List[BehaviorFlag] + + def __init__( + self, + behavior_flags: List[RawBehaviorFlag], + user_overrides: Dict[str, Any], + ) -> None: + flags = [] + for raw_flag in behavior_flags: + flags.append( + BehaviorFlag( + name=raw_flag["name"], + setting=user_overrides.get(raw_flag["name"], raw_flag["default"]), + deprecation_event=_behavior_deprecation_event(raw_flag), + ) + ) + self._flags = flags + + def __getattr__(self, name: str) -> BehaviorFlag: + for flag in self._flags: + if flag.name == name: + return flag + raise DbtInternalError(f"The flag {name} has not be registered.") + + +def _behavior_deprecation_event(flag: RawBehaviorFlag) -> BehaviorDeprecationEvent: + return BehaviorDeprecationEvent( + flag_name=flag["name"], + flag_source=flag.get("source", _default_source()), + deprecation_version=flag.get("deprecation_version"), + deprecation_message=flag.get("deprecation_message"), + docs_url=flag.get("docs_url"), + ) def _default_source() -> str: @@ -106,7 +110,7 @@ def _default_source() -> str: If the maintainer did not provide a source, default to the module that called `register`. For adapters, this will likely be `dbt.adapters..impl` for `dbt-foo`. """ - frame = inspect.stack()[2] + frame = inspect.stack()[3] if module := inspect.getmodule(frame[0]): return module.__name__ return "Unknown" diff --git a/tests/unit/test_behavior_flags.py b/tests/unit/test_behavior_flags.py index 105566e..9d5dc3b 100644 --- a/tests/unit/test_behavior_flags.py +++ b/tests/unit/test_behavior_flags.py @@ -1,7 +1,7 @@ import pytest from pytest_mock import MockerFixture # type: ignore -from dbt_common.behavior_flags import register +from dbt_common.behavior_flags import Behavior from dbt_common.events.event_manager import EventManager from dbt_common.events.event_manager_client import add_callback_to_manager @@ -9,7 +9,7 @@ def test_behavior_default(): - behavior = register( + behavior = Behavior( behavior_flags=[ {"name": "default_false_flag", "default": False}, {"name": "default_true_flag", "default": True}, @@ -22,7 +22,7 @@ def test_behavior_default(): def test_behavior_user_override(): - behavior = register( + behavior = Behavior( behavior_flags=[ {"name": "flag_default_false", "default": False}, {"name": "flag_default_false_override_false", "default": False}, @@ -48,7 +48,7 @@ def test_behavior_user_override(): def test_behavior_flag_can_be_used_as_conditional(): - behavior = register( + behavior = Behavior( behavior_flags=[ {"name": "flag_false", "default": False}, {"name": "flag_true", "default": True}, @@ -70,7 +70,7 @@ def event_catcher(mocker: MockerFixture) -> EventCatcher: def test_behavior_flags_emit_deprecation_event_on_evaluation(event_catcher) -> None: - behavior = register( + behavior = Behavior( behavior_flags=[ {"name": "flag_false", "default": False}, {"name": "flag_true", "default": True}, @@ -90,7 +90,7 @@ def test_behavior_flags_emit_deprecation_event_on_evaluation(event_catcher) -> N def test_behavior_flags_emit_correct_deprecation(event_catcher) -> None: - behavior = register( + behavior = Behavior( behavior_flags=[{"name": "flag_false", "default": False}], user_overrides={}, ) @@ -106,7 +106,7 @@ def test_behavior_flags_emit_correct_deprecation(event_catcher) -> None: def test_behavior_flags_no_deprecation_event_on_no_warn(event_catcher) -> None: - behavior = register( + behavior = Behavior( behavior_flags=[ {"name": "flag_false", "default": False}, ],