import codecs import linecache import os import re import tempfile import threading from ast import literal_eval from contextlib import contextmanager from itertools import chain, islice from typing import ( List, Union, Set, Optional, Dict, Any, Iterator, Type, NoReturn, Tuple, Callable ) import jinja2 import jinja2.ext import jinja2.nativetypes # type: ignore import jinja2.nodes import jinja2.parser import jinja2.sandbox from dbt.utils import ( get_dbt_macro_name, get_docs_macro_name, get_materialization_macro_name, get_test_macro_name, deep_map ) from dbt.clients._jinja_blocks import BlockIterator, BlockData, BlockTag from dbt.contracts.graph.compiled import CompiledSchemaTestNode from dbt.contracts.graph.parsed import ParsedSchemaTestNode from dbt.exceptions import ( InternalException, raise_compiler_error, CompilationException, invalid_materialization_argument, MacroReturn, JinjaRenderingException, UndefinedMacroException ) from dbt import flags from dbt.logger import GLOBAL_LOGGER as logger # noqa def _linecache_inject(source, write): if write: # this is the only reliable way to accomplish this. Obviously, it's # really darn noisy and will fill your temporary directory tmp_file = tempfile.NamedTemporaryFile( prefix='dbt-macro-compiled-', suffix='.py', delete=False, mode='w+', encoding='utf-8', ) tmp_file.write(source) filename = tmp_file.name else: # `codecs.encode` actually takes a `bytes` as the first argument if # the second argument is 'hex' - mypy does not know this. rnd = codecs.encode(os.urandom(12), 'hex') # type: ignore filename = rnd.decode('ascii') # put ourselves in the cache cache_entry = ( len(source), None, [line + '\n' for line in source.splitlines()], filename ) # linecache does in fact have an attribute `cache`, thanks linecache.cache[filename] = cache_entry # type: ignore return filename class MacroFuzzParser(jinja2.parser.Parser): def parse_macro(self): node = jinja2.nodes.Macro(lineno=next(self.stream).lineno) # modified to fuzz macros defined in the same file. this way # dbt can understand the stack of macros being called. # - @cmcarthur node.name = get_dbt_macro_name( self.parse_assign_target(name_only=True).name) self.parse_signature(node) node.body = self.parse_statements(('name:endmacro',), drop_needle=True) return node class MacroFuzzEnvironment(jinja2.sandbox.SandboxedEnvironment): def _parse(self, source, name, filename): return MacroFuzzParser(self, source, name, filename).parse() def _compile(self, source, filename): """Override jinja's compilation to stash the rendered source inside the python linecache for debugging when the appropriate environment variable is set. If the value is 'write', also write the files to disk. WARNING: This can write a ton of data if you aren't careful. """ if filename == '