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..4d8d5cfa 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 @@ -21,6 +22,16 @@ def temporary_cwd(tmpdir): prev_cwd = Path.cwd() os.chdir(tmpdir) + try: + yield Path(tmpdir) + finally: + os.chdir(prev_cwd) + + +@contextmanager +def switch_cwd(new_cwd): + prev_cwd = Path.cwd() + os.chdir(new_cwd) try: yield finally: @@ -477,3 +488,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