From 424b503458f9f9c4728da46a86923b692645e07d Mon Sep 17 00:00:00 2001 From: Gerda Shank Date: Tue, 6 Jul 2021 16:58:34 -0400 Subject: [PATCH] Configs in schema files --- CHANGELOG.md | 1 + core/dbt/context/context_config.py | 57 +++++++-- core/dbt/context/providers.py | 4 +- core/dbt/contracts/graph/manifest.py | 60 +-------- core/dbt/contracts/graph/model_config.py | 19 ++- core/dbt/contracts/graph/parsed.py | 11 ++ core/dbt/contracts/graph/unparsed.py | 12 +- core/dbt/parser/base.py | 41 +++--- core/dbt/parser/models.py | 26 ++-- core/dbt/parser/schema_test_builders.py | 22 ++++ core/dbt/parser/schemas.py | 119 ++++++++++++------ core/dbt/parser/sources.py | 2 +- .../test_docs_generate.py | 4 + .../033_event_tracking_test/test_events.py | 2 +- .../039_config_test/data-alt/some_seed.csv | 2 + .../039_config_test/macros-alt/my_macro.sql | 8 ++ .../039_config_test/macros-alt/schema.yml | 5 + .../039_config_test/models-alt/schema.yml | 18 +++ .../models-alt/tagged/model.sql | 15 +++ .../039_config_test/models-alt/untagged.sql | 5 + .../test_configs_in_schema_files.py | 82 ++++++++++++ test/integration/047_dbt_ls_test/test_ls.py | 11 ++ .../test_partial_parsing.py | 2 +- test/integration/base.py | 14 +++ test/unit/test_contracts_graph_compiled.py | 8 +- test/unit/test_contracts_graph_parsed.py | 35 ++++-- test/unit/test_contracts_graph_unparsed.py | 5 + 27 files changed, 428 insertions(+), 162 deletions(-) create mode 100644 test/integration/039_config_test/data-alt/some_seed.csv create mode 100644 test/integration/039_config_test/macros-alt/my_macro.sql create mode 100644 test/integration/039_config_test/macros-alt/schema.yml create mode 100644 test/integration/039_config_test/models-alt/schema.yml create mode 100644 test/integration/039_config_test/models-alt/tagged/model.sql create mode 100644 test/integration/039_config_test/models-alt/untagged.sql create mode 100644 test/integration/039_config_test/test_configs_in_schema_files.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 545a4361e0d..1242a4fdae7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ### Features - Add `dbt build` command to run models, tests, seeds, and snapshots in DAG order. ([#2743] (https://github.com/dbt-labs/dbt/issues/2743), [#3490] (https://github.com/dbt-labs/dbt/issues/3490)) - Introduce `on_schema_change` config to detect and handle schema changes on incremental models ([#1132](https://github.com/fishtown-analytics/dbt/issues/1132), [#3387](https://github.com/fishtown-analytics/dbt/issues/3387)) +- Enable setting configs in schema files ([#2401](https://github.com/dbt-labs/dbt/issues/2401)) ### Breaking changes - Add full node selection to source freshness command and align selection syntax with other tasks (`dbt source freshness --select source_name` --> `dbt source freshness --select source:souce_name`) and rename `dbt source snapshot-freshness` -> `dbt source freshness`. ([#2987](https://github.com/dbt-labs/dbt/issues/2987), [#3554](https://github.com/dbt-labs/dbt/pull/3554)) diff --git a/core/dbt/context/context_config.py b/core/dbt/context/context_config.py index 14e279d0585..3dd3779e23b 100644 --- a/core/dbt/context/context_config.py +++ b/core/dbt/context/context_config.py @@ -120,11 +120,12 @@ def initial_result(self, resource_type: NodeType, base: bool) -> T: def calculate_node_config( self, - config_calls: List[Dict[str, Any]], + config_call_dict: Dict[str, Any], fqn: List[str], resource_type: NodeType, project_name: str, base: bool, + patch_config_dict: Dict[str, Any] = None ) -> BaseConfig: own_config = self.get_node_project(project_name) @@ -134,8 +135,15 @@ def calculate_node_config( for fqn_config in project_configs: result = self._update_from_config(result, fqn_config) - for config_call in config_calls: - result = self._update_from_config(result, config_call) + # When schema files patch config, it has lower precedence than + # config in the models (config_call_dict), so we add the patch_config_dict + # before the config_call_dict + if patch_config_dict: + result = self._update_from_config(result, patch_config_dict) + + # config_calls are created in the 'experimental' model parser and + # the ParseConfigObject (via add_config_call) + result = self._update_from_config(result, config_call_dict) if own_config.project_name != self._active_project.project_name: for fqn_config in self._active_project_configs(fqn, resource_type): @@ -147,11 +155,12 @@ def calculate_node_config( @abstractmethod def calculate_node_config_dict( self, - config_calls: List[Dict[str, Any]], + config_call_dict: Dict[str, Any], fqn: List[str], resource_type: NodeType, project_name: str, base: bool, + patch_config_dict: Dict[str, Any], ) -> Dict[str, Any]: ... @@ -186,18 +195,20 @@ def _update_from_config( def calculate_node_config_dict( self, - config_calls: List[Dict[str, Any]], + config_call_dict: Dict[str, Any], fqn: List[str], resource_type: NodeType, project_name: str, base: bool, + patch_config_dict: dict = None ) -> Dict[str, Any]: config = self.calculate_node_config( - config_calls=config_calls, + config_call_dict=config_call_dict, fqn=fqn, resource_type=resource_type, project_name=project_name, base=base, + patch_config_dict=patch_config_dict ) finalized = config.finalize_and_validate() return finalized.to_dict(omit_none=True) @@ -209,18 +220,20 @@ def get_config_source(self, project: Project) -> ConfigSource: def calculate_node_config_dict( self, - config_calls: List[Dict[str, Any]], + config_call_dict: Dict[str, Any], fqn: List[str], resource_type: NodeType, project_name: str, base: bool, + patch_config_dict: dict = None ) -> Dict[str, Any]: return self.calculate_node_config( - config_calls=config_calls, + config_call_dict=config_call_dict, fqn=fqn, resource_type=resource_type, project_name=project_name, base=base, + patch_config_dict=patch_config_dict ) def initial_result( @@ -251,20 +264,39 @@ def __init__( resource_type: NodeType, project_name: str, ) -> None: - self._config_calls: List[Dict[str, Any]] = [] + self._config_call_dict: Dict[str, Any] = {} self._active_project = active_project self._fqn = fqn self._resource_type = resource_type self._project_name = project_name - def update_in_model_config(self, opts: Dict[str, Any]) -> None: - self._config_calls.append(opts) + def add_config_call(self, opts: Dict[str, Any]) -> None: + dct = self._config_call_dict + self._add_config_call(dct, opts) + + @classmethod + def _add_config_call(cls, config_call_dict, opts: Dict[str, Any]) -> None: + for k, v in opts.items(): + # MergeBehavior for post-hook and pre-hook is to collect all + # values, instead of overwriting + if k in BaseConfig.mergebehavior['append']: + if not isinstance(v, list): + v = [v] + if k in BaseConfig.mergebehavior['update'] and not isinstance(v, dict): + raise InternalException(f'expected dict, got {v}') + if k in config_call_dict and isinstance(config_call_dict[k], list): + config_call_dict[k].extend(v) + elif k in config_call_dict and isinstance(config_call_dict[k], dict): + config_call_dict[k].update(v) + else: + config_call_dict[k] = v def build_config_dict( self, base: bool = False, *, rendered: bool = True, + patch_config_dict: dict = None ) -> Dict[str, Any]: if rendered: src = ContextConfigGenerator(self._active_project) @@ -272,9 +304,10 @@ def build_config_dict( src = UnrenderedConfigGenerator(self._active_project) return src.calculate_node_config_dict( - config_calls=self._config_calls, + config_call_dict=self._config_call_dict, fqn=self._fqn, resource_type=self._resource_type, project_name=self._project_name, base=base, + patch_config_dict=patch_config_dict ) diff --git a/core/dbt/context/providers.py b/core/dbt/context/providers.py index 24c265c83da..4599a85b07f 100644 --- a/core/dbt/context/providers.py +++ b/core/dbt/context/providers.py @@ -279,7 +279,7 @@ def __init__(self, model, context_config: Optional[ContextConfig]): ... -# `config` implementations +# Implementation of "config(..)" calls in models class ParseConfigObject(Config): def __init__(self, model, context_config: Optional[ContextConfig]): self.model = model @@ -316,7 +316,7 @@ def __call__(self, *args, **kwargs): raise RuntimeException( 'At parse time, did not receive a context config' ) - self.context_config.update_in_model_config(opts) + self.context_config.add_config_call(opts) return '' def set(self, name, value): diff --git a/core/dbt/contracts/graph/manifest.py b/core/dbt/contracts/graph/manifest.py index 6fc13852989..ad606fed1a9 100644 --- a/core/dbt/contracts/graph/manifest.py +++ b/core/dbt/contracts/graph/manifest.py @@ -14,7 +14,7 @@ CompileResultNode, ManifestNode, NonSourceCompiledNode, GraphMemberNode ) from dbt.contracts.graph.parsed import ( - ParsedMacro, ParsedDocumentation, ParsedNodePatch, ParsedMacroPatch, + ParsedMacro, ParsedDocumentation, ParsedSourceDefinition, ParsedExposure, HasUniqueID, UnpatchedSourceDefinition, ManifestNodes ) @@ -26,9 +26,7 @@ from dbt.dataclass_schema import dbtClassMixin from dbt.exceptions import ( CompilationException, - raise_duplicate_resource_name, raise_compiler_error, warn_or_error, - raise_duplicate_patch_name, - raise_duplicate_macro_patch_name, raise_duplicate_source_patch_name, + raise_duplicate_resource_name, raise_compiler_error, ) from dbt.helper_types import PathSet from dbt.logger import GLOBAL_LOGGER as logger @@ -718,60 +716,6 @@ def get_resource_fqns(self) -> Mapping[str, PathSet]: resource_fqns[resource_type_plural].add(tuple(resource.fqn)) return resource_fqns - # This is called by 'parse_patch' in the NodePatchParser - def add_patch( - self, source_file: SchemaSourceFile, patch: ParsedNodePatch, - ) -> None: - if patch.yaml_key in ['models', 'seeds', 'snapshots']: - unique_id = self.ref_lookup.get_unique_id(patch.name, None) - elif patch.yaml_key == 'analyses': - unique_id = self.analysis_lookup.get_unique_id(patch.name, None) - else: - raise dbt.exceptions.InternalException( - f'Unexpected yaml_key {patch.yaml_key} for patch in ' - f'file {source_file.path.original_file_path}' - ) - if unique_id is None: - # This will usually happen when a node is disabled - return - - # patches can't be overwritten - node = self.nodes.get(unique_id) - if node: - if node.patch_path: - package_name, existing_file_path = node.patch_path.split('://') - raise_duplicate_patch_name(patch, existing_file_path) - source_file.append_patch(patch.yaml_key, unique_id) - node.patch(patch) - - def add_macro_patch( - self, source_file: SchemaSourceFile, patch: ParsedMacroPatch, - ) -> None: - # macros are fully namespaced - unique_id = f'macro.{patch.package_name}.{patch.name}' - macro = self.macros.get(unique_id) - if not macro: - warn_or_error( - f'WARNING: Found documentation for macro "{patch.name}" ' - f'which was not found' - ) - return - if macro.patch_path: - package_name, existing_file_path = macro.patch_path.split('://') - raise_duplicate_macro_patch_name(patch, existing_file_path) - source_file.macro_patches.append(unique_id) - macro.patch(patch) - - def add_source_patch( - self, source_file: SchemaSourceFile, patch: SourcePatch, - ) -> None: - # source patches must be unique - key = (patch.overrides, patch.name) - if key in self.source_patches: - raise_duplicate_source_patch_name(patch, self.source_patches[key]) - self.source_patches[key] = patch - source_file.source_patches.append(key) - def get_used_schemas(self, resource_types=None): return frozenset({ (node.database, node.schema) for node in diff --git a/core/dbt/contracts/graph/model_config.py b/core/dbt/contracts/graph/model_config.py index f3ae56174c1..3b1b78c2a8d 100644 --- a/core/dbt/contracts/graph/model_config.py +++ b/core/dbt/contracts/graph/model_config.py @@ -239,8 +239,15 @@ def same_contents( return False return True + # This is used in 'add_config_call' to created the combined config_call_dict. + # 'meta' moved here from node + mergebehavior = { + "append": ['pre-hook', 'pre_hook', 'post-hook', 'post_hook', 'tags'], + "update": ['quoting', 'column_types', 'meta'], + } + @classmethod - def _extract_dict( + def _merge_dicts( cls, src: Dict[str, Any], data: Dict[str, Any] ) -> Dict[str, Any]: """Find all the items in data that match a target_field on this class, @@ -286,10 +293,10 @@ def update_from( adapter_config_cls = get_config_class_by_name(adapter_type) - self_merged = self._extract_dict(dct, data) + self_merged = self._merge_dicts(dct, data) dct.update(self_merged) - adapter_merged = adapter_config_cls._extract_dict(dct, data) + adapter_merged = adapter_config_cls._merge_dicts(dct, data) dct.update(adapter_merged) # any remaining fields must be "clobber" @@ -322,6 +329,8 @@ class SourceConfig(BaseConfig): @dataclass class NodeConfig(BaseConfig): + # Note: if any new fields are added with MergeBehavior, also update the + # 'mergebehavior' dictionary enabled: bool = True materialized: str = 'view' persist_docs: Dict[str, Any] = field(default_factory=dict) @@ -370,6 +379,10 @@ class NodeConfig(BaseConfig): ) full_refresh: Optional[bool] = None on_schema_change: Optional[str] = 'ignore' + meta: Dict[str, Any] = field( + default_factory=dict, + metadata=MergeBehavior.Update.meta(), + ) @classmethod def __pre_deserialize__(cls, data): diff --git a/core/dbt/contracts/graph/parsed.py b/core/dbt/contracts/graph/parsed.py index d2dbe48c225..a231274e2db 100644 --- a/core/dbt/contracts/graph/parsed.py +++ b/core/dbt/contracts/graph/parsed.py @@ -155,6 +155,10 @@ def patch(self, patch: 'ParsedNodePatch'): self.columns = patch.columns self.meta = patch.meta self.docs = patch.docs + if patch.config: + pass # for now. reorganizing code... + # we need to re-do the 'update_parsed_node_config' steps, i.e. + # apply dbt_project config, patch config, and model file config if flags.STRICT_MODE: # It seems odd that an instance can be invalid # Maybe there should be validation or restrictions @@ -203,6 +207,7 @@ class ParsedNodeDefaults(ParsedNodeMandatory): deferred: bool = False unrendered_config: Dict[str, Any] = field(default_factory=dict) created_at: int = field(default_factory=lambda: int(time.time())) + config_call_dict: Dict[str, Any] = field(default_factory=dict) def write_node(self, target_path: str, subdirectory: str, payload: str): if (os.path.basename(self.path) == @@ -229,6 +234,11 @@ class ParsedNode(ParsedNodeDefaults, ParsedNodeMixins, SerializableType): def _serialize(self): return self.to_dict() + def __post_serialize__(self, dct): + if 'config_call_dict' in dct: + del dct['config_call_dict'] + return dct + @classmethod def _deserialize(cls, dct: Dict[str, int]): # The serialized ParsedNodes do not differ from each other @@ -456,6 +466,7 @@ class ParsedPatch(HasYamlMetadata, Replaceable): description: str meta: Dict[str, Any] docs: Docs + config: Dict[str, Any] # The parsed node update is only the 'patch', not the test. The test became a diff --git a/core/dbt/contracts/graph/unparsed.py b/core/dbt/contracts/graph/unparsed.py index 477c06fc672..673ca1476c5 100644 --- a/core/dbt/contracts/graph/unparsed.py +++ b/core/dbt/contracts/graph/unparsed.py @@ -126,12 +126,17 @@ def file_id(self): @dataclass -class UnparsedAnalysisUpdate(HasColumnDocs, HasDocs, HasYamlMetadata): +class HasConfig(): + config: Dict[str, Any] = field(default_factory=dict) + + +@dataclass +class UnparsedAnalysisUpdate(HasConfig, HasColumnDocs, HasDocs, HasYamlMetadata): pass @dataclass -class UnparsedNodeUpdate(HasColumnTests, HasTests, HasYamlMetadata): +class UnparsedNodeUpdate(HasConfig, HasColumnTests, HasTests, HasYamlMetadata): quote_columns: Optional[bool] = None @@ -143,7 +148,7 @@ class MacroArgument(dbtClassMixin): @dataclass -class UnparsedMacroUpdate(HasDocs, HasYamlMetadata): +class UnparsedMacroUpdate(HasConfig, HasDocs, HasYamlMetadata): arguments: List[MacroArgument] = field(default_factory=list) @@ -261,6 +266,7 @@ class UnparsedSourceDefinition(dbtClassMixin, Replaceable): loaded_at_field: Optional[str] = None tables: List[UnparsedSourceTableDefinition] = field(default_factory=list) tags: List[str] = field(default_factory=list) + config: Dict[str, Any] = field(default_factory=dict) @property def yaml_key(self) -> 'str': diff --git a/core/dbt/parser/base.py b/core/dbt/parser/base.py index 8b65d2b3b00..716f90d5d44 100644 --- a/core/dbt/parser/base.py +++ b/core/dbt/parser/base.py @@ -256,9 +256,7 @@ def _context_for( parsed_node, self.root_project, self.manifest, config ) - def render_with_context( - self, parsed_node: IntermediateNode, config: ContextConfig - ) -> None: + def render_with_context(self, parsed_node: IntermediateNode, config: ContextConfig): # Given the parsed node and a ContextConfig to use during parsing, # render the node's sql wtih macro capture enabled. # Note: this mutates the config object when config calls are rendered. @@ -273,11 +271,12 @@ def render_with_context( get_rendered( parsed_node.raw_sql, context, parsed_node, capture_macros=True ) + return context # This is taking the original config for the node, converting it to a dict, # updating the config with new config passed in, then re-creating the # config from the dict in the node. - def update_parsed_node_config( + def update_parsed_node_config_dict( self, parsed_node: IntermediateNode, config_dict: Dict[str, Any] ) -> None: # Overwrite node config @@ -294,26 +293,39 @@ def update_parsed_node_name( self._update_node_schema(parsed_node, config_dict) self._update_node_alias(parsed_node, config_dict) - def update_parsed_node( - self, parsed_node: IntermediateNode, config: ContextConfig + def update_parsed_node_config( + self, parsed_node: IntermediateNode, config: ContextConfig, + context=None, patch_config_dict=None ) -> None: """Given the ContextConfig used for parsing and the parsed node, generate and set the true values to use, overriding the temporary parse values set in _build_intermediate_parsed_node. """ - config_dict = config.build_config_dict() - # Set tags on node provided in config blocks + # build_config_dict takes the config_call_dict in the ContextConfig object + # and calls calculate_node_config to combine dbt_project configs and + # config calls from SQL files + config_dict = config.build_config_dict(patch_config_dict=patch_config_dict) + + # Set tags on node provided in config blocks. Tags are additive, so even if + # config has been built before, we don't have to reset tags in the parsed_node. model_tags = config_dict.get('tags', []) - parsed_node.tags.extend(model_tags) + for tag in model_tags: + if tag not in parsed_node.tags: + parsed_node.tags.append(tag) + # unrendered_config is used to compare the original database/schema/alias + # values and to handle 'same_config' and 'same_contents' calls parsed_node.unrendered_config = config.build_config_dict( rendered=False ) + parsed_node.config_call_dict = config._config_call_dict + # do this once before we parse the node database/schema/alias, so # parsed_node.config is what it would be if they did nothing - self.update_parsed_node_config(parsed_node, config_dict) + self.update_parsed_node_config_dict(parsed_node, config_dict) + # This updates the node database/schema/alias self.update_parsed_node_name(parsed_node, config_dict) # at this point, we've collected our hooks. Use the node context to @@ -323,9 +335,8 @@ def update_parsed_node( # skip context rebuilding if there aren't any hooks if not hooks: return - # we could cache the original context from parsing this node. Is that - # worth the cost in memory/complexity? - context = self._context_for(parsed_node, config) + if not context: + context = self._context_for(parsed_node, config) for hook in hooks: get_rendered(hook.sql, context, parsed_node, capture_macros=True) @@ -357,8 +368,8 @@ def render_update( self, node: IntermediateNode, config: ContextConfig ) -> None: try: - self.render_with_context(node, config) - self.update_parsed_node(node, config) + context = self.render_with_context(node, config) + self.update_parsed_node_config(node, config, context=context) except ValidationError as exc: # we got a ValidationError - probably bad types in config() msg = validator_error_message(exc) diff --git a/core/dbt/parser/models.py b/core/dbt/parser/models.py index 37f00d16396..f941cb88a15 100644 --- a/core/dbt/parser/models.py +++ b/core/dbt/parser/models.py @@ -7,9 +7,8 @@ import dbt.tracking as tracking from dbt import utils from dbt_extractor import ExtractionError, py_extract_from_source # type: ignore -import itertools import random -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, List class ModelParser(SimpleSQLParser[ParsedModelNode]): @@ -40,9 +39,9 @@ def render_update( experimentally_parsed: Dict[str, List[Any]] = py_extract_from_source(node.raw_sql) # second config format - config_calls: List[Dict[str, str]] = [] + config_call_dict: Dict[str, str] = {} for c in experimentally_parsed['configs']: - config_calls.append({c[0]: c[1]}) + ContextConfig._add_config_call(config_call_dict, {c[0]: c[1]}) # format sources TODO change extractor to match this type source_calls: List[List[str]] = [] @@ -64,22 +63,15 @@ def render_update( if isinstance(experimentally_parsed, Exception): result += ["01_experimental_parser_cannot_parse"] else: - # rearrange existing configs to match: - real_configs: List[Tuple[str, Any]] = list( - itertools.chain.from_iterable( - map(lambda x: x.items(), config._config_calls) - ) - ) - # look for false positive configs - for c in experimentally_parsed['configs']: - if c not in real_configs: + for k in config_call_dict.keys(): + if k not in config._config_call_dict: result += ["02_false_positive_config_value"] break # look for missed configs - for c in real_configs: - if c not in experimentally_parsed['configs']: + for k in config._config_call_dict.keys(): + if k not in config_call_dict: result += ["03_missed_config_value"] break @@ -127,12 +119,12 @@ def render_update( # since it doesn't need python jinja, fit the refs, sources, and configs # into the node. Down the line the rest of the node will be updated with # this information. (e.g. depends_on etc.) - config._config_calls = config_calls + config._config_call_dict = config_call_dict # this uses the updated config to set all the right things in the node. # if there are hooks present, it WILL render jinja. Will need to change # when the experimental parser supports hooks - self.update_parsed_node(node, config) + self.update_parsed_node_config(node, config) # update the unrendered config with values from the file. # values from yaml files are in there already diff --git a/core/dbt/parser/schema_test_builders.py b/core/dbt/parser/schema_test_builders.py index 87a279fc4d0..13c41273fda 100644 --- a/core/dbt/parser/schema_test_builders.py +++ b/core/dbt/parser/schema_test_builders.py @@ -320,6 +320,28 @@ def error_if(self) -> Optional[str]: def fail_calc(self) -> Optional[str]: return self.modifiers.get('fail_calc') + def get_static_config(self): + config = {} + if self.alias is not None: + config['alias'] = self.alias + if self.severity is not None: + config['severity'] = self.severity + if self.enabled is not None: + config['enabled'] = self.enabled + if self.where is not None: + config['where'] = self.where + if self.limit is not None: + config['limit'] = self.limit + if self.warn_if is not None: + config['warn_if'] = self.warn_if + if self.error_if is not None: + config['error_id'] = self.error_if + if self.fail_calc is not None: + config['fail_calc'] = self.fail_calc + if self.store_failures is not None: + config['store_failures'] = self.store_failures + return config + def tags(self) -> List[str]: tags = self.modifiers.get('tags', []) if isinstance(tags, str): diff --git a/core/dbt/parser/schemas.py b/core/dbt/parser/schemas.py index fe73992cb6a..5280a63eb5a 100644 --- a/core/dbt/parser/schemas.py +++ b/core/dbt/parser/schemas.py @@ -22,8 +22,7 @@ generate_parse_exposure, generate_test_context ) from dbt.context.macro_resolver import MacroResolver -from dbt.contracts.files import FileHash -from dbt.contracts.graph.manifest import SchemaSourceFile +from dbt.contracts.files import FileHash, SchemaSourceFile from dbt.contracts.graph.parsed import ( ParsedNodePatch, ColumnInfo, @@ -47,7 +46,10 @@ from dbt.exceptions import ( validator_error_message, JSONValidationException, raise_invalid_schema_yml_version, ValidationException, - CompilationException, + CompilationException, raise_duplicate_patch_name, + raise_duplicate_macro_patch_name, InternalException, + raise_duplicate_source_patch_name, + warn_or_error, ) from dbt.node_types import NodeType from dbt.parser.base import SimpleParser @@ -318,7 +320,7 @@ def _parse_generic_test( # is not necessarily this package's name fqn = self.get_fqn(fqn_path, builder.fqn_name) - # this is the config that is used in render_update + # this is the ContextConfig that is used in render_update config = self.initial_config(fqn) metadata = { @@ -360,37 +362,10 @@ def render_test_update(self, node, config, builder): node.depends_on.add_macro(macro_unique_id) if (macro_unique_id in ['macro.dbt.test_not_null', 'macro.dbt.test_unique']): - self.update_parsed_node(node, config) - # manually set configs - # note: this does not respect generate_alias_name() macro - if builder.alias is not None: - node.unrendered_config['alias'] = builder.alias - node.config['alias'] = builder.alias - node.alias = builder.alias - if builder.severity is not None: - node.unrendered_config['severity'] = builder.severity - node.config['severity'] = builder.severity - if builder.enabled is not None: - node.unrendered_config['enabled'] = builder.enabled - node.config['enabled'] = builder.enabled - if builder.where is not None: - node.unrendered_config['where'] = builder.where - node.config['where'] = builder.where - if builder.limit is not None: - node.unrendered_config['limit'] = builder.limit - node.config['limit'] = builder.limit - if builder.warn_if is not None: - node.unrendered_config['warn_if'] = builder.warn_if - node.config['warn_if'] = builder.warn_if - if builder.error_if is not None: - node.unrendered_config['error_if'] = builder.error_if - node.config['error_if'] = builder.error_if - if builder.fail_calc is not None: - node.unrendered_config['fail_calc'] = builder.fail_calc - node.config['fail_calc'] = builder.fail_calc - if builder.store_failures is not None: - node.unrendered_config['store_failures'] = builder.store_failures - node.config['store_failures'] = builder.store_failures + config_call_dict = builder.get_static_config() + config._config_call_dict = config_call_dict + # This sets the config from dbt_project + self.update_parsed_node_config(node, config) # source node tests are processed at patch_source time if isinstance(builder.target, UnpatchedSourceDefinition): sources = [builder.target.fqn[-2], builder.target.fqn[-1]] @@ -410,7 +385,7 @@ def render_test_update(self, node, config, builder): get_rendered( node.raw_sql, context, node, capture_macros=True ) - self.update_parsed_node(node, config) + self.update_parsed_node_config(node, config) except ValidationError as exc: # we got a ValidationError - probably bad types in config() msg = validator_error_message(exc) @@ -678,7 +653,14 @@ def parse(self) -> List[TestBlock]: if is_override: data['path'] = self.yaml.path.original_file_path patch = self._target_from_dict(SourcePatch, data) - self.manifest.add_source_patch(self.yaml.file, patch) + assert isinstance(self.yaml.file, SchemaSourceFile) + source_file = self.yaml.file + # source patches must be unique + key = (patch.overrides, patch.name) + if key in self.manifest.source_patches: + raise_duplicate_source_patch_name(patch, self.manifest.source_patches[key]) + self.manifest.source_patches[key] = patch + source_file.source_patches.append(key) else: source = self._target_from_dict(UnparsedSourceDefinition, data) self.add_source_definitions(source) @@ -782,6 +764,19 @@ def get_unparsed_target(self) -> Iterable[NonSourceTarget]: else: yield node + def patch_node_config(self, node, patch): + # Get the ContextConfig that's used in calculating the config + # This must match the model resource_type that's being patched + config = ContextConfig( + self.schema_parser.root_project, + node.fqn, + node.resource_type, + self.schema_parser.project.project_name, + ) + # We need to re-apply the config_call_dict after the patch config + config._config_call_dict = node.config_call_dict + self.schema_parser.update_parsed_node_config(node, config, patch_config_dict=patch.config) + class NodePatchParser( NonSourceParser[NodeTarget, ParsedNodePatch], @@ -790,6 +785,9 @@ class NodePatchParser( def parse_patch( self, block: TargetBlock[NodeTarget], refs: ParserRef ) -> None: + # We're not passing the ParsedNodePatch around anymore, so we + # could possibly skip creating one. Leaving here for now for + # code consistency. patch = ParsedNodePatch( name=block.target.name, original_file_path=block.target.original_file_path, @@ -799,8 +797,35 @@ def parse_patch( columns=refs.column_info, meta=block.target.meta, docs=block.target.docs, + config=block.target.config, ) - self.manifest.add_patch(self.yaml.file, patch) + assert isinstance(self.yaml.file, SchemaSourceFile) + source_file: SchemaSourceFile = self.yaml.file + if patch.yaml_key in ['models', 'seeds', 'snapshots']: + unique_id = self.manifest.ref_lookup.get_unique_id(patch.name, None) + elif patch.yaml_key == 'analyses': + unique_id = self.manifest.analysis_lookup.get_unique_id(patch.name, None) + else: + raise InternalException( + f'Unexpected yaml_key {patch.yaml_key} for patch in ' + f'file {source_file.path.original_file_path}' + ) + if unique_id is None: + # This will usually happen when a node is disabled + return + + # patches can't be overwritten + node = self.manifest.nodes.get(unique_id) + if node: + if node.patch_path: + package_name, existing_file_path = node.patch_path.split('://') + raise_duplicate_patch_name(patch, existing_file_path) + source_file.append_patch(patch.yaml_key, unique_id) + # If this patch has config changes, re-calculate the node config + # with the patch config + if patch.config: + self.patch_node_config(node, patch) + node.patch(patch) class TestablePatchParser(NodePatchParser[UnparsedNodeUpdate]): @@ -838,8 +863,24 @@ def parse_patch( description=block.target.description, meta=block.target.meta, docs=block.target.docs, + config=block.target.config, ) - self.manifest.add_macro_patch(self.yaml.file, patch) + assert isinstance(self.yaml.file, SchemaSourceFile) + source_file = self.yaml.file + # macros are fully namespaced + unique_id = f'macro.{patch.package_name}.{patch.name}' + macro = self.manifest.macros.get(unique_id) + if not macro: + warn_or_error( + f'WARNING: Found patch for macro "{patch.name}" ' + f'which was not found' + ) + return + if macro.patch_path: + package_name, existing_file_path = macro.patch_path.split('://') + raise_duplicate_macro_patch_name(patch, existing_file_path) + source_file.macro_patches.append(unique_id) + macro.patch(patch) class ExposureParser(YamlReader): diff --git a/core/dbt/parser/sources.py b/core/dbt/parser/sources.py index a3784e21868..035e345f1cb 100644 --- a/core/dbt/parser/sources.py +++ b/core/dbt/parser/sources.py @@ -286,7 +286,7 @@ def _generate_source_config(self, fqn: List[str], rendered: bool, project_name: ) return generator.calculate_node_config( - config_calls=[], + config_call_dict={}, fqn=fqn, resource_type=NodeType.Source, project_name=project_name, diff --git a/test/integration/029_docs_generate_tests/test_docs_generate.py b/test/integration/029_docs_generate_tests/test_docs_generate.py index eb2fb1cd51f..695859004b4 100644 --- a/test/integration/029_docs_generate_tests/test_docs_generate.py +++ b/test/integration/029_docs_generate_tests/test_docs_generate.py @@ -910,6 +910,7 @@ def rendered_model_config(self, **updates): 'persist_docs': {}, 'full_refresh': None, 'on_schema_change': 'ignore', + 'meta': {}, } result.update(updates) return result @@ -934,6 +935,7 @@ def rendered_seed_config(self, **updates): 'database': None, 'schema': None, 'alias': None, + 'meta': {}, } result.update(updates) return result @@ -963,6 +965,7 @@ def rendered_snapshot_config(self, **updates): 'check_cols': 'all', 'unique_key': 'id', 'target_schema': None, + 'meta': {}, } result.update(updates) return result @@ -1000,6 +1003,7 @@ def rendered_tst_config(self, **updates): 'database': None, 'schema': 'dbt_test__audit', 'alias': None, + 'meta': {}, } result.update(updates) return result diff --git a/test/integration/033_event_tracking_test/test_events.py b/test/integration/033_event_tracking_test/test_events.py index 867e474136d..a3a4707d205 100644 --- a/test/integration/033_event_tracking_test/test_events.py +++ b/test/integration/033_event_tracking_test/test_events.py @@ -684,7 +684,7 @@ def test__postgres_event_tracking_tests(self): test_result_B = self.run_event_test( ["test"], - expected_calls_A, + expected_calls_B, expected_contexts, expect_pass=False ) diff --git a/test/integration/039_config_test/data-alt/some_seed.csv b/test/integration/039_config_test/data-alt/some_seed.csv new file mode 100644 index 00000000000..83f9676727c --- /dev/null +++ b/test/integration/039_config_test/data-alt/some_seed.csv @@ -0,0 +1,2 @@ +id,value +4,2 diff --git a/test/integration/039_config_test/macros-alt/my_macro.sql b/test/integration/039_config_test/macros-alt/my_macro.sql new file mode 100644 index 00000000000..79c941d7518 --- /dev/null +++ b/test/integration/039_config_test/macros-alt/my_macro.sql @@ -0,0 +1,8 @@ +{% macro do_something2(foo2, bar2) %} + + select + '{{ foo2 }}' as foo2, + '{{ bar2 }}' as bar2 + +{% endmacro %} + diff --git a/test/integration/039_config_test/macros-alt/schema.yml b/test/integration/039_config_test/macros-alt/schema.yml new file mode 100644 index 00000000000..317c9a0546e --- /dev/null +++ b/test/integration/039_config_test/macros-alt/schema.yml @@ -0,0 +1,5 @@ +macros: + - name: my_macro + config: + meta: + owner: 'Joe Jones' diff --git a/test/integration/039_config_test/models-alt/schema.yml b/test/integration/039_config_test/models-alt/schema.yml new file mode 100644 index 00000000000..6b7a3ceb503 --- /dev/null +++ b/test/integration/039_config_test/models-alt/schema.yml @@ -0,0 +1,18 @@ +version: 2 +sources: + - name: raw + database: "{{ target.database }}" + schema: "{{ target.schema }}" + tables: + - name: 'some_seed' + columns: + - name: id + +models: + - name: model + description: "This is a model description" + config: + tags: ['tag_in_schema'] + meta: + owner: 'Julie Smith' + materialization: view diff --git a/test/integration/039_config_test/models-alt/tagged/model.sql b/test/integration/039_config_test/models-alt/tagged/model.sql new file mode 100644 index 00000000000..6dbc83c3d6e --- /dev/null +++ b/test/integration/039_config_test/models-alt/tagged/model.sql @@ -0,0 +1,15 @@ +{{ + config( + materialized='view', + tags=['tag_1_in_model'], + ) +}} + +{{ + config( + materialized='table', + tags=['tag_2_in_model'], + ) +}} + +select 4 as id, 2 as value diff --git a/test/integration/039_config_test/models-alt/untagged.sql b/test/integration/039_config_test/models-alt/untagged.sql new file mode 100644 index 00000000000..a0dce3f086f --- /dev/null +++ b/test/integration/039_config_test/models-alt/untagged.sql @@ -0,0 +1,5 @@ +{{ + config(materialized='table') +}} + +select id, value from {{ source('raw', 'some_seed') }} diff --git a/test/integration/039_config_test/test_configs_in_schema_files.py b/test/integration/039_config_test/test_configs_in_schema_files.py new file mode 100644 index 00000000000..adfc18b69de --- /dev/null +++ b/test/integration/039_config_test/test_configs_in_schema_files.py @@ -0,0 +1,82 @@ +import os +import shutil + +from test.integration.base import DBTIntegrationTest, use_profile, get_manifest +from dbt.exceptions import CompilationException + + +class TestSchemaFileConfigs(DBTIntegrationTest): + @property + def schema(self): + return "config_039-alt" + + def unique_schema(self): + return super().unique_schema().upper() + + @property + def project_config(self): + return { + 'config-version': 2, + 'data-paths': ['data-alt'], + 'models': { + '+meta': { + 'company': 'NuMade', + }, + 'test': { + '+meta': { + 'project': 'test', + }, + 'tagged': { + '+meta': { + 'team': 'Core Team', + }, + 'tags': ['tag_in_project'], + 'model': { + 'materialized': 'table', + '+meta': { + 'owner': 'Julie Dent', + }, + } + } + }, + }, + 'seeds': { + 'quote_columns': False, + }, + } + + @property + def models(self): + return "models-alt" + + @use_profile('postgres') + def test_postgres_config_layering(self): + self.assertEqual(len(self.run_dbt(['seed'])), 1) + # test the project-level tag, and both config() call tags + self.assertEqual(len(self.run_dbt(['run', '--model', 'tag:tag_in_project'])), 1) + self.assertEqual(len(self.run_dbt(['run', '--model', 'tag:tag_1_in_model'])), 1) + self.assertEqual(len(self.run_dbt(['run', '--model', 'tag:tag_2_in_model'])), 1) + self.assertEqual(len(self.run_dbt(['run', '--model', 'tag:tag_in_schema'])), 1) + manifest = get_manifest() + model_id = 'model.test.model' + model_node = manifest.nodes[model_id] + model_tags = ['tag_1_in_model', 'tag_2_in_model', 'tag_in_project', 'tag_in_schema'] + model_node_tags = model_node.tags.copy() + model_node_tags.sort() + self.assertEqual(model_node_tags, model_tags) + model_node_config_tags = model_node.config.tags.copy() + model_node_config_tags.sort() + self.assertEqual(model_node_config_tags, model_tags) + model_meta = { + 'company': 'NuMade', + 'project': 'test', + 'team': 'Core Team', + 'owner': 'Julie Smith', + } + self.assertEqual(model_node.config.meta, model_meta) + # make sure we overwrote the materialization properly + models = self.get_models_in_schema() + self.assertEqual(models['model'], 'table') + self.assertTablesEqual('some_seed', 'model') + + diff --git a/test/integration/047_dbt_ls_test/test_ls.py b/test/integration/047_dbt_ls_test/test_ls.py index d7fb1a05593..195ccf90f28 100644 --- a/test/integration/047_dbt_ls_test/test_ls.py +++ b/test/integration/047_dbt_ls_test/test_ls.py @@ -95,6 +95,7 @@ def expect_snapshot_output(self): 'alias': None, 'check_cols': None, 'on_schema_change': 'ignore', + 'meta': {}, }, 'unique_id': 'snapshot.test.my_snapshot', 'original_file_path': normalize('snapshots/snapshot.sql'), @@ -129,6 +130,7 @@ def expect_analyses_output(self): 'database': None, 'schema': None, 'alias': None, + 'meta': {}, }, 'unique_id': 'analysis.test.a', 'original_file_path': normalize('analyses/a.sql'), @@ -164,6 +166,7 @@ def expect_model_output(self): 'database': None, 'schema': None, 'alias': None, + 'meta': {}, }, 'original_file_path': normalize('models/ephemeral.sql'), 'unique_id': 'model.test.ephemeral', @@ -191,6 +194,7 @@ def expect_model_output(self): 'database': None, 'schema': None, 'alias': None, + 'meta': {}, }, 'original_file_path': normalize('models/incremental.sql'), 'unique_id': 'model.test.incremental', @@ -217,6 +221,7 @@ def expect_model_output(self): 'database': None, 'schema': None, 'alias': None, + 'meta': {}, }, 'original_file_path': normalize('models/sub/inner.sql'), 'unique_id': 'model.test.inner', @@ -243,6 +248,7 @@ def expect_model_output(self): 'database': None, 'schema': None, 'alias': None, + 'meta': {}, }, 'original_file_path': normalize('models/outer.sql'), 'unique_id': 'model.test.outer', @@ -280,6 +286,7 @@ def expect_model_ephemeral_output(self): 'database': None, 'schema': None, 'alias': None, + 'meta': {}, }, 'unique_id': 'model.test.ephemeral', 'original_file_path': normalize('models/ephemeral.sql'), @@ -338,6 +345,7 @@ def expect_seed_output(self): 'database': None, 'schema': None, 'alias': None, + 'meta': {}, }, 'unique_id': 'seed.test.seed', 'original_file_path': normalize('data/seed.csv'), @@ -380,6 +388,7 @@ def expect_test_output(self): 'database': None, 'schema': 'dbt_test__audit', 'alias': None, + 'meta': {}, }, 'unique_id': 'test.test.not_null_outer_id.e5db1d4aad', 'original_file_path': normalize('models/schema.yml'), @@ -413,6 +422,7 @@ def expect_test_output(self): 'database': None, 'schema': 'dbt_test__audit', 'alias': None, + 'meta': {}, }, 'unique_id': 'test.test.t', 'original_file_path': normalize('tests/t.sql'), @@ -446,6 +456,7 @@ def expect_test_output(self): 'database': None, 'schema': 'dbt_test__audit', 'alias': None, + 'meta': {}, }, 'unique_id': 'test.test.unique_outer_id.615b011076', 'original_file_path': normalize('models/schema.yml'), diff --git a/test/integration/068_partial_parsing_tests/test_partial_parsing.py b/test/integration/068_partial_parsing_tests/test_partial_parsing.py index ffd3a4c4455..e6996c0e351 100644 --- a/test/integration/068_partial_parsing_tests/test_partial_parsing.py +++ b/test/integration/068_partial_parsing_tests/test_partial_parsing.py @@ -59,7 +59,7 @@ def test_postgres_pp_models(self): results = self.run_dbt(["--partial-parse", "test"], expect_pass=False) self.assertEqual(len(results), 1) manifest = get_manifest() - self.assertEqual(len(manifest.files), 33) + self.assertEqual(len(manifest.files), 34) model_3_file_id = 'test://' + normalize('models-a/model_three.sql') self.assertIn(model_3_file_id, manifest.files) model_three_file = manifest.files[model_3_file_id] diff --git a/test/integration/base.py b/test/integration/base.py index 463948de4b8..27e5333b43f 100644 --- a/test/integration/base.py +++ b/test/integration/base.py @@ -24,6 +24,7 @@ from dbt.config import RuntimeConfig from dbt.context import providers from dbt.logger import GLOBAL_LOGGER as logger, log_manager +from dbt.contracts.graph.manifest import Manifest INITIAL_ROOT = os.getcwd() @@ -1224,9 +1225,22 @@ def __eq__(self, other): def __repr__(self): return 'AnyStringWith<{!r}>'.format(self.contains) + def bigquery_rate_limiter(err, *args): msg = str(err) if 'too many table update operations for this table' in msg: time.sleep(1) return True return False + + +def get_manifest(): + path = './target/partial_parse.msgpack' + if os.path.exists(path): + with open(path, 'rb') as fp: + manifest_mp = fp.read() + manifest: Manifest = Manifest.from_msgpack(manifest_mp) + return manifest + else: + return None + diff --git a/test/unit/test_contracts_graph_compiled.py b/test/unit/test_contracts_graph_compiled.py index 041fae3f3cf..550f7e88971 100644 --- a/test/unit/test_contracts_graph_compiled.py +++ b/test/unit/test_contracts_graph_compiled.py @@ -137,7 +137,8 @@ def basic_uncompiled_dict(): 'quoting': {}, 'tags': [], 'vars': {}, - 'on_schema_change': 'ignore' + 'on_schema_change': 'ignore', + 'meta': {}, }, 'docs': {'show': True}, 'columns': {}, @@ -182,7 +183,8 @@ def basic_compiled_dict(): 'quoting': {}, 'tags': [], 'vars': {}, - 'on_schema_change': 'ignore' + 'on_schema_change': 'ignore', + 'meta': {}, }, 'docs': {'show': True}, 'columns': {}, @@ -440,6 +442,7 @@ def basic_uncompiled_schema_test_dict(): 'error_if': '!= 0', 'fail_calc': 'count(*)', 'on_schema_change': 'ignore', + 'meta': {}, }, 'deferred': False, 'docs': {'show': True}, @@ -495,6 +498,7 @@ def basic_compiled_schema_test_dict(): 'error_if': '!= 0', 'fail_calc': 'count(*)', 'on_schema_change': 'ignore', + 'meta': {}, }, 'docs': {'show': True}, 'columns': {}, diff --git a/test/unit/test_contracts_graph_parsed.py b/test/unit/test_contracts_graph_parsed.py index 38a539ae4eb..ed6521f6446 100644 --- a/test/unit/test_contracts_graph_parsed.py +++ b/test/unit/test_contracts_graph_parsed.py @@ -77,6 +77,7 @@ def populated_node_config_dict(): 'vars': {}, 'extra': 'even more', 'on_schema_change': 'ignore', + 'meta': {}, } @@ -155,7 +156,8 @@ def base_parsed_model_dict(): 'quoting': {}, 'tags': [], 'vars': {}, - 'on_schema_change': 'ignore' + 'on_schema_change': 'ignore', + 'meta': {}, }, 'deferred': False, 'docs': {'show': True}, @@ -246,7 +248,8 @@ def complex_parsed_model_dict(): 'quoting': {}, 'tags': [], 'vars': {'foo': 100}, - 'on_schema_change': 'ignore' + 'on_schema_change': 'ignore', + 'meta': {}, }, 'docs': {'show': True}, 'columns': { @@ -442,7 +445,8 @@ def basic_parsed_seed_dict(): 'quoting': {}, 'tags': [], 'vars': {}, - 'on_schema_change': 'ignore' + 'on_schema_change': 'ignore', + 'meta': {}, }, 'deferred': False, 'docs': {'show': True}, @@ -536,7 +540,8 @@ def complex_parsed_seed_dict(): 'tags': [], 'vars': {}, 'quote_columns': True, - 'on_schema_change': 'ignore' + 'on_schema_change': 'ignore', + 'meta': {}, }, 'deferred': False, 'docs': {'show': True}, @@ -678,6 +683,7 @@ def basic_parsed_model_patch_dict(): 'tags': [], }, }, + 'config': {}, } @@ -692,6 +698,7 @@ def basic_parsed_model_patch_object(): columns={'a': ColumnInfo(name='a', description='a text field', meta={})}, docs=Docs(), meta={}, + config={}, ) @@ -789,7 +796,8 @@ def base_parsed_hook_dict(): 'quoting': {}, 'tags': [], 'vars': {}, - 'on_schema_change': 'ignore' + 'on_schema_change': 'ignore', + 'meta': {}, }, 'docs': {'show': True}, 'columns': {}, @@ -860,7 +868,8 @@ def complex_parsed_hook_dict(): 'quoting': {}, 'tags': [], 'vars': {}, - 'on_schema_change': 'ignore' + 'on_schema_change': 'ignore', + 'meta': {}, }, 'docs': {'show': True}, 'columns': { @@ -1009,6 +1018,7 @@ def basic_parsed_schema_test_dict(): 'error_if': '!= 0', 'fail_calc': 'count(*)', 'on_schema_change': 'ignore', + 'meta': {}, }, 'docs': {'show': True}, 'columns': {}, @@ -1088,6 +1098,7 @@ def complex_parsed_schema_test_dict(): 'fail_calc': 'count(*)', 'extra_key': 'extra value', 'on_schema_change': 'ignore', + 'meta': {}, }, 'docs': {'show': False}, 'columns': { @@ -1208,6 +1219,7 @@ def basic_timestamp_snapshot_config_dict(): 'target_database': 'some_snapshot_db', 'target_schema': 'some_snapshot_schema', 'on_schema_change': 'ignore', + 'meta': {}, } @@ -1241,6 +1253,7 @@ def complex_timestamp_snapshot_config_dict(): 'strategy': 'timestamp', 'updated_at': 'last_update', 'on_schema_change': 'ignore', + 'meta': {}, } @@ -1298,6 +1311,7 @@ def basic_check_snapshot_config_dict(): 'strategy': 'check', 'check_cols': 'all', 'on_schema_change': 'ignore', + 'meta': {}, } @@ -1331,6 +1345,7 @@ def complex_set_snapshot_config_dict(): 'strategy': 'check', 'check_cols': ['a', 'b'], 'on_schema_change': 'ignore', + 'meta': {}, } @@ -1436,7 +1451,8 @@ def basic_timestamp_snapshot_dict(): 'unique_key': 'id', 'strategy': 'timestamp', 'updated_at': 'last_update', - 'on_schema_change': 'ignore' + 'on_schema_change': 'ignore', + 'meta': {}, }, 'docs': {'show': True}, 'columns': {}, @@ -1568,7 +1584,8 @@ def basic_check_snapshot_dict(): 'unique_key': 'id', 'strategy': 'check', 'check_cols': 'all', - 'on_schema_change': 'ignore' + 'on_schema_change': 'ignore', + 'meta': {}, }, 'docs': {'show': True}, 'columns': {}, @@ -1717,6 +1734,7 @@ def populated_parsed_node_patch_dict(): 'meta': {'key': ['value']}, 'yaml_key': 'models', 'package_name': 'test', + 'config': {}, } @@ -1731,6 +1749,7 @@ def populated_parsed_node_patch_object(): yaml_key='models', package_name='test', docs=Docs(show=False), + config={}, ) diff --git a/test/unit/test_contracts_graph_unparsed.py b/test/unit/test_contracts_graph_unparsed.py index a15ba1e16c6..26b2bc3b947 100644 --- a/test/unit/test_contracts_graph_unparsed.py +++ b/test/unit/test_contracts_graph_unparsed.py @@ -258,6 +258,7 @@ def test_defaults(self): 'loader': '', 'meta': {}, 'tags': [], + 'config': {}, } self.assert_from_dict(minimum, from_dict) self.assert_to_dict(minimum, to_dict) @@ -281,6 +282,7 @@ def test_contents(self): 'tables': [], 'meta': {}, 'tags': [], + 'config': {}, } self.assert_symmetric(empty, dct) @@ -338,6 +340,7 @@ def test_table_defaults(self): }, ], 'tags': [], + 'config': {}, } self.assert_from_dict(source, from_dict) self.assert_symmetric(source, to_dict) @@ -406,6 +409,7 @@ def test_defaults(self): 'docs': {'show': True}, 'tests': [], 'meta': {}, + 'config': {}, } self.assert_from_dict(minimum, from_dict) self.assert_to_dict(minimum, to_dict) @@ -468,6 +472,7 @@ def test_contents(self): }, ], 'docs': {'show': False}, + 'config': {}, } self.assert_symmetric(update, dct) pickle.loads(pickle.dumps(update))