diff --git a/.changes/unreleased/Features-20240808-194933.yaml b/.changes/unreleased/Features-20240808-194933.yaml new file mode 100644 index 0000000..1dfd106 --- /dev/null +++ b/.changes/unreleased/Features-20240808-194933.yaml @@ -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" diff --git a/.gitignore b/.gitignore index 5ad1a6c..85795bd 100644 --- a/.gitignore +++ b/.gitignore @@ -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/ diff --git a/dbt_common/__about__.py b/dbt_common/__about__.py index a55413d..f15b401 100644 --- a/dbt_common/__about__.py +++ b/dbt_common/__about__.py @@ -1 +1 @@ -version = "1.7.0" +version = "1.8.0a1" diff --git a/dbt_common/behavior_flags.py b/dbt_common/behavior_flags.py new file mode 100644 index 0000000..a647ed7 --- /dev/null +++ b/dbt_common/behavior_flags.py @@ -0,0 +1,125 @@ +import inspect +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.functions import fire_event +from dbt_common.events.types import BehaviorDeprecationEvent +from dbt_common.exceptions import CompilationError + + +class BehaviorFlag(TypedDict): + """ + Configuration used to create a BehaviorFlagRendered instance + + Args: + name: the name of the behavior flag + default: default setting, starts as False, becomes True after a bake-in period + 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: + """ + A rendered behavior flag that gets used throughout dbt packages + + Args: + 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, 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: + if self._setting is False: + fire_event(self.deprecation_event) + return self._setting + + @setting.setter + def setting(self, value: bool) -> None: + self._setting = value + + @property + 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 this class. + For adapters, this will likely be `dbt.adapters..impl` for `dbt-foo`. + """ + for frame in inspect.stack(): + if module := inspect.getmodule(frame[0]): + if module.__name__ != __name__: + return module.__name__ + return "Unknown" + + def __bool__(self) -> bool: + return self.setting + + +class Behavior: + """ + A collection of behavior flags + + This is effectively a dictionary that supports dot notation for easy reference, e.g.: + ```python + if adapter.behavior.my_flag: + ... + + if adapter.behavior.my_flag.no_warn: # this will not fire the deprecation event + ... + ``` + ```jinja + {% if adapter.behavior.my_flag %} + ... + {% endif %} + + {% if adapter.behavior.my_flag.no_warn %} {# this will not fire the deprecation event #} + ... + {% endif %} + ``` + + Args: + flags: a list of configurations, one for each behavior flag + user_overrides: a set of user settings, which may include overrides on one or more of the behavior flags + """ + + _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 CompilationError(f"The flag {name} has not be registered.") diff --git a/dbt_common/events/types.proto b/dbt_common/events/types.proto index 3ca066d..d72d6b2 100644 --- a/dbt_common/events/types.proto +++ b/dbt_common/events/types.proto @@ -23,6 +23,22 @@ message GenericMessage { EventInfo info = 1; } +// D - Deprecations + +// D018 +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 diff --git a/dbt_common/events/types.py b/dbt_common/events/types.py index b60f9e1..02fc3ee 100644 --- a/dbt_common/events/types.py +++ b/dbt_common/events/types.py @@ -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 @@ -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 "D018" + + 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 # ======================================================= diff --git a/dbt_common/events/types_pb2.py b/dbt_common/events/types_pb2.py index 2112273..6b3afc5 100644 --- a/dbt_common/events/types_pb2.py +++ b/dbt_common/events/types_pb2.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Generated by the protocol buffer compiler. DO NOT EDIT! # source: types.proto -# Protobuf Python Version: 5.26.1 +# Protobuf Python Version: 4.25.2 """Generated protocol buffer code.""" from google.protobuf import descriptor as _descriptor from google.protobuf import descriptor_pool as _descriptor_pool @@ -15,14 +15,14 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0btypes.proto\x12\x0bproto_types\x1a\x1fgoogle/protobuf/timestamp.proto\"\x91\x02\n\tEventInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0b\n\x03msg\x18\x03 \x01(\t\x12\r\n\x05level\x18\x04 \x01(\t\x12\x15\n\rinvocation_id\x18\x05 \x01(\t\x12\x0b\n\x03pid\x18\x06 \x01(\x05\x12\x0e\n\x06thread\x18\x07 \x01(\t\x12&\n\x02ts\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x05\x65xtra\x18\t \x03(\x0b\x32!.proto_types.EventInfo.ExtraEntry\x12\x10\n\x08\x63\x61tegory\x18\n \x01(\t\x1a,\n\nExtraEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"6\n\x0eGenericMessage\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\"1\n\x11RetryExternalCall\x12\x0f\n\x07\x61ttempt\x18\x01 \x01(\x05\x12\x0b\n\x03max\x18\x02 \x01(\x05\"j\n\x14RetryExternalCallMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12,\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x1e.proto_types.RetryExternalCall\"#\n\x14RecordRetryException\x12\x0b\n\x03\x65xc\x18\x01 \x01(\t\"p\n\x17RecordRetryExceptionMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12/\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32!.proto_types.RecordRetryException\"@\n\x13SystemCouldNotWrite\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x0b\n\x03\x65xc\x18\x03 \x01(\t\"n\n\x16SystemCouldNotWriteMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12.\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32 .proto_types.SystemCouldNotWrite\"!\n\x12SystemExecutingCmd\x12\x0b\n\x03\x63md\x18\x01 \x03(\t\"l\n\x15SystemExecutingCmdMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12-\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x1f.proto_types.SystemExecutingCmd\"\x1c\n\x0cSystemStdOut\x12\x0c\n\x04\x62msg\x18\x01 \x01(\t\"`\n\x0fSystemStdOutMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12\'\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x19.proto_types.SystemStdOut\"\x1c\n\x0cSystemStdErr\x12\x0c\n\x04\x62msg\x18\x01 \x01(\t\"`\n\x0fSystemStdErrMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12\'\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x19.proto_types.SystemStdErr\",\n\x16SystemReportReturnCode\x12\x12\n\nreturncode\x18\x01 \x01(\x05\"t\n\x19SystemReportReturnCodeMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12\x31\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32#.proto_types.SystemReportReturnCode\"\x19\n\nFormatting\x12\x0b\n\x03msg\x18\x01 \x01(\t\"\\\n\rFormattingMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12%\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x17.proto_types.Formatting\"\x13\n\x04Note\x12\x0b\n\x03msg\x18\x01 \x01(\t\"P\n\x07NoteMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12\x1f\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x11.proto_types.Note\"\x19\n\nPrintEvent\x12\x0b\n\x03msg\x18\x01 \x01(\t\"\\\n\rPrintEventMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12%\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x17.proto_types.PrintEventb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0btypes.proto\x12\x0bproto_types\x1a\x1fgoogle/protobuf/timestamp.proto\"\x91\x02\n\tEventInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04\x63ode\x18\x02 \x01(\t\x12\x0b\n\x03msg\x18\x03 \x01(\t\x12\r\n\x05level\x18\x04 \x01(\t\x12\x15\n\rinvocation_id\x18\x05 \x01(\t\x12\x0b\n\x03pid\x18\x06 \x01(\x05\x12\x0e\n\x06thread\x18\x07 \x01(\t\x12&\n\x02ts\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x30\n\x05\x65xtra\x18\t \x03(\x0b\x32!.proto_types.EventInfo.ExtraEntry\x12\x10\n\x08\x63\x61tegory\x18\n \x01(\t\x1a,\n\nExtraEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\"6\n\x0eGenericMessage\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\"\x8e\x01\n\x18\x42\x65haviorDeprecationEvent\x12\x11\n\tflag_name\x18\x01 \x01(\t\x12\x13\n\x0b\x66lag_source\x18\x02 \x01(\t\x12\x1b\n\x13\x64\x65precation_version\x18\x03 \x01(\t\x12\x1b\n\x13\x64\x65precation_message\x18\x04 \x01(\t\x12\x10\n\x08\x64ocs_url\x18\x05 \x01(\t\"x\n\x1b\x42\x65haviorDeprecationEventMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12\x33\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32%.proto_types.BehaviorDeprecationEvent\"1\n\x11RetryExternalCall\x12\x0f\n\x07\x61ttempt\x18\x01 \x01(\x05\x12\x0b\n\x03max\x18\x02 \x01(\x05\"j\n\x14RetryExternalCallMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12,\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x1e.proto_types.RetryExternalCall\"#\n\x14RecordRetryException\x12\x0b\n\x03\x65xc\x18\x01 \x01(\t\"p\n\x17RecordRetryExceptionMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12/\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32!.proto_types.RecordRetryException\"@\n\x13SystemCouldNotWrite\x12\x0c\n\x04path\x18\x01 \x01(\t\x12\x0e\n\x06reason\x18\x02 \x01(\t\x12\x0b\n\x03\x65xc\x18\x03 \x01(\t\"n\n\x16SystemCouldNotWriteMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12.\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32 .proto_types.SystemCouldNotWrite\"!\n\x12SystemExecutingCmd\x12\x0b\n\x03\x63md\x18\x01 \x03(\t\"l\n\x15SystemExecutingCmdMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12-\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x1f.proto_types.SystemExecutingCmd\"\x1c\n\x0cSystemStdOut\x12\x0c\n\x04\x62msg\x18\x01 \x01(\t\"`\n\x0fSystemStdOutMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12\'\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x19.proto_types.SystemStdOut\"\x1c\n\x0cSystemStdErr\x12\x0c\n\x04\x62msg\x18\x01 \x01(\t\"`\n\x0fSystemStdErrMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12\'\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x19.proto_types.SystemStdErr\",\n\x16SystemReportReturnCode\x12\x12\n\nreturncode\x18\x01 \x01(\x05\"t\n\x19SystemReportReturnCodeMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12\x31\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32#.proto_types.SystemReportReturnCode\"\x19\n\nFormatting\x12\x0b\n\x03msg\x18\x01 \x01(\t\"\\\n\rFormattingMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12%\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x17.proto_types.Formatting\"\x13\n\x04Note\x12\x0b\n\x03msg\x18\x01 \x01(\t\"P\n\x07NoteMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12\x1f\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x11.proto_types.Note\"\x19\n\nPrintEvent\x12\x0b\n\x03msg\x18\x01 \x01(\t\"\\\n\rPrintEventMsg\x12$\n\x04info\x18\x01 \x01(\x0b\x32\x16.proto_types.EventInfo\x12%\n\x04\x64\x61ta\x18\x02 \x01(\x0b\x32\x17.proto_types.PrintEventb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'types_pb2', _globals) -if not _descriptor._USE_C_DESCRIPTORS: - DESCRIPTOR._loaded_options = None - _globals['_EVENTINFO_EXTRAENTRY']._loaded_options = None +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_EVENTINFO_EXTRAENTRY']._options = None _globals['_EVENTINFO_EXTRAENTRY']._serialized_options = b'8\001' _globals['_EVENTINFO']._serialized_start=62 _globals['_EVENTINFO']._serialized_end=335 @@ -30,44 +30,48 @@ _globals['_EVENTINFO_EXTRAENTRY']._serialized_end=335 _globals['_GENERICMESSAGE']._serialized_start=337 _globals['_GENERICMESSAGE']._serialized_end=391 - _globals['_RETRYEXTERNALCALL']._serialized_start=393 - _globals['_RETRYEXTERNALCALL']._serialized_end=442 - _globals['_RETRYEXTERNALCALLMSG']._serialized_start=444 - _globals['_RETRYEXTERNALCALLMSG']._serialized_end=550 - _globals['_RECORDRETRYEXCEPTION']._serialized_start=552 - _globals['_RECORDRETRYEXCEPTION']._serialized_end=587 - _globals['_RECORDRETRYEXCEPTIONMSG']._serialized_start=589 - _globals['_RECORDRETRYEXCEPTIONMSG']._serialized_end=701 - _globals['_SYSTEMCOULDNOTWRITE']._serialized_start=703 - _globals['_SYSTEMCOULDNOTWRITE']._serialized_end=767 - _globals['_SYSTEMCOULDNOTWRITEMSG']._serialized_start=769 - _globals['_SYSTEMCOULDNOTWRITEMSG']._serialized_end=879 - _globals['_SYSTEMEXECUTINGCMD']._serialized_start=881 - _globals['_SYSTEMEXECUTINGCMD']._serialized_end=914 - _globals['_SYSTEMEXECUTINGCMDMSG']._serialized_start=916 - _globals['_SYSTEMEXECUTINGCMDMSG']._serialized_end=1024 - _globals['_SYSTEMSTDOUT']._serialized_start=1026 - _globals['_SYSTEMSTDOUT']._serialized_end=1054 - _globals['_SYSTEMSTDOUTMSG']._serialized_start=1056 - _globals['_SYSTEMSTDOUTMSG']._serialized_end=1152 - _globals['_SYSTEMSTDERR']._serialized_start=1154 - _globals['_SYSTEMSTDERR']._serialized_end=1182 - _globals['_SYSTEMSTDERRMSG']._serialized_start=1184 - _globals['_SYSTEMSTDERRMSG']._serialized_end=1280 - _globals['_SYSTEMREPORTRETURNCODE']._serialized_start=1282 - _globals['_SYSTEMREPORTRETURNCODE']._serialized_end=1326 - _globals['_SYSTEMREPORTRETURNCODEMSG']._serialized_start=1328 - _globals['_SYSTEMREPORTRETURNCODEMSG']._serialized_end=1444 - _globals['_FORMATTING']._serialized_start=1446 - _globals['_FORMATTING']._serialized_end=1471 - _globals['_FORMATTINGMSG']._serialized_start=1473 - _globals['_FORMATTINGMSG']._serialized_end=1565 - _globals['_NOTE']._serialized_start=1567 - _globals['_NOTE']._serialized_end=1586 - _globals['_NOTEMSG']._serialized_start=1588 - _globals['_NOTEMSG']._serialized_end=1668 - _globals['_PRINTEVENT']._serialized_start=1670 - _globals['_PRINTEVENT']._serialized_end=1695 - _globals['_PRINTEVENTMSG']._serialized_start=1697 - _globals['_PRINTEVENTMSG']._serialized_end=1789 + _globals['_BEHAVIORDEPRECATIONEVENT']._serialized_start=394 + _globals['_BEHAVIORDEPRECATIONEVENT']._serialized_end=536 + _globals['_BEHAVIORDEPRECATIONEVENTMSG']._serialized_start=538 + _globals['_BEHAVIORDEPRECATIONEVENTMSG']._serialized_end=658 + _globals['_RETRYEXTERNALCALL']._serialized_start=660 + _globals['_RETRYEXTERNALCALL']._serialized_end=709 + _globals['_RETRYEXTERNALCALLMSG']._serialized_start=711 + _globals['_RETRYEXTERNALCALLMSG']._serialized_end=817 + _globals['_RECORDRETRYEXCEPTION']._serialized_start=819 + _globals['_RECORDRETRYEXCEPTION']._serialized_end=854 + _globals['_RECORDRETRYEXCEPTIONMSG']._serialized_start=856 + _globals['_RECORDRETRYEXCEPTIONMSG']._serialized_end=968 + _globals['_SYSTEMCOULDNOTWRITE']._serialized_start=970 + _globals['_SYSTEMCOULDNOTWRITE']._serialized_end=1034 + _globals['_SYSTEMCOULDNOTWRITEMSG']._serialized_start=1036 + _globals['_SYSTEMCOULDNOTWRITEMSG']._serialized_end=1146 + _globals['_SYSTEMEXECUTINGCMD']._serialized_start=1148 + _globals['_SYSTEMEXECUTINGCMD']._serialized_end=1181 + _globals['_SYSTEMEXECUTINGCMDMSG']._serialized_start=1183 + _globals['_SYSTEMEXECUTINGCMDMSG']._serialized_end=1291 + _globals['_SYSTEMSTDOUT']._serialized_start=1293 + _globals['_SYSTEMSTDOUT']._serialized_end=1321 + _globals['_SYSTEMSTDOUTMSG']._serialized_start=1323 + _globals['_SYSTEMSTDOUTMSG']._serialized_end=1419 + _globals['_SYSTEMSTDERR']._serialized_start=1421 + _globals['_SYSTEMSTDERR']._serialized_end=1449 + _globals['_SYSTEMSTDERRMSG']._serialized_start=1451 + _globals['_SYSTEMSTDERRMSG']._serialized_end=1547 + _globals['_SYSTEMREPORTRETURNCODE']._serialized_start=1549 + _globals['_SYSTEMREPORTRETURNCODE']._serialized_end=1593 + _globals['_SYSTEMREPORTRETURNCODEMSG']._serialized_start=1595 + _globals['_SYSTEMREPORTRETURNCODEMSG']._serialized_end=1711 + _globals['_FORMATTING']._serialized_start=1713 + _globals['_FORMATTING']._serialized_end=1738 + _globals['_FORMATTINGMSG']._serialized_start=1740 + _globals['_FORMATTINGMSG']._serialized_end=1832 + _globals['_NOTE']._serialized_start=1834 + _globals['_NOTE']._serialized_end=1853 + _globals['_NOTEMSG']._serialized_start=1855 + _globals['_NOTEMSG']._serialized_end=1935 + _globals['_PRINTEVENT']._serialized_start=1937 + _globals['_PRINTEVENT']._serialized_end=1962 + _globals['_PRINTEVENTMSG']._serialized_start=1964 + _globals['_PRINTEVENTMSG']._serialized_end=2056 # @@protoc_insertion_point(module_scope) diff --git a/pyproject.toml b/pyproject.toml index ba30643..2e40385 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -127,9 +127,10 @@ exclude = [ "dbt_common/events/types_pb2.py", "venv", ".venv", - "env*" + "env*", + ".hatch/*", ] -per-file-ignores = ["*/__init__.py: F401"] +per-file-ignores = ["*/__init__.py: F401", "*/conftest.py: F401"] docstring-convention = "google" [tool.mypy] @@ -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 exclude = [ "dbt_common/events/types_pb2.py", "env*", diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py new file mode 100644 index 0000000..c553bf5 --- /dev/null +++ b/tests/unit/conftest.py @@ -0,0 +1 @@ +from tests.unit.utils import event_catcher diff --git a/tests/unit/test_behavior_flags.py b/tests/unit/test_behavior_flags.py new file mode 100644 index 0000000..256b744 --- /dev/null +++ b/tests/unit/test_behavior_flags.py @@ -0,0 +1,116 @@ +import pytest + +from dbt_common.behavior_flags import Behavior +from dbt_common.exceptions.base import CompilationError + + +def test_behavior_default(): + behavior = Behavior( + [ + {"name": "default_false_flag", "default": False}, + {"name": "default_true_flag", "default": True}, + ], + {}, + ) + + assert behavior.default_false_flag.setting is False + assert behavior.default_true_flag.setting is True + + +def test_behavior_user_override(): + behavior = Behavior( + [ + {"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}, + ], + { + "flag_default_false_override_false": False, + "flag_default_false_override_true": True, + "flag_default_true_override_false": False, + "flag_default_true_override_true": True, + }, + ) + + assert behavior.flag_default_false.setting is False + assert behavior.flag_default_false_override_false.setting is False + assert behavior.flag_default_false_override_true.setting is True + assert behavior.flag_default_true.setting is True + assert behavior.flag_default_true_override_false.setting is False + assert behavior.flag_default_true_override_true.setting is True + + +def test_behavior_unregistered_flag_raises_correct_exception(): + behavior = Behavior( + [ + {"name": "behavior_flag_exists", "default": False}, + ], + {}, + ) + + assert behavior.behavior_flag_exists.setting is False + with pytest.raises(CompilationError): + assert behavior.behavior_flag_does_not_exist + + +def test_behavior_flag_can_be_used_as_conditional(): + behavior = Behavior( + [ + {"name": "flag_false", "default": False}, + {"name": "flag_true", "default": True}, + ], + {}, + ) + + assert False if behavior.flag_false else True + assert True if behavior.flag_true else False + + +def test_behavior_flags_emit_deprecation_event_on_evaluation(event_catcher) -> None: + behavior = Behavior( + [ + {"name": "flag_false", "default": False}, + {"name": "flag_true", "default": True}, + ], + {}, + ) + + # trigger the evaluation, no event should fire + if behavior.flag_true: + pass + assert len(event_catcher.caught_events) == 0 + + # trigger the evaluation, an event should fire + if behavior.flag_false: + pass + assert len(event_catcher.caught_events) == 1 + + +def test_behavior_flags_emit_correct_deprecation_event(event_catcher) -> None: + behavior = Behavior([{"name": "flag_false", "default": False}], {}) + + # trigger the evaluation + if behavior.flag_false: + pass + + msg = event_catcher.caught_events[0] + assert msg.info.name == "BehaviorDeprecationEvent" + assert msg.data.flag_name == "flag_false" + assert msg.data.flag_source == __name__ # defaults to the calling module + + +def test_behavior_flags_no_deprecation_event_on_no_warn(event_catcher) -> None: + behavior = Behavior([{"name": "flag_false", "default": False}], {}) + + # trigger the evaluation with no_warn, no event should fire + if behavior.flag_false.no_warn: + pass + assert len(event_catcher.caught_events) == 0 + + # trigger the evaluation, an event should fire + if behavior.flag_false: + pass + assert len(event_catcher.caught_events) == 1 diff --git a/tests/unit/test_events.py b/tests/unit/test_events.py index 800790d..9a9abc6 100644 --- a/tests/unit/test_events.py +++ b/tests/unit/test_events.py @@ -61,6 +61,8 @@ class TestEventJSONSerialization: SAMPLE_VALUES = [ # N.B. Events instantiated here include the module prefix in order to # avoid having the entire list twice in the code. + # D - Deprecations ====================== + types.BehaviorDeprecationEvent(flag_name="Do you have a flag?", flag_source="dbt_common"), # M - Deps generation ====================== types.RetryExternalCall(attempt=0, max=0), types.RecordRetryException(exc=""), diff --git a/tests/unit/utils.py b/tests/unit/utils.py index c79ecc5..3a46a2c 100644 --- a/tests/unit/utils.py +++ b/tests/unit/utils.py @@ -1,7 +1,12 @@ from dataclasses import dataclass, field from typing import List +import pytest +from pytest_mock import MockerFixture + from dbt_common.events.base_types import EventMsg +from dbt_common.events.event_manager import EventManager +from dbt_common.events.event_manager_client import add_callback_to_manager @dataclass @@ -10,3 +15,12 @@ class EventCatcher: def catch(self, event: EventMsg) -> None: self.caught_events.append(event) + + +@pytest.fixture(scope="function") +def event_catcher(mocker: MockerFixture) -> EventCatcher: + # mock out the global event manager so the callback doesn't get added to all other tests + mocker.patch("dbt_common.events.event_manager_client._EVENT_MANAGER", EventManager()) + catcher = EventCatcher() + add_callback_to_manager(catcher.catch) + return catcher