Skip to content

Commit

Permalink
move flag initialization into rendered flag, adopt Rendered naming co…
Browse files Browse the repository at this point in the history
…nvention
  • Loading branch information
mikealfare committed Aug 29, 2024
1 parent 6adff8c commit b9ad7c2
Show file tree
Hide file tree
Showing 2 changed files with 63 additions and 87 deletions.
122 changes: 53 additions & 69 deletions dbt_common/behavior_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,44 @@
# This is the suggested way to implement a TypedDict with optional arguments
from typing import Optional as NotRequired

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:
class BehaviorFlag(TypedDict):
"""
A set of config used to create a BehaviorFlag
Args:
name: the name of the behavior flag
default: default setting, starts as False, becomes True in the next minor release
deprecation_version: the version when the default will change to True
deprecation_message: an additional message to send when the flag evaluates to False
docs_url: the url to the relevant docs on docs.getdbt.com
"""

name: str
default: bool
source: NotRequired[str]
deprecation_version: NotRequired[str]
deprecation_message: NotRequired[str]
docs_url: NotRequired[str]


class BehaviorFlagRendered:
"""
The canonical behavior flag that gets used through dbt packages
Args:
name: the name of the behavior flag, e.g. enforce_quoting_on_relation_creation
setting: the flag setting, after accounting for user input and the default
deprecation_event: the event to fire if the flag evaluates to False
flag: the configuration for the behavior flag
user_overrides: a set of user settings, one of which may be an override on this behavior flag
"""

def __init__(self, name: str, setting: bool, deprecation_event: WarnLevel) -> None:
self.name = name
self.setting = setting
self.deprecation_event = deprecation_event
def __init__(self, flag: BehaviorFlag, user_overrides: Dict[str, Any]) -> None:
self.name = flag["name"]
self.setting = user_overrides.get(flag["name"], flag["default"])
self.deprecation_event = self._deprecation_event(flag)

@property
def setting(self) -> bool:
Expand All @@ -43,74 +61,40 @@ def setting(self, value: bool) -> None:
def no_warn(self) -> bool:
return self._setting

def _deprecation_event(self, flag: BehaviorFlag) -> BehaviorDeprecationEvent:
return BehaviorDeprecationEvent(
flag_name=flag["name"],
flag_source=flag.get("source", self._default_source()),
deprecation_version=flag.get("deprecation_version"),
deprecation_message=flag.get("deprecation_message"),
docs_url=flag.get("docs_url"),
)

@staticmethod
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.<foo>.impl` for `dbt-foo`.
"""
frame = inspect.stack()[5]
if module := inspect.getmodule(frame[0]):
return module.__name__
return "Unknown"

def __bool__(self) -> bool:
return self.setting


class RawBehaviorFlag(TypedDict):
"""
A set of config used to create a BehaviorFlag
Args:
name: the name of the behavior flag
default: default setting, starts as False, becomes True in the next minor release
deprecation_version: the version when the default will change to True
deprecation_message: an additional message to send when the flag evaluates to False
docs_url: the url to the relevant docs on docs.getdbt.com
"""

name: str
default: bool
source: NotRequired[str]
deprecation_version: NotRequired[str]
deprecation_message: NotRequired[str]
docs_url: NotRequired[str]


# this is effectively a dictionary that supports dot notation
# it makes usage easy, e.g. adapter.behavior.my_flag
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:
_flags: List[BehaviorFlagRendered]

def __init__(self, flags: List[BehaviorFlag], user_overrides: Dict[str, Any]) -> None:
self._flags = [BehaviorFlagRendered(flag, user_overrides) for flag in flags]

def __getattr__(self, name: str) -> BehaviorFlagRendered:
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:
"""
If the maintainer did not provide a source, default to the module that called `register`.
For adapters, this will likely be `dbt.adapters.<foo>.impl` for `dbt-foo`.
"""
frame = inspect.stack()[3]
if module := inspect.getmodule(frame[0]):
return module.__name__
return "Unknown"
28 changes: 10 additions & 18 deletions tests/unit/test_behavior_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@

def test_behavior_default():
behavior = Behavior(
behavior_flags=[
[
{"name": "default_false_flag", "default": False},
{"name": "default_true_flag", "default": True},
],
user_overrides={},
{},
)

assert behavior.default_false_flag.setting is False
Expand All @@ -23,15 +23,15 @@ def test_behavior_default():

def test_behavior_user_override():
behavior = Behavior(
behavior_flags=[
[
{"name": "flag_default_false", "default": False},
{"name": "flag_default_false_override_false", "default": False},
{"name": "flag_default_false_override_true", "default": False},
{"name": "flag_default_true", "default": True},
{"name": "flag_default_true_override_false", "default": True},
{"name": "flag_default_true_override_true", "default": True},
],
user_overrides={
{
"flag_default_false_override_false": False,
"flag_default_false_override_true": True,
"flag_default_true_override_false": False,
Expand All @@ -49,11 +49,11 @@ def test_behavior_user_override():

def test_behavior_flag_can_be_used_as_conditional():
behavior = Behavior(
behavior_flags=[
[
{"name": "flag_false", "default": False},
{"name": "flag_true", "default": True},
],
user_overrides={},
{},
)

assert False if behavior.flag_false else True
Expand All @@ -71,11 +71,11 @@ def event_catcher(mocker: MockerFixture) -> EventCatcher:

def test_behavior_flags_emit_deprecation_event_on_evaluation(event_catcher) -> None:
behavior = Behavior(
behavior_flags=[
[
{"name": "flag_false", "default": False},
{"name": "flag_true", "default": True},
],
user_overrides={},
{},
)

# trigger the evaluation, no event should fire
Expand All @@ -90,10 +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 = Behavior(
behavior_flags=[{"name": "flag_false", "default": False}],
user_overrides={},
)
behavior = Behavior([{"name": "flag_false", "default": False}], {})

# trigger the evaluation
if behavior.flag_false:
Expand All @@ -106,12 +103,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 = Behavior(
behavior_flags=[
{"name": "flag_false", "default": False},
],
user_overrides={},
)
behavior = Behavior([{"name": "flag_false", "default": False}], {})

# trigger the evaluation with no_warn, no event should fire
if behavior.flag_false.no_warn:
Expand Down

0 comments on commit b9ad7c2

Please sign in to comment.