From adf9b38181aa0448a00f7f47063e3f2b6679736d Mon Sep 17 00:00:00 2001 From: Bartlomiej Hirsz Date: Tue, 22 Aug 2023 18:20:36 +0200 Subject: [PATCH 1/2] Read default configuration file in --list command --- robotidy/cli.py | 7 ++-- tests/utest/test_cli.py | 89 ++++++++++++++++++++++++----------------- 2 files changed, 55 insertions(+), 41 deletions(-) diff --git a/robotidy/cli.py b/robotidy/cli.py index 73b596eb..6bbc0448 100644 --- a/robotidy/cli.py +++ b/robotidy/cli.py @@ -154,16 +154,15 @@ def print_transformers_list(global_config: config_module.MainConfig): target_version = global_config.default.target_version list_transformers = global_config.default.list_transformers + config = global_config.get_config_for_source(Path.cwd()) table = Table(title="Transformers", header_style="bold red") table.add_column("Name", justify="left", no_wrap=True) table.add_column("Enabled") transformers = load_transformers(TransformConfigMap([], [], []), allow_disabled=True, target_version=target_version) - transformers.extend( - _load_external_transformers(transformers, global_config.default_loaded.transformers_config, target_version) - ) + transformers.extend(_load_external_transformers(transformers, config.transformers_config, target_version)) for transformer in transformers: - enabled = transformer.name in global_config.default_loaded.transformers_lookup + enabled = transformer.name in config.transformers_lookup if list_transformers != "all": filter_by = list_transformers == "enabled" if enabled != filter_by: diff --git a/tests/utest/test_cli.py b/tests/utest/test_cli.py index e330884e..5b1e3c8b 100644 --- a/tests/utest/test_cli.py +++ b/tests/utest/test_cli.py @@ -1,4 +1,5 @@ import os +from contextlib import contextmanager from pathlib import Path from unittest.mock import patch @@ -13,10 +14,7 @@ from .utils import run_tidy - -@pytest.fixture(scope="session") -def test_data_dir(): - return Path(__file__).parent / "testdata" +TEST_DATA_DIR = Path(__file__).parent / "testdata" @pytest.fixture @@ -29,6 +27,16 @@ def temporary_cwd(tmpdir): os.chdir(prev_cwd) +@contextmanager +def switch_cwd(new_cwd): + prev_cwd = Path.cwd() + os.chdir(new_cwd) + try: + yield + finally: + os.chdir(prev_cwd) + + class TestCli: @pytest.mark.parametrize( "name, similar", @@ -140,23 +148,23 @@ def test_invalid_argument_type_for_transform(self): result = run_tidy(args, exit_code=1) assert result.output == expected_output - def test_find_project_root_from_src(self, test_data_dir): - src = test_data_dir / "nested" / "test.robot" + def test_find_project_root_from_src(self): + src = TEST_DATA_DIR / "nested" / "test.robot" path = find_project_root((src,)) - assert path == test_data_dir / "nested" + assert path == TEST_DATA_DIR / "nested" - def test_ignore_git_dir(self, test_data_dir): + def test_ignore_git_dir(self): """Test if --ignore-git-dir works when locating pyproject.toml file.""" - src = test_data_dir / "with_git_dir" / "project_a" + src = TEST_DATA_DIR / "with_git_dir" / "project_a" (src / ".git").mkdir(exist_ok=True) root_with_git = src - root_without_git = test_data_dir / "with_git_dir" + root_without_git = TEST_DATA_DIR / "with_git_dir" path = find_project_root((src,), ignore_git_dir=False) assert path == root_with_git path = find_project_root((src,), ignore_git_dir=True) assert path == root_without_git - def test_read_robotidy_config(self, test_data_dir): + def test_read_robotidy_config(self): """robotidy.toml follows the same format as pyproject starting from 1.2.0""" expected_config = { "overwrite": False, @@ -164,11 +172,11 @@ def test_read_robotidy_config(self, test_data_dir): "spacecount": 4, "transform": ["DiscardEmptySections:allow_only_comments=True", "ReplaceRunKeywordIf"], } - config_path = test_data_dir / "config" / "robotidy.toml" + config_path = TEST_DATA_DIR / "config" / "robotidy.toml" config = read_pyproject_config(config_path) assert config == expected_config - def test_read_pyproject_config(self, test_data_dir): + def test_read_pyproject_config(self): expected_parsed_config = { "overwrite": False, "diff": False, @@ -181,19 +189,19 @@ def test_read_pyproject_config(self, test_data_dir): "OrderSettings: keyword_before = documentation,tags,timeout,arguments", ], } - config_path = test_data_dir / "only_pyproject" / "pyproject.toml" + config_path = TEST_DATA_DIR / "only_pyproject" / "pyproject.toml" config = read_pyproject_config(config_path) assert config == expected_parsed_config - def test_read_invalid_config(self, test_data_dir): - config_path = test_data_dir / "invalid_pyproject" / "pyproject.toml" + def test_read_invalid_config(self): + config_path = TEST_DATA_DIR / "invalid_pyproject" / "pyproject.toml" with pytest.raises(FileError) as err: read_pyproject_config(config_path) assert "Error reading configuration file: " in str(err) @pytest.mark.parametrize("option, correct", [("confgure", "configure"), ("idontexist", None)]) - def test_read_invalid_option_config(self, option, correct, test_data_dir): - config_path = test_data_dir / "invalid_options_config" / f"pyproject_{option}.toml" + def test_read_invalid_option_config(self, option, correct): + config_path = TEST_DATA_DIR / "invalid_options_config" / f"pyproject_{option}.toml" with pytest.raises(NoSuchOption) as err: config_file = read_pyproject_config(config_path) RawConfig().from_config_file(config_file, config_path) @@ -230,10 +238,17 @@ def test_list_transformers_filter_enabled(self, flag): assert "NormalizeNewLines" in result.output assert "SmartSortKeywords" not in result.output - def test_no_config(self, temporary_cwd): + def test_list_no_config(self, temporary_cwd): """Execute Robotidy in temporary directory to ensure it supports running without default configuration file.""" run_tidy(["--list"]) + def test_list_with_config(self): + config_dir = TEST_DATA_DIR / "pyproject_with_src" + with switch_cwd(config_dir): + result = run_tidy(["--list", "enabled"]) + assert "SplitTooLongLine" in result.output + assert "NormalizeSeparators" not in result.output + @pytest.mark.parametrize("flag", ["--list", "-l"]) def test_list_transformers_filter_disabled(self, flag): # --transform X should not have X in output, even if it is default transformer @@ -294,8 +309,8 @@ def test_help(self, flag): assert f"Robotidy is a tool for formatting" in result.output @pytest.mark.parametrize("source, return_status", [("golden.robot", 0), ("not_golden.robot", 1)]) - def test_check(self, source, return_status, test_data_dir): - source = test_data_dir / "check" / source + def test_check(self, source, return_status): + source = TEST_DATA_DIR / "check" / source with patch("robotidy.utils.ModelWriter") as mock_writer: run_tidy( ["--check", "--transform", "NormalizeSectionHeaderName", str(source)], @@ -304,8 +319,8 @@ def test_check(self, source, return_status, test_data_dir): mock_writer.assert_not_called() @pytest.mark.parametrize("source, return_status", [("golden.robot", 0), ("not_golden.robot", 1)]) - def test_check_overwrite(self, source, return_status, test_data_dir): - source = test_data_dir / "check" / source + def test_check_overwrite(self, source, return_status): + source = TEST_DATA_DIR / "check" / source with patch("robotidy.utils.ModelWriter") as mock_writer: run_tidy( ["--check", "--overwrite", "--transform", "NormalizeSectionHeaderName", str(source)], @@ -318,10 +333,10 @@ def test_check_overwrite(self, source, return_status, test_data_dir): @pytest.mark.parametrize("color_flag", ["--color", "--no-color", None]) @pytest.mark.parametrize("color_env", [True, False]) - def test_disable_coloring(self, color_flag, color_env, test_data_dir): + def test_disable_coloring(self, color_flag, color_env): should_be_colored = not ((color_flag is not None and color_flag == "--no-color") or color_env) mocked_env = {"NO_COLOR": ""} if color_env else {} - source = test_data_dir / "check" / "not_golden.robot" + source = TEST_DATA_DIR / "check" / "not_golden.robot" command = ["--diff", "--no-overwrite"] if color_flag: command.append(color_flag) @@ -333,17 +348,17 @@ def test_disable_coloring(self, color_flag, color_env, test_data_dir): else: mock_color.assert_not_called() - def test_diff(self, test_data_dir): - source = test_data_dir / "check" / "not_golden.robot" + def test_diff(self): + source = TEST_DATA_DIR / "check" / "not_golden.robot" result = run_tidy(["--diff", "--no-overwrite", "--transform", "NormalizeSectionHeaderName", str(source)]) assert "*** settings ***" in result.output assert "*** Settings ***" in result.output @pytest.mark.parametrize("line_sep", ["unix", "windows", "native", None]) - def test_line_sep(self, line_sep, test_data_dir): - source = test_data_dir / "line_sep" / "test.robot" - expected = test_data_dir / "line_sep" / "expected.robot" - actual = test_data_dir.parent / "actual" / "test.robot" + def test_line_sep(self, line_sep): + source = TEST_DATA_DIR / "line_sep" / "test.robot" + expected = TEST_DATA_DIR / "line_sep" / "expected.robot" + actual = TEST_DATA_DIR.parent / "actual" / "test.robot" if line_sep is not None: run_tidy(["--lineseparator", line_sep, str(source)], output="test.robot") else: @@ -366,12 +381,12 @@ def test_line_sep(self, line_sep, test_data_dir): ("test.resource", "nested/*", ["test.robot"]), ], ) - def test_exclude_gitignore(self, exclude, extend_exclude, skip_gitignore, allowed, test_data_dir): + def test_exclude_gitignore(self, exclude, extend_exclude, skip_gitignore, allowed): if skip_gitignore: allowed = allowed + ["test2.robot"] # extend will not work due to mutability of list if not extend_exclude or "nested" not in extend_exclude: allowed = allowed + ["nested/test2.robot"] - source = test_data_dir / "gitignore" + source = TEST_DATA_DIR / "gitignore" allowed_paths = {Path(source, path) for path in allowed} paths = get_paths( (str(source),), @@ -390,8 +405,8 @@ def test_exclude_gitignore(self, exclude, extend_exclude, skip_gitignore, allowe (".", ["test.robot", "test3.robot", "resources/test.robot"]), ], ) - def test_src_and_space_in_param_in_configuration(self, source, should_parse, test_data_dir): - source_dir = test_data_dir / "pyproject_with_src" + def test_src_and_space_in_param_in_configuration(self, source, should_parse): + source_dir = TEST_DATA_DIR / "pyproject_with_src" os.chdir(source_dir) if source is not None: source = source_dir / source @@ -406,8 +421,8 @@ def test_src_and_space_in_param_in_configuration(self, source, should_parse, tes assert actual == sorted(expected) @pytest.mark.parametrize("source", [1, 2]) - def test_empty_configuration(self, source, test_data_dir): - config_dir = test_data_dir / f"empty_pyproject{source}" + def test_empty_configuration(self, source): + config_dir = TEST_DATA_DIR / f"empty_pyproject{source}" os.chdir(config_dir) result = run_tidy(exit_code=1) assert "Loaded configuration from" not in result.output From d77f93f25ba62bf395673542b37c29ece3858242 Mon Sep 17 00:00:00 2001 From: Bartlomiej Hirsz Date: Wed, 23 Aug 2023 19:01:03 +0200 Subject: [PATCH 2/2] Generate configuration file command --- docs/releasenotes/unreleased/other.1.rst | 57 ++++++++++ docs/source/configuration/config_file.rst | 120 ++++++++++++++++++++ robotidy/cli.py | 49 +++++++- robotidy/config.py | 3 +- robotidy/exceptions.py | 8 ++ robotidy/transformers/ReplaceEmptyValues.py | 1 + setup.py | 2 + tests/utest/test_cli.py | 71 +++++++++++- 8 files changed, 306 insertions(+), 5 deletions(-) create mode 100644 docs/releasenotes/unreleased/other.1.rst diff --git a/docs/releasenotes/unreleased/other.1.rst b/docs/releasenotes/unreleased/other.1.rst new file mode 100644 index 00000000..f548574d --- /dev/null +++ b/docs/releasenotes/unreleased/other.1.rst @@ -0,0 +1,57 @@ +Generate configuration file (#563) +----------------------------------- + +New option to generate configuration file with most important options and their default values:: + + robotidy --generate-config + +It is only enabled after installing optional ``generate_config`` profile:: + + pip install robotidy[generate_config] + +Generated configuration contains all transformers with their enabled status. Example of the file:: + + [tool.robotidy] + diff = false + overwrite = true + verbose = false + separator = "space" + spacecount = 4 + line_length = 120 + lineseparator = "native" + skip_gitignore = false + ignore_git_dir = false + configure = [ + "AddMissingEnd:enabled=True", + "NormalizeSeparators:enabled=True", + "DiscardEmptySections:enabled=True", + "MergeAndOrderSections:enabled=True", + "RemoveEmptySettings:enabled=True", + "ReplaceEmptyValues:enabled=True", + "NormalizeAssignments:enabled=True", + "GenerateDocumentation:enabled=False", + "OrderSettings:enabled=True", + "OrderSettingsSection:enabled=True", + "NormalizeTags:enabled=True", + "OrderTags:enabled=False", + "RenameVariables:enabled=False", + "IndentNestedKeywords:enabled=False", + "AlignSettingsSection:enabled=True", + "AlignVariablesSection:enabled=True", + "AlignTemplatedTestCases:enabled=False", + "AlignTestCasesSection:enabled=False", + "AlignKeywordsSection:enabled=False", + "NormalizeNewLines:enabled=True", + "NormalizeSectionHeaderName:enabled=True", + "NormalizeSettingName:enabled=True", + "ReplaceRunKeywordIf:enabled=True", + "SplitTooLongLine:enabled=True", + "SmartSortKeywords:enabled=False", + "RenameTestCases:enabled=False", + "RenameKeywords:enabled=False", + "ReplaceReturns:enabled=True", + "ReplaceBreakContinue:enabled=True", + "InlineIf:enabled=True", + "Translate:enabled=False", + "NormalizeComments:enabled=True", + ] diff --git a/docs/source/configuration/config_file.rst b/docs/source/configuration/config_file.rst index bebe9e09..6a024cd8 100644 --- a/docs/source/configuration/config_file.rst +++ b/docs/source/configuration/config_file.rst @@ -83,6 +83,126 @@ multiple configuration files, the source paths from other configurations than th "NormalizeSeparators" ] +Generate configuration file +--------------------------- + +It is possible to generate configuration files that contains most important options with their default values. +First install ``robotidy`` with ``generate_config`` that contains module for writing TOML files:: + + pip install robotidy[generate_config] + +You can generate configuration now:: + + robotidy --generate-config + +.. dropdown:: Example of generated configuration file + + .. code-block:: toml + + [tool.robotidy] + diff = false + overwrite = true + verbose = false + separator = "space" + spacecount = 4 + line_length = 120 + lineseparator = "native" + skip_gitignore = false + ignore_git_dir = false + configure = [ + "AddMissingEnd:enabled=True", + "NormalizeSeparators:enabled=True", + "DiscardEmptySections:enabled=True", + "MergeAndOrderSections:enabled=True", + "RemoveEmptySettings:enabled=True", + "ReplaceEmptyValues:enabled=True", + "NormalizeAssignments:enabled=True", + "GenerateDocumentation:enabled=False", + "OrderSettings:enabled=True", + "OrderSettingsSection:enabled=True", + "NormalizeTags:enabled=True", + "OrderTags:enabled=False", + "RenameVariables:enabled=False", + "IndentNestedKeywords:enabled=False", + "AlignSettingsSection:enabled=True", + "AlignVariablesSection:enabled=True", + "AlignTemplatedTestCases:enabled=False", + "AlignTestCasesSection:enabled=False", + "AlignKeywordsSection:enabled=False", + "NormalizeNewLines:enabled=True", + "NormalizeSectionHeaderName:enabled=True", + "NormalizeSettingName:enabled=True", + "ReplaceRunKeywordIf:enabled=True", + "SplitTooLongLine:enabled=True", + "SmartSortKeywords:enabled=False", + "RenameTestCases:enabled=False", + "RenameKeywords:enabled=False", + "ReplaceReturns:enabled=True", + "ReplaceBreakContinue:enabled=True", + "InlineIf:enabled=True", + "Translate:enabled=False", + "NormalizeComments:enabled=True", + ] + +By default configuration file will be save in the current working directory as ``pyproject.toml`` file. Default +filename can be configured:: + + robotidy --generate-config your_name.txt + +Configuration is based on default values and configuration from the cli:: + + robotidy --transform ReplaceReturns --diff --generate-config + +.. dropdown:: Generated file + + .. code-block:: toml + + [tool.robotidy] + diff = true + overwrite = true + verbose = false + separator = "space" + spacecount = 4 + line_length = 120 + lineseparator = "native" + skip_gitignore = false + ignore_git_dir = false + configure = [ + "AddMissingEnd:enabled=False", + "NormalizeSeparators:enabled=False", + "DiscardEmptySections:enabled=False", + "MergeAndOrderSections:enabled=False", + "RemoveEmptySettings:enabled=False", + "ReplaceEmptyValues:enabled=False", + "NormalizeAssignments:enabled=False", + "GenerateDocumentation:enabled=False", + "OrderSettings:enabled=False", + "OrderSettingsSection:enabled=False", + "NormalizeTags:enabled=False", + "OrderTags:enabled=False", + "RenameVariables:enabled=False", + "IndentNestedKeywords:enabled=False", + "AlignSettingsSection:enabled=False", + "AlignVariablesSection:enabled=False", + "AlignTemplatedTestCases:enabled=False", + "AlignTestCasesSection:enabled=False", + "AlignKeywordsSection:enabled=False", + "NormalizeNewLines:enabled=False", + "NormalizeSectionHeaderName:enabled=False", + "NormalizeSettingName:enabled=False", + "ReplaceRunKeywordIf:enabled=False", + "SplitTooLongLine:enabled=False", + "SmartSortKeywords:enabled=False", + "RenameTestCases:enabled=False", + "RenameKeywords:enabled=False", + "ReplaceReturns:enabled=True", + "ReplaceBreakContinue:enabled=False", + "InlineIf:enabled=False", + "Translate:enabled=False", + "NormalizeComments:enabled=False", + ] + + Multiline configuration ------------------------ Transformers with multiple parameters can be configured in one line (each param delimited by ``:``) or in separate lines: diff --git a/robotidy/cli.py b/robotidy/cli.py index 6bbc0448..b59440e6 100644 --- a/robotidy/cli.py +++ b/robotidy/cli.py @@ -13,7 +13,7 @@ from robotidy import app from robotidy import config as config_module -from robotidy import decorators, files, skip, utils, version +from robotidy import decorators, exceptions, files, skip, utils, version from robotidy.config import RawConfig, csv_list_type, validate_target_version from robotidy.rich_console import console from robotidy.transformers import TransformConfigMap, TransformConfigParameter, load_transformers @@ -37,7 +37,7 @@ }, { "name": "Configuration", - "options": ["--configure", "--config", "--ignore-git-dir"], + "options": ["--configure", "--config", "--ignore-git-dir", "--generate-config"], }, { "name": "Global formatting settings", @@ -186,6 +186,40 @@ def print_transformers_list(global_config: config_module.MainConfig): ) +def generate_config(global_config: config_module.MainConfig): + try: + import tomli_w + except ImportError: + raise exceptions.MissingOptionalTomliWDependencyError() + target_version = global_config.default.target_version + config = global_config.default_loaded + transformers = load_transformers(TransformConfigMap([], [], []), allow_disabled=True, target_version=target_version) + transformers.extend(_load_external_transformers(transformers, config.transformers_config, target_version)) + + toml_config = { + "tool": { + "robotidy": { + "diff": global_config.default_loaded.show_diff, + "overwrite": global_config.default_loaded.overwrite, + "verbose": global_config.default_loaded.verbose, + "separator": global_config.default.separator, + "spacecount": global_config.default_loaded.formatting.space_count, + "line_length": global_config.default.line_length, + "lineseparator": global_config.default.lineseparator, + "skip_gitignore": global_config.default.skip_gitignore, + "ignore_git_dir": global_config.default.ignore_git_dir, + } + } + } + configure_transformers = [ + f"{transformer.name}:enabled={transformer.name in config.transformers_lookup}" for transformer in transformers + ] + toml_config["tool"]["robotidy"]["configure"] = configure_transformers + + with open(global_config.default.generate_config, "w") as fp: + fp.write(tomli_w.dumps(toml_config)) + + @click.command(context_settings=CONTEXT_SETTINGS) @click.option( "--transform", @@ -367,6 +401,14 @@ def print_transformers_list(global_config: config_module.MainConfig): help="List available transformers and exit. " "Pass optional value **enabled** or **disabled** to filter out list by transformer status.", ) +@click.option( + "--generate-config", + is_flag=False, + default="", + flag_value="pyproject.toml", + help="Generate configuration file. Pass optional value to change default config filename.", + show_default="pyproject.toml", +) @click.option( "--desc", "-d", @@ -442,6 +484,9 @@ def cli(ctx: click.Context, **kwargs): if global_config.default.desc is not None: return_code = print_description(global_config.default.desc, global_config.default.target_version) sys.exit(return_code) + if global_config.default.generate_config: + generate_config(global_config) + sys.exit(0) tidy = app.Robotidy(global_config) status = tidy.transform_files() sys.exit(status) diff --git a/robotidy/config.py b/robotidy/config.py index 90bfb8d1..0ffc7bfb 100644 --- a/robotidy/config.py +++ b/robotidy/config.py @@ -138,6 +138,7 @@ class RawConfig: endline: int = None line_length: int = 120 list_transformers: str = "" + generate_config: str = "" desc: str = None output: Path = None force_order: bool = False @@ -227,7 +228,7 @@ def __init__(self, cli_config: RawConfig): self.sources = self.get_sources(self.default.src) def validate_src_is_required(self): - if self.sources or self.default.list_transformers or self.default.desc: + if self.sources or self.default.list_transformers or self.default.desc or self.default.generate_config: return print("No source path provided. Run robotidy --help to see how to use robotidy") sys.exit(1) diff --git a/robotidy/exceptions.py b/robotidy/exceptions.py index 43f24913..eccb7c33 100644 --- a/robotidy/exceptions.py +++ b/robotidy/exceptions.py @@ -42,6 +42,14 @@ def __init__(self): super().__init__(msg) +class MissingOptionalTomliWDependencyError(RobotidyConfigError): + def __init__(self): + super().__init__( + f"Missing optional dependency: tomli_w. Install robotidy with extra `generate_config` " + f"profile:\n\npip install robotidy[generate_config]" + ) + + class NoSuchOptionError(NoSuchOption): def __init__(self, option_name: str, allowed_options: List[str]): rec_finder = utils.RecommendationFinder() diff --git a/robotidy/transformers/ReplaceEmptyValues.py b/robotidy/transformers/ReplaceEmptyValues.py index c8794582..41771678 100644 --- a/robotidy/transformers/ReplaceEmptyValues.py +++ b/robotidy/transformers/ReplaceEmptyValues.py @@ -35,6 +35,7 @@ class ReplaceEmptyValues(Transformer): ... value3 ``` """ + HANDLES_SKIP = frozenset({"skip_sections"}) def __init__(self, skip: Skip = None): diff --git a/setup.py b/setup.py index fa2af312..f3f9019f 100644 --- a/setup.py +++ b/setup.py @@ -59,6 +59,7 @@ "pylama", "pytest", "pre-commit", + "tomli_w==1.0.*", ], "doc": [ "sphinx", @@ -66,6 +67,7 @@ "sphinx-design", "sphinx-copybutton==0.5.2", ], + "generate_config": ["tomli_w==1.0.*"], }, entry_points={"console_scripts": ["robotidy=robotidy.cli:cli"]}, ) diff --git a/tests/utest/test_cli.py b/tests/utest/test_cli.py index 5b1e3c8b..41cca605 100644 --- a/tests/utest/test_cli.py +++ b/tests/utest/test_cli.py @@ -1,4 +1,5 @@ import os +import shutil from contextlib import contextmanager from pathlib import Path from unittest.mock import patch @@ -8,7 +9,7 @@ from robotidy import skip, utils from robotidy.config import RawConfig -from robotidy.files import DEFAULT_EXCLUDES, find_project_root, get_paths, read_pyproject_config +from robotidy.files import DEFAULT_EXCLUDES, find_project_root, get_paths, load_toml_file, read_pyproject_config from robotidy.transformers.aligners_core import AlignKeywordsTestsSection from robotidy.transformers.AlignSettingsSection import AlignSettingsSection @@ -22,7 +23,7 @@ def temporary_cwd(tmpdir): prev_cwd = Path.cwd() os.chdir(tmpdir) try: - yield + yield Path(tmpdir) finally: os.chdir(prev_cwd) @@ -477,3 +478,69 @@ def test_skip_options(self, tmp_path): if option in with_values: option_names.append("empty") run_tidy([*option_names, str(tmp_path)]) + + +class TestGenerateConfig: + def validate_generated_default_configuration( + self, config_path: Path, diff: bool, add_missing_enabled: bool, rename_variables_enabled: bool + ): + assert config_path.is_file() + config = load_toml_file(config_path) + configured_transformers = config["tool"]["robotidy"].pop("configure") + expected_config = { + "tool": { + "robotidy": { + "diff": diff, + "overwrite": True, + "verbose": False, + "separator": "space", + "spacecount": 4, + "line_length": 120, + "lineseparator": "native", + "skip_gitignore": False, + "ignore_git_dir": False, + } + } + } + assert expected_config == config + assert f"AddMissingEnd:enabled={add_missing_enabled}" in configured_transformers + assert f"RenameVariables:enabled={rename_variables_enabled}" in configured_transformers + + def test_generate_default_config(self, temporary_cwd): + config_path = temporary_cwd / "pyproject.toml" + run_tidy(["--generate-config"]) + self.validate_generated_default_configuration( + config_path, diff=False, add_missing_enabled=True, rename_variables_enabled=False + ) + + def test_generate_config_ignore_existing_config(self, temporary_cwd): + config_path = temporary_cwd / "pyproject.toml" + orig_config_path = TEST_DATA_DIR / "only_pyproject" / "pyproject.toml" + shutil.copy(orig_config_path, config_path) + run_tidy(["--generate-config"]) + self.validate_generated_default_configuration( + config_path, diff=False, add_missing_enabled=True, rename_variables_enabled=False + ) + + def test_generate_config_with_filename(self, temporary_cwd): + config_path = temporary_cwd / "different.txt" + run_tidy(["--generate-config", "different.txt"]) + self.validate_generated_default_configuration( + config_path, diff=False, add_missing_enabled=True, rename_variables_enabled=False + ) + + def test_generate_config_with_cli_config(self, temporary_cwd): + config_path = temporary_cwd / "pyproject.toml" + run_tidy(["--generate-config", "--diff", "--transform", "RenameVariables"]) + self.validate_generated_default_configuration( + config_path, diff=True, add_missing_enabled=False, rename_variables_enabled=True + ) + + def test_missing_dependency(self, monkeypatch, temporary_cwd): + with patch.dict("sys.modules", {"tomli_w": None}): + result = run_tidy(["--generate-config"], exit_code=1) + expected_output = ( + "Error: Missing optional dependency: tomli_w. Install robotidy with extra `generate_config` " + "profile:\n\npip install robotidy[generate_config]\n" + ) + assert result.output == expected_output