diff --git a/docs/cli.rst b/docs/cli.rst index 9a7f2a26..80b492c9 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -27,6 +27,8 @@ The available commands are described below. .. auto-generated-from-here +.. _cli_cmd: + Main options ------------ @@ -45,8 +47,11 @@ Main options --help Show this message and exit. Commands: - ls List of files configured in the Nitpick style. - run Apply suggestions to configuration files. + init Create a configuration file if it doesn't exist already. + ls List of files configured in the Nitpick style. + run Apply suggestions to configuration files. + +.. _cli_cmd_run: ``run``: Apply style to files ----------------------------- @@ -73,6 +78,8 @@ At the end of execution, this command displays: -v, --verbose Verbose logging --help Show this message and exit. +.. _cli_cmd_ls: + ``ls``: List configures files ----------------------------- @@ -88,3 +95,18 @@ At the end of execution, this command displays: Options: --help Show this message and exit. + +.. _cli_cmd_init: + +``init``: Initialise a configuration file +----------------------------------------- + + +.. code-block:: + + Usage: nitpick init [OPTIONS] + + Create a configuration file if it doesn't exist already. + + Options: + --help Show this message and exit. diff --git a/docs/configuration.rst b/docs/configuration.rst index 72d1918d..90ef755a 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -16,7 +16,9 @@ Possible configuration files (in order of precedence): The first file found will be used; the other files will be ignored. -You can configure your own style like this: +Run the ``nipick init`` CLI command to create a config file (:ref:`cli_cmd_init`). + +To configure your own style: .. code-block:: toml diff --git a/docs/generate_rst.py b/docs/generate_rst.py index 7b23b241..75bc3e6e 100644 --- a/docs/generate_rst.py +++ b/docs/generate_rst.py @@ -64,6 +64,7 @@ """, ), ("ls", "List configures files", ""), + ("init", "Initialise a configuration file", ""), ] nit = Nitpick.singleton().init() @@ -197,6 +198,8 @@ def generate_plugins(filename: str) -> int: def generate_cli(filename: str) -> int: """Generate CLI docs.""" template = """ + .. _cli_cmd{anchor}: + {header} {dashes} {long} @@ -209,6 +212,7 @@ def generate_cli(filename: str) -> int: blocks = [] for command, short, long in CLI_MAPPING: + anchor = f"_{command}" if command else "" header = f"``{command}``: {short}" if command else short blocks.append("") parts = ["nitpick"] @@ -219,7 +223,7 @@ def generate_cli(filename: str) -> int: output = check_output(parts).decode().strip() # nosec blocks.append( clean_template.format( - header=header, dashes="-" * len(header), long=dedent(long), help=indent(output, " ") + anchor=anchor, header=header, dashes="-" * len(header), long=dedent(long), help=indent(output, " ") ) ) diff --git a/src/nitpick/cli.py b/src/nitpick/cli.py index d1ae1455..50c44618 100644 --- a/src/nitpick/cli.py +++ b/src/nitpick/cli.py @@ -19,7 +19,7 @@ from click.exceptions import Exit from loguru import logger -from nitpick.constants import PROJECT_NAME +from nitpick.constants import DOT_NITPICK_TOML, PROJECT_NAME from nitpick.core import Nitpick from nitpick.enums import OptionEnum from nitpick.exceptions import QuitComplainingError @@ -114,3 +114,17 @@ def ls(context, files): # pylint: disable=invalid-name # TODO: test API .configured_files for file in nit.configured_files(*files): click.secho(relative_to_current_dir(file), fg="green" if file.exists() else "red") + + +@nitpick_cli.command() +@click.pass_context +def init(context): + """Create a configuration file if it doesn't exist already.""" + nit = get_nitpick(context) + config = nit.project.read_configuration() + if config.file: + click.secho(f"A config file already exists: {config.file.name}", fg="yellow") + raise Exit(1) + + nit.project.create_configuration() + click.secho(f"Config file created: {DOT_NITPICK_TOML}", fg="green") diff --git a/src/nitpick/project.py b/src/nitpick/project.py index 2e7b8836..3ba8dc71 100644 --- a/src/nitpick/project.py +++ b/src/nitpick/project.py @@ -10,14 +10,18 @@ from loguru import logger from marshmallow_polyfield import PolyField from pluggy import PluginManager +from tomlkit import comment, document, dumps, table +from tomlkit.items import Key, KeyType from nitpick import fields, plugins from nitpick.constants import ( CONFIG_FILES, + DOT_NITPICK_TOML, MANAGE_PY, NITPICK_MINIMUM_VERSION_JMEX, PROJECT_NAME, PYPROJECT_TOML, + READ_THE_DOCS_URL, ROOT_FILES, ROOT_PYTHON_FILES, TOOL_NITPICK, @@ -233,3 +237,15 @@ def merge_styles(self, offline: bool) -> Iterator[Fuss]: self.nitpick_section = self.style_dict.get("nitpick", {}) self.nitpick_files_section = self.nitpick_section.get("files", {}) + + def create_configuration(self) -> None: + """Create a configuration file.""" + from nitpick.style import Style # pylint: disable=import-outside-toplevel + + doc = document() + doc.add(comment("This file was generated by the `nitpick init` command")) + doc.add(comment(f"More info at {READ_THE_DOCS_URL}configuration.html")) + doc.add(Key(TOOL_NITPICK, KeyType.Bare), table().add("style", [Style.get_default_style_url()])) + + path: Path = self.root / DOT_NITPICK_TOML + path.write_text(dumps(doc, sort_keys=True)) diff --git a/tests/helpers.py b/tests/helpers.py index e07d5965..6f45951e 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -353,10 +353,17 @@ def cli_run( compare(actual=actual, expected=expected) return self - def cli_ls(self, str_or_lines: StrOrList, exit_code: int = None): + def cli_ls(self, str_or_lines: StrOrList, *, exit_code: int = None) -> "ProjectMock": """Run the ls command and assert the output.""" result, actual, expected = self._simulate_cli("ls", str_or_lines, exit_code=exit_code) compare(actual=actual, expected=expected, prefix=f"Result: {result}") + return self + + def cli_init(self, str_or_lines: StrOrList, *, exit_code: int = None) -> "ProjectMock": + """Run the init command and assert the output.""" + result, actual, expected = self._simulate_cli("init", str_or_lines, exit_code=exit_code) + compare(actual=actual, expected=expected, prefix=f"Result: {result}") + return self def assert_file_contents(self, *name_contents: Union[PathOrStr, str]): """Assert the file has the expected contents.""" diff --git a/tests/test_cli.py b/tests/test_cli.py index 6651f7bf..82796aaa 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,4 +1,8 @@ """CLI tests.""" +import pytest + +from nitpick.constants import DOT_NITPICK_TOML, PYPROJECT_TOML, READ_THE_DOCS_URL, TOOL_NITPICK +from nitpick.style import Style from tests.helpers import XFAIL_ON_WINDOWS, ProjectMock @@ -28,3 +32,25 @@ def test_simple_error(tmp_path): line-length = 100 """ ) + + +@pytest.mark.parametrize("config_file", [DOT_NITPICK_TOML, PYPROJECT_TOML]) +def test_config_file_already_exists(tmp_path, config_file): + """Test if .nitpick.toml already exists.""" + project = ProjectMock(tmp_path, pyproject_toml=False, setup_py=True).save_file(config_file, "") + project.cli_init(f"A config file already exists: {config_file}", exit_code=1) + + +def test_create_basic_dot_nitpick_toml(tmp_path): + """If no config file is found, create a basic .nitpick.toml.""" + project = ProjectMock(tmp_path, pyproject_toml=False, setup_py=True) + project.cli_init(f"Config file created: {DOT_NITPICK_TOML}").assert_file_contents( + DOT_NITPICK_TOML, + f""" + # This file was generated by the `nitpick init` command + # More info at {READ_THE_DOCS_URL}configuration.html + + [{TOOL_NITPICK}] + style = ["{Style.get_default_style_url()}"] + """, + )