Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Behavior Flag Framework #183

Merged
merged 17 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/unreleased/Features-20240808-194933.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
kind: Features
body: Add Behavior Flag Framework
time: 2024-08-08T19:49:33.738569-04:00
custom:
Author: mikealfare
Issue: "178"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/

# VSCode
.vscode/
108 changes: 108 additions & 0 deletions dbt_common/behavior_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import inspect
from types import SimpleNamespace
from typing import Any, Dict, List, TypedDict

try:
from typing import NotRequired
except ImportError:
# NotRequired was introduced in Python 3.11
# 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


class BehaviorFlag:
"""
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
"""

def __init__(self, name: str, setting: bool, deprecation_event: WarnLevel) -> None:
self.name = name
self.setting = setting
self.deprecation_event = deprecation_event

@property
def setting(self) -> bool:
if self._setting is False:
fire_event(self.deprecation_event)
return self._setting

@setting.setter
def setting(self, value: bool) -> None:
self._setting = value

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
Behavior = SimpleNamespace
mikealfare marked this conversation as resolved.
Show resolved Hide resolved


def register(
behavior_flags: List[RawBehaviorFlag],
user_flags: Dict[str, Any],
mikealfare marked this conversation as resolved.
Show resolved Hide resolved
) -> 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_flags.get(raw_flag["name"]) is not None:
flag["setting"] = user_flags.get(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


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()[2]
if module := inspect.getmodule(frame[0]):
return module.__name__
return "Unknown"
16 changes: 16 additions & 0 deletions dbt_common/events/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,22 @@ message GenericMessage {
EventInfo info = 1;
}

// D - Deprecations

// D042
message BehaviorDeprecationEvent {
string flag_name = 1;
string flag_source = 2;
string deprecation_version = 3;
string deprecation_message = 4;
string docs_url = 5;
}

message BehaviorDeprecationEventMsg {
EventInfo info = 1;
BehaviorDeprecationEvent data = 2;
}

// M - Deps generation

// M020
Expand Down
39 changes: 39 additions & 0 deletions dbt_common/events/types.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
from typing import Optional

from dbt_common.events.base_types import (
DebugLevel,
InfoLevel,
WarnLevel,
)
from dbt_common.ui import warning_tag


# The classes in this file represent the data necessary to describe a
Expand All @@ -28,6 +32,41 @@
#
# The basic idea is that event codes roughly translate to the natural order of running a dbt task


# =======================================================
# D - Deprecations
# =======================================================


class BehaviorDeprecationEvent(WarnLevel):
flag_name: str
flag_source: str
deprecation_version: Optional[str]
deprecation_message: Optional[str]
docs_url: Optional[str]

def code(self) -> str:
return "D042" # TODO: update this to the next unused code

def message(self) -> str:
msg = f"The legacy behavior controlled by `{self.flag_name}` is deprecated.\n"

if self.deprecation_version:
msg = (
f"The legacy behavior is expected to be retired in `{self.deprecation_version}`.\n"
)

msg += f"The new behavior can be turned on by setting `flags.{self.flag_name}` to `True` in `dbt_project.yml`.\n"

if self.deprecation_message:
msg += f"{self.deprecation_message}.\n"

docs_url = self.docs_url or f"https://docs.getdbt.com/search?q={self.flag_name}"
msg += f"Visit {docs_url} for more information."

return warning_tag(msg)


# =======================================================
# M - Deps generation
# =======================================================
Expand Down
94 changes: 49 additions & 45 deletions dbt_common/events/types_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ exclude = [
"dbt_common/events/types_pb2.py",
"venv",
".venv",
"env*"
"env*",
".hatch/*",
]
per-file-ignores = ["*/__init__.py: F401"]
docstring-convention = "google"
Expand All @@ -140,6 +141,7 @@ show_error_codes = true
disable_error_code = "attr-defined" # TODO: revisit once other mypy errors resolved
disallow_untyped_defs = false # TODO: add type annotations everywhere
warn_redundant_casts = true
ignore_missing_imports = true
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mypy was looking for pytest_mock types.

exclude = [
"dbt_common/events/types_pb2.py",
"env*",
Expand Down
Loading
Loading