diff --git a/.changes/unreleased/Features-20220908-123650.yaml b/.changes/unreleased/Features-20220908-123650.yaml new file mode 100644 index 00000000000..a68f1d55b23 --- /dev/null +++ b/.changes/unreleased/Features-20220908-123650.yaml @@ -0,0 +1,7 @@ +kind: Features +body: Flags work with new Click CLI +time: 2022-09-08T12:36:50.386978-05:00 +custom: + Author: iknox-fa + Issue: "5529" + PR: "5790" diff --git a/core/dbt/cli/flags.py b/core/dbt/cli/flags.py new file mode 100644 index 00000000000..3593a69de84 --- /dev/null +++ b/core/dbt/cli/flags.py @@ -0,0 +1,44 @@ +# TODO Move this to /core/dbt/flags.py when we're ready to break things +import os +from dataclasses import dataclass +from multiprocessing import get_context +from pprint import pformat as pf + +from click import get_current_context + +if os.name != "nt": + # https://bugs.python.org/issue41567 + import multiprocessing.popen_spawn_posix # type: ignore # noqa: F401 + + +@dataclass(frozen=True) +class Flags: + def __init__(self, ctx=None) -> None: + + if ctx is None: + ctx = get_current_context() + + def assign_params(ctx): + """Recursively adds all click params to flag object""" + for param_name, param_value in ctx.params.items(): + # N.B. You have to use the base MRO method (object.__setattr__) to set attributes + # when using frozen dataclasses. + # https://docs.python.org/3/library/dataclasses.html#frozen-instances + if hasattr(self, param_name): + raise Exception(f"Duplicate flag names found in click command: {param_name}") + object.__setattr__(self, param_name.upper(), param_value) + if ctx.parent: + assign_params(ctx.parent) + + assign_params(ctx) + + # Hard coded flags + object.__setattr__(self, "WHICH", ctx.info_name) + object.__setattr__(self, "MP_CONTEXT", get_context("spawn")) + + # Support console DO NOT TRACK initiave + if os.getenv("DO_NOT_TRACK", "").lower() in (1, "t", "true", "y", "yes"): + object.__setattr__(self, "ANONYMOUS_USAGE_STATS", False) + + def __str__(self) -> str: + return str(pf(self.__dict__)) diff --git a/core/dbt/cli/main.py b/core/dbt/cli/main.py index 91accb3af67..4e7760b6ce8 100644 --- a/core/dbt/cli/main.py +++ b/core/dbt/cli/main.py @@ -1,37 +1,52 @@ +import inspect # This is temporary for RAT-ing +import sys +from copy import copy +from pprint import pformat as pf # This is temporary for RAT-ing + import click from dbt.cli import params as p -import sys +from dbt.cli.flags import Flags -# This is temporary for RAT-ing -import inspect -from pprint import pformat as pf + +def cli_runner(): + # Alias "list" to "ls" + ls = copy(cli.commands["list"]) + ls.hidden = True + cli.add_command(ls, "ls") + + # Run the cli + cli() # dbt @click.group( + context_settings={"help_option_names": ["-h", "--help"]}, invoke_without_command=True, no_args_is_help=True, epilog="Specify one of these sub-commands and you can find more help from there.", ) @click.pass_context -@p.version +@p.anonymous_usage_stats @p.cache_selected_only @p.debug +@p.enable_legacy_logger +@p.event_buffer_size @p.fail_fast +@p.log_cache_events @p.log_format +@p.macro_debugging @p.partial_parse @p.print @p.printer_width @p.quiet -@p.send_anonymous_usage_stats +@p.record_timing_info @p.static_parser @p.use_colors @p.use_experimental_parser +@p.version @p.version_check @p.warn_error @p.write_json -@p.event_buffer_size -@p.record_timing def cli(ctx, **kwargs): """An ELT tool for managing your SQL transformations and data models. For more documentation on these commands, visit: docs.getdbt.com @@ -46,26 +61,43 @@ def cli(ctx, **kwargs): # dbt build @cli.command("build") @click.pass_context +@p.defer +@p.exclude +@p.fail_fast +@p.full_refresh +@p.indirect_selection +@p.log_path +@p.models +@p.profile +@p.profiles_dir +@p.project_dir +@p.selector +@p.show +@p.state +@p.store_failures +@p.target +@p.target_path +@p.threads +@p.vars +@p.version_check def build(ctx, **kwargs): """Run all Seeds, Models, Snapshots, and tests in DAG order""" - click.echo( - f"`{inspect.stack()[0][3]}` called\n kwargs: {kwargs}\n ctx: {pf(ctx.parent.params)}" - ) + flags = Flags() + click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}") # dbt clean @cli.command("clean") @click.pass_context -@p.project_dir -@p.profiles_dir @p.profile +@p.profiles_dir +@p.project_dir @p.target @p.vars def clean(ctx, **kwargs): """Delete all folders in the clean-targets list (usually the dbt_packages and target directories.)""" - click.echo( - f"`{inspect.stack()[0][3]}` called\n kwargs: {kwargs}\n ctx: {pf(ctx.parent.params)}" - ) + flags = Flags() + click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}") # dbt docs @@ -78,86 +110,82 @@ def docs(ctx, **kwargs): # dbt docs generate @docs.command("generate") @click.pass_context -@p.version_check -@p.project_dir -@p.profiles_dir -@p.profile -@p.target -@p.vars @p.compile_docs @p.defer -@p.threads -@p.target_path +@p.exclude @p.log_path @p.models -@p.exclude +@p.profile +@p.profiles_dir +@p.project_dir @p.selector @p.state +@p.target +@p.target_path +@p.threads +@p.vars +@p.version_check def docs_generate(ctx, **kwargs): """Generate the documentation website for your project""" - click.echo( - f"`{inspect.stack()[0][3]}` called\n kwargs: {kwargs}\n ctx: {pf(ctx.parent.parent.params)}" - ) + flags = Flags() + click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}") # dbt docs serve @docs.command("serve") @click.pass_context -@p.project_dir -@p.profiles_dir +@p.browser +@p.port @p.profile +@p.profiles_dir +@p.project_dir @p.target @p.vars -@p.port -@p.browser def docs_serve(ctx, **kwargs): """Serve the documentation website for your project""" - click.echo( - f"`{inspect.stack()[0][3]}` called\n kwargs: {kwargs}\n ctx: {pf(ctx.parent.parent.params)}" - ) + flags = Flags() + click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}") # dbt compile @cli.command("compile") @click.pass_context -@p.version_check -@p.project_dir -@p.profiles_dir -@p.profile -@p.target -@p.vars -@p.parse_only -@p.threads -@p.target_path +@p.defer +@p.exclude +@p.full_refresh @p.log_path @p.models -@p.exclude +@p.parse_only +@p.profile +@p.profiles_dir +@p.project_dir @p.selector @p.state -@p.defer -@p.full_refresh +@p.target +@p.target_path +@p.threads +@p.vars +@p.version_check def compile(ctx, **kwargs): """Generates executable SQL from source, model, test, and analysis files. Compiled SQL files are written to the target/ directory.""" - click.echo( - f"`{inspect.stack()[0][3]}` called\n kwargs: {kwargs}\n ctx: {pf(ctx.parent.params)}" - ) + flags = Flags() + click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}") # dbt debug @cli.command("debug") @click.pass_context -@p.version_check -@p.project_dir -@p.profiles_dir +@p.config_dir @p.profile +@p.profiles_dir +@p.project_dir @p.target @p.vars -@p.config_dir +@p.version_check def debug(ctx, **kwargs): """Show some helpful information about dbt for debugging. Not to be confused with the --debug option which increases verbosity.""" - click.echo( - f"`{inspect.stack()[0][3]}` called\n kwargs: {kwargs}\n ctx: {pf(ctx.parent.params)}" - ) + flags = Flags() + click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}") # dbt deps @@ -170,9 +198,8 @@ def debug(ctx, **kwargs): @p.vars def deps(ctx, **kwargs): """Pull the most recent version of the dependencies listed in packages.yml""" - click.echo( - f"`{inspect.stack()[0][3]}` called\n kwargs: {kwargs}\n ctx: {pf(ctx.parent.params)}" - ) + flags = Flags() + click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}") # dbt init @@ -181,147 +208,139 @@ def deps(ctx, **kwargs): @p.profile @p.profiles_dir @p.project_dir +@p.skip_profile_setup @p.target @p.vars -@p.skip_profile_setup def init(ctx, **kwargs): """Initialize a new DBT project.""" - click.echo( - f"`{inspect.stack()[0][3]}` called\n kwargs: {kwargs}\n ctx: {pf(ctx.parent.params)}" - ) + flags = Flags() + click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}") # dbt list -# dbt TODO: Figure out aliasing for ls (or just c/p?) @cli.command("list") @click.pass_context +@p.exclude +@p.indirect_selection +@p.models +@p.output +@p.output_keys @p.profile @p.profiles_dir @p.project_dir -@p.target -@p.vars -@p.output -@p.ouptut_keys @p.resource_type -@p.models -@p.indirect_selection -@p.exclude @p.selector @p.state +@p.target +@p.vars def list(ctx, **kwargs): """List the resources in your project""" - click.echo( - f"`{inspect.stack()[0][3]}` called\n kwargs: {kwargs}\n ctx: {pf(ctx.parent.params)}" - ) + flags = Flags() + click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}") # dbt parse @cli.command("parse") @click.pass_context +@p.compile_parse +@p.log_path @p.profile @p.profiles_dir @p.project_dir @p.target -@p.vars -@p.write_manifest -@p.compile_parse -@p.threads @p.target_path -@p.log_path +@p.threads +@p.vars @p.version_check +@p.write_manifest def parse(ctx, **kwargs): """Parses the project and provides information on performance""" - click.echo( - f"`{inspect.stack()[0][3]}` called\n kwargs: {kwargs}\n ctx: {pf(ctx.parent.params)}" - ) + flags = Flags() + click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}") # dbt run @cli.command("run") @click.pass_context +@p.defer +@p.exclude @p.fail_fast -@p.version_check +@p.full_refresh +@p.log_path +@p.models @p.profile @p.profiles_dir @p.project_dir +@p.selector +@p.state @p.target -@p.vars -@p.log_path @p.target_path @p.threads -@p.models -@p.exclude -@p.selector -@p.state -@p.defer -@p.full_refresh +@p.vars +@p.version_check def run(ctx, **kwargs): """Compile SQL and execute against the current target database.""" - click.echo( - f"`{inspect.stack()[0][3]}` called\n kwargs: {kwargs}\n ctx: {pf(ctx.parent.params)}" - ) + flags = Flags() + click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}") # dbt run operation @cli.command("run-operation") @click.pass_context +@p.args @p.profile @p.profiles_dir @p.project_dir @p.target @p.vars -@p.args def run_operation(ctx, **kwargs): """Run the named macro with any supplied arguments.""" - click.echo( - f"`{inspect.stack()[0][3]}` called\n kwargs: {kwargs}\n ctx: {pf(ctx.parent.params)}" - ) + flags = Flags() + click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}") # dbt seed @cli.command("seed") @click.pass_context -@p.version_check +@p.exclude +@p.full_refresh +@p.log_path +@p.models @p.profile @p.profiles_dir @p.project_dir +@p.selector +@p.show +@p.state @p.target -@p.vars -@p.full_refresh -@p.log_path @p.target_path @p.threads -@p.models -@p.exclude -@p.selector -@p.state -@p.show +@p.vars +@p.version_check def seed(ctx, **kwargs): """Load data from csv files into your data warehouse.""" - click.echo( - f"`{inspect.stack()[0][3]}` called\n kwargs: {kwargs}\n ctx: {pf(ctx.parent.params)}" - ) + flags = Flags() + click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}") # dbt snapshot @cli.command("snapshot") @click.pass_context +@p.defer +@p.exclude +@p.models @p.profile @p.profiles_dir @p.project_dir -@p.target -@p.vars -@p.threads -@p.models -@p.exclude @p.selector @p.state -@p.defer +@p.target +@p.threads +@p.vars def snapshot(ctx, **kwargs): """Execute snapshots defined in your project""" - click.echo( - f"`{inspect.stack()[0][3]}` called\n kwargs: {kwargs}\n ctx: {pf(ctx.parent.params)}" - ) + flags = Flags() + click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}") # dbt source @@ -334,51 +353,49 @@ def source(ctx, **kwargs): # dbt source freshness @source.command("freshness") @click.pass_context +@p.exclude +@p.models +@p.output_path # TODO: Is this ok to re-use? We have three different output params, how much can we consolidate? @p.profile @p.profiles_dir @p.project_dir -@p.target -@p.vars -@p.threads -@p.models -@p.exclude @p.selector @p.state -@p.output_path # TODO: Is this ok to re-use? We have three different output params, how much can we consolidate? +@p.target +@p.threads +@p.vars def freshness(ctx, **kwargs): """Snapshots the current freshness of the project's sources""" - click.echo( - f"`{inspect.stack()[0][3]}` called\n kwargs: {kwargs}\n ctx: {pf(ctx.parent.parent.params)}" - ) + flags = Flags() + click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}") # dbt test @cli.command("test") @click.pass_context +@p.defer +@p.exclude @p.fail_fast -@p.version_check -@p.store_failures +@p.indirect_selection +@p.log_path +@p.models @p.profile @p.profiles_dir @p.project_dir +@p.selector +@p.state +@p.store_failures @p.target -@p.vars -@p.indirect_selection -@p.log_path @p.target_path @p.threads -@p.models -@p.exclude -@p.selector -@p.state -@p.defer +@p.vars +@p.version_check def test(ctx, **kwargs): """Runs tests on data in deployed models. Run this after `dbt run`""" - click.echo( - f"`{inspect.stack()[0][3]}` called\n kwargs: {kwargs}\n ctx: {pf(ctx.parent.params)}" - ) + flags = Flags() + click.echo(f"`{inspect.stack()[0][3]}` called\n flags: {flags}") # Support running as a module if __name__ == "__main__": - cli() + cli_runner() diff --git a/core/dbt/cli/option_types.py b/core/dbt/cli/option_types.py new file mode 100644 index 00000000000..523df651775 --- /dev/null +++ b/core/dbt/cli/option_types.py @@ -0,0 +1,33 @@ +from click import ParamType +import yaml + + +class YAML(ParamType): + """The Click YAML type. Converts YAML strings into objects.""" + + name = "YAML" + + def convert(self, value, param, ctx): + # assume non-string values are a problem + if not isinstance(value, str): + self.fail(f"Cannot load YAML from type {type(value)}", param, ctx) + try: + return yaml.load(value, Loader=yaml.Loader) + except yaml.parser.ParserError: + self.fail(f"String '{value}' is not valid YAML", param, ctx) + + +class Truthy(ParamType): + """The Click Truthy type. Converts strings into a "truthy" type""" + + name = "TRUTHY" + + def convert(self, value, param, ctx): + # assume non-string / non-None values are a problem + if not isinstance(value, (str, None)): + self.fail(f"Cannot load TRUTHY from type {type(value)}", param, ctx) + + if value is None or value.lower() in ("0", "false", "f"): + return None + else: + return value diff --git a/core/dbt/cli/params.py b/core/dbt/cli/params.py index f991359ecf7..68b3b0b8bf0 100644 --- a/core/dbt/cli/params.py +++ b/core/dbt/cli/params.py @@ -1,56 +1,56 @@ -import click -import yaml from pathlib import Path, PurePath -from click import ParamType - - -class YAML(ParamType): - """The Click YAML type. Converts YAML strings into objects.""" - name = "YAML" - - def convert(self, value, param, ctx): - # assume non-string values are a problem - if not isinstance(value, str): - self.fail(f"Cannot load YAML from type {type(value)}", param, ctx) - try: - return yaml.load(value, Loader=yaml.Loader) - except yaml.parser.ParserError: - self.fail(f"String '{value}' is not valid YAML", param, ctx) +import click +from dbt.cli.option_types import YAML +# TODO: The name (reflected in flags) is a correction! +# The original name was `SEND_ANONYMOUS_USAGE_STATS` and used an env var called "DBT_SEND_ANONYMOUS_USAGE_STATS" +# Both of which break existing naming conventions (doesn't match param flag). +# This will need to be fixed before use in the main codebase and communicated as a change to the community! +anonymous_usage_stats = click.option( + "--anonymous-usage-stats/--no-anonymous-usage-stats", + envvar="DBT_ANONYMOUS_USAGE_STATS", + help="Send anonymous usage stats to dbt Labs.", + default=True, +) args = click.option( "--args", + envvar=None, help="Supply arguments to the macro. This dictionary will be mapped to the keyword arguments defined in the selected macro. This argument should be a YAML string, eg. '{my_variable: my_value}'", type=YAML(), ) browser = click.option( "--browser/--no-browser", + envvar=None, help="Wether or not to open a local web browser after starting the server", default=True, ) cache_selected_only = click.option( "--cache-selected-only/--no-cache-selected-only", + envvar="DBT_CACHE_SELECTED_ONLY", help="Pre cache database objects relevant to selected resource only.", - default=False, ) compile_docs = click.option( "--compile/--no-compile", + envvar=None, help="Wether or not to run 'dbt compile' as part of docs generation", default=True, ) compile_parse = click.option( "--compile/--no-compile", + envvar=None, help="TODO: No help text currently available", default=True, ) config_dir = click.option( "--config-dir", + envvar=None, help="If specified, DBT will show path information for this project", type=click.STRING, ) @@ -58,44 +58,67 @@ def convert(self, value, param, ctx): debug = click.option( "--debug/--no-debug", "-d/ ", + envvar="DBT_DEBUG", help="Display debug logging during dbt execution. Useful for debugging and making bug reports.", - default=False, ) +# TODO: The env var and name (reflected in flags) are corrections! +# The original name was `DEFER_MODE` and used an env var called "DBT_DEFER_TO_STATE" +# Both of which break existing naming conventions. +# This will need to be fixed before use in the main codebase and communicated as a change to the community! defer = click.option( "--defer/--no-defer", + envvar="DBT_DEFER", help="If set, defer to the state variable for resolving unselected nodes.", - default=True, +) + +enable_legacy_logger = click.option( + "--enable-legacy-logger/--no-enable-legacy-logger", + envvar="DBT_ENABLE_LEGACY_LOGGER", + hidden=True, ) event_buffer_size = click.option( "--event-buffer-size", + envvar="DBT_EVENT_BUFFER_SIZE", help="Sets the max number of events to buffer in EVENT_HISTORY.", default=100000, type=click.INT, ) -exclude = click.option("--exclude", help="Specify the nodes to exclude.") +exclude = click.option("--exclude", envvar=None, help="Specify the nodes to exclude.") fail_fast = click.option( - "--fail-fast/--no-fail-fast", "-x/ ", help="Stop execution on first failure.", default=False + "--fail-fast/--no-fail-fast", + "-x/ ", + envvar="DBT_FAIL_FAST", + help="Stop execution on first failure.", ) full_refresh = click.option( "--full-refresh", + envvar="DBT_FULL_REFRESH", help="If specified, dbt will drop incremental models and fully-recalculate the incremental table from the model definition.", is_flag=True, ) indirect_selection = click.option( - "--indirect_selection", + "--indirect-selection", + envvar="DBT_INDIRECT_SELECTION", help="Select all tests that are adjacent to selected resources, even if they those resources have been explicitly selected.", type=click.Choice(["eager", "cautious"], case_sensitive=False), default="eager", ) +log_cache_events = click.option( + "--log-cache-events/--no-log-cache-events", + help="Enable verbose adapter cache logging.", + envvar="DBT_LOG_CACHE_EVENTS", +) + log_format = click.option( "--log-format", + envvar="DBT_LOG_FORMAT", help="Specify the log format, overriding the command's default.", type=click.Choice(["text", "json", "default"], case_sensitive=False), default="default", @@ -103,28 +126,42 @@ def convert(self, value, param, ctx): log_path = click.option( "--log-path", + envvar="DBT_LOG_PATH", help="Configure the 'log-path'. Only applies this setting for the current run. Overrides the 'DBT_LOG_PATH' if it is set.", type=click.Path(), ) -models = click.option("-m", "-s", help="Specify the nodes to include.", multiple=True) +macro_debugging = click.option( + "--macro-debugging/--no-macro-debugging", + envvar="DBT_MACRO_DEBUGGING", + hidden=True, +) + +models = click.option( + "-m", + "-s", + "models", + envvar=None, + help="Specify the nodes to include.", + multiple=True, +) output = click.option( "--output", + envvar=None, help="TODO: No current help text", type=click.Choice(["json", "name", "path", "selector"], case_sensitive=False), default="name", ) -ouptut_keys = click.option( - "--output-keys", - help="TODO: No current help text", - default=False, +output_keys = click.option( + "--output-keys", envvar=None, help="TODO: No current help text", type=click.STRING ) output_path = click.option( "--output", "-o", + envvar=None, help="Specify the output path for the json report. By default, outputs to 'target/sources.json'", type=click.Path(file_okay=True, dir_okay=False, writable=True), default=PurePath.joinpath(Path.cwd(), "target/sources.json"), @@ -132,35 +169,54 @@ def convert(self, value, param, ctx): parse_only = click.option( "--parse-only", + envvar=None, help="TODO: No help text currently available", is_flag=True, ) partial_parse = click.option( "--partial-parse/--no-partial-parse", + envvar="DBT_PARTIAL_PARSE", help="Allow for partial parsing by looking for and writing to a pickle file in the target directory. This overrides the user configuration file.", default=True, ) port = click.option( - "--port", help="Specify the port number for the docs server", default=8080, type=click.INT + "--port", + envvar=None, + help="Specify the port number for the docs server", + default=8080, + type=click.INT, ) +# TODO: The env var and name (reflected in flags) are corrections! +# The original name was `NO_PRINT` and used the env var `DBT_NO_PRINT`. +# Both of which break existing naming conventions. +# This will need to be fixed before use in the main codebase and communicated as a change to the community! print = click.option( - "--print/--no-print", help="Output all {{ print() }} macro calls.", default=True + "--print/--no-print", + envvar="DBT_PRINT", + help="Output all {{ print() }} macro calls.", + default=True, ) printer_width = click.option( - "--printer_width", help="Sets the width of terminal output", type=click.INT, default=80 + "--printer-width", + envvar="DBT_PRINTER_WIDTH", + help="Sets the width of terminal output", + type=click.INT, + default=80, ) profile = click.option( "--profile", + envvar=None, help="Which profile to load. Overrides setting in dbt_project.yml.", ) profiles_dir = click.option( "--profiles-dir", + envvar="DBT_PROFILES_DIR", help=f"Which directory to look in for the profiles.yml file. Default = {PurePath.joinpath(Path.home(), '.dbt')}", default=PurePath.joinpath(Path.home(), ".dbt"), type=click.Path( @@ -170,6 +226,7 @@ def convert(self, value, param, ctx): project_dir = click.option( "--project-dir", + envvar=None, help="Which directory to look in for the dbt_project.yml file. Default is the current working directory and its parents.", default=Path.cwd(), type=click.Path(exists=True), @@ -177,20 +234,21 @@ def convert(self, value, param, ctx): quiet = click.option( "--quiet/--no-quiet", + envvar="DBT_QUIET", help="Suppress all non-error logging to stdout. Does not affect {{ print() }} macro calls.", - default=False, ) -record_timing = click.option( - "-r", +record_timing_info = click.option( "--record-timing-info", + "-r", + envvar=None, help="When this option is passed, dbt will output low-level timing stats to the specified file. Example: `--record-timing-info output.profile`", is_flag=True, - default=False, ) resource_type = click.option( "--resource-type", + envvar=None, help="TODO: No current help text", type=click.Choice( [ @@ -210,50 +268,63 @@ def convert(self, value, param, ctx): default="default", ) -selector = click.option("--selector", help="The selector name to use, as defined in selectors.yml") - -send_anonymous_usage_stats = click.option( - "--anonymous-usage-stats/--no-anonymous-usage-stats", - help="Send anonymous usage stats to dbt Labs.", - default=True, +selector = click.option( + "--selector", envvar=None, help="The selector name to use, as defined in selectors.yml" ) show = click.option( - "--show", - help="Show a sample of the loaded data in the terminal", - default=False, + "--show", envvar=None, help="Show a sample of the loaded data in the terminal", is_flag=True ) skip_profile_setup = click.option( - "--skip-profile-setup", - "-s", - help="Skip interative profile setup.", - default=False, + "--skip-profile-setup", "-s", envvar=None, help="Skip interative profile setup.", is_flag=True ) +# TODO: The env var and name (reflected in flags) are corrections! +# The original name was `ARTIFACT_STATE_PATH` and used the env var `DBT_ARTIFACT_STATE_PATH`. +# Both of which break existing naming conventions. +# This will need to be fixed before use in the main codebase and communicated as a change to the community! state = click.option( "--state", + envvar="DBT_STATE", help="If set, use the given directory as the source for json files to compare with this project.", + type=click.Path( + dir_okay=True, + exists=True, + file_okay=False, + readable=True, + resolve_path=True, + ), ) static_parser = click.option( - "--static-parser/--no-static-parser", help="Use the static parser.", default=True + "--static-parser/--no-static-parser", + envvar="DBT_STATIC_PARSER", + help="Use the static parser.", + default=True, ) store_failures = click.option( - "--store-failures", help="Store test results (failing rows) in the database", default=False + "--store-failures", + envvar="DBT_STORE_FAILURES", + help="Store test results (failing rows) in the database", + is_flag=True, ) -target = click.option("-t", "--target", help="Which target to load for the given profile") +target = click.option( + "--target", "-t", envvar=None, help="Which target to load for the given profile" +) target_path = click.option( "--target-path", + envvar="DBT_TARGET_PATH", help="Configure the 'target-path'. Only applies this setting for the current run. Overrides the 'DBT_TARGET_PATH' if it is set.", type=click.Path(), ) threads = click.option( "--threads", + envvar=None, help="Specify number of threads to use while executing models. Overrides settings in profiles.yml.", default=1, type=click.INT, @@ -261,44 +332,54 @@ def convert(self, value, param, ctx): use_colors = click.option( "--use-colors/--no-use-colors", + envvar="DBT_USE_COLORS", help="Output is colorized by default and may also be set in a profile or at the command line.", default=True, ) use_experimental_parser = click.option( "--use-experimental-parser/--no-use-experimental-parser", + envvar="DBT_USE_EXPERIMENTAL_PARSER", help="Enable experimental parsing features.", - default=False, ) vars = click.option( "--vars", + envvar=None, help="Supply variables to the project. This argument overrides variables defined in your dbt_project.yml file. This argument should be a YAML string, eg. '{my_variable: my_value}'", type=YAML(), ) -version = click.option("--version", help="Show version information", is_flag=True, default=False) +version = click.option( + "--version", + envvar=None, + help="Show version information", + is_flag=True, +) version_check = click.option( "--version-check/--no-version-check", + envvar="DBT_VERSION_CHECK", help="Ensure dbt's version matches the one specified in the dbt_project.yml file ('require-dbt-version')", default=True, ) warn_error = click.option( "--warn-error/--no-warn-error", + envvar="DBT_WARN_ERROR", help="If dbt would normally warn, instead raise an exception. Examples include --models that selects nothing, deprecations, configurations with no associated models, invalid test configurations, and missing sources/refs in tests.", - default=False, ) write_json = click.option( "--write-json/--no-write-json", + envvar="DBT_WRITE_JSON", help="Writing the manifest and run_results.json files to disk", default=True, ) write_manifest = click.option( "--write-manifest/--no-write-manifest", + envvar=None, help="TODO: No help text currently available", default=True, ) diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 294961c98e7..07dda952a9d 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -1,25 +1,50 @@ -from dbt.cli.main import cli +import ast +from inspect import getsource + import click +from dbt.cli import params +from dbt.cli.main import cli class TestCLI: def test_commands_have_docstrings(self): def run_test(commands): - for _, command in commands.items(): - if type(command) is click.core.Command: + for command in commands.values(): + if type(command) is click.Command: assert command.__doc__ is not None - if type(command) is click.core.Group: + if type(command) is click.Group: run_test(command.commands) run_test(cli.commands) - def test_params_have_help_texts(self): - def run_test(commands): - for _, command in commands.items(): - if type(command) is click.core.Command: - for param in command.params: - assert param.help is not None - if type(command) is click.core.Group: - run_test(command.commands) + # TODO: This isn't the ideal way to test params as + # they will be tested as many times as they are used as decorators. + # This is inefficent (obvs) + def test_unhidden_params_have_help_texts(self): + def run_test(command): + for param in command.params: + if not param.hidden: + assert param.help is not None + if type(command) is click.Group: + for command in command.commands.values(): + run_test(command) - run_test(cli.commands) + run_test(cli) + + def test_param_names_match_envvars(self): + def run_test(command): + for param in command.params: + if param.envvar is not None: + assert "DBT_" + param.name.upper() == param.envvar + if type(command) is click.Group: + for command in command.commands.values(): + run_test(command) + + run_test(cli) + + def test_params_are_alpha_sorted(self): + root_node = ast.parse(getsource(params)) + param_var_names = [ + node.targets[0].id for node in ast.walk(root_node) if isinstance(node, ast.Assign) + ] + assert param_var_names == sorted(param_var_names)