Repository: NextGenContributions/nitpick
Issue category: Feature Request

Issue:
Rework support for comma separated values config
- flake8 config has comma separated string value that can't be properly parsed and requires https://nitpick.readthedocs.io/en/latest/nitpick_section.html#comma-separated-values
- We prefer `.flake8` config over `setup.cfg` for clarity and nitpick has implemented `comma-separated-values` feature by hardcoding support only for `setup.cfg` in files section schema. This wasn't mentioned in the documentation and caused failure when trying to configure `.flake8` style
- The implementation is the inverse of how nitpick files section config should be done. A list of files should be listed per config e.g. `absent`, `present`, `comma_separated_values` instead of using hardcoded filename i.e. `[nitpick.files."setup.cfg"]` ref: https://nitpick.readthedocs.io/en/latest/nitpick_section.html#files-that-should-exist and https://nitpick.readthedocs.io/en/latest/nitpick_section.html#files-that-should-be-deleted


Hints:

Patch:
diff --git a/docs/nitpick_section.rst b/docs/nitpick_section.rst
index 768db54b..2d3b8c56 100644
--- a/docs/nitpick_section.rst
+++ b/docs/nitpick_section.rst
@@ -50,15 +50,15 @@ The message is optional.
 Comma separated values
 ^^^^^^^^^^^^^^^^^^^^^^
 
-On ``setup.cfg``, some keys are lists of multiple values separated by commas, like ``flake8.ignore``.
+On ``setup.cfg`` or ``.flake8``, some keys are lists of multiple values separated by commas, like ``flake8.ignore``.
 
 On the style file, it's possible to indicate which key/value pairs should be treated as multiple values instead of an exact string.
 Multiple keys can be added.
 
 .. code-block:: toml
 
-    [nitpick.files."setup.cfg"]
-    comma_separated_values = ["flake8.ignore", "isort.some_key", "another_section.another_key"]
+    [nitpick.files.comma_separated_values]
+    "setup.cfg" = ["flake8.ignore", "isort.some_key", "another_section.another_key"]
 
 [nitpick.styles]
 ----------------
diff --git a/src/nitpick/plugins/base.py b/src/nitpick/plugins/base.py
index 6c5eabdd..be9af9b8 100644
--- a/src/nitpick/plugins/base.py
+++ b/src/nitpick/plugins/base.py
@@ -9,10 +9,9 @@
 from autorepr import autotext
 from loguru import logger
 
-from nitpick.blender import BaseDoc, flatten_quotes, search_json
+from nitpick.blender import BaseDoc, flatten_quotes
 from nitpick.config import SpecialConfig
 from nitpick.constants import CONFIG_DUNDER_LIST_KEYS
-from nitpick.typedefs import JsonDict, mypy_property
 from nitpick.violations import Fuss, Reporter, SharedViolations
 
 if TYPE_CHECKING:
@@ -21,6 +20,7 @@
     from marshmallow import Schema
 
     from nitpick.plugins.info import FileInfo
+    from nitpick.typedefs import JsonDict
 
 
 class NitpickPlugin(metaclass=abc.ABCMeta):  # pylint: disable=too-many-instance-attributes
@@ -89,23 +89,11 @@ def predefined_special_config(self) -> SpecialConfig:  # pylint: disable=no-self
         """
         return SpecialConfig()
 
-    @mypy_property
-    def nitpick_file_dict(self) -> JsonDict:
-        """Nitpick configuration for this file as a TOML dict, taken from the style file."""
-        return search_json(self.info.project.nitpick_section, f'files."{self.filename}"', {})
-
     def entry_point(self) -> Iterator[Fuss]:
         """Entry point of the Nitpick plugin."""
         self.post_init()
 
-        should_exist: bool = bool(self.info.project.nitpick_files_section.get(self.filename, True))
-        if self.file_path.exists() and not should_exist:
-            logger.info(f"{self}: File {self.filename} exists when it should not")
-            # Only display this message if the style is valid.
-            yield self.reporter.make_fuss(SharedViolations.DELETE_FILE)
-            return
-
-        has_config_dict = bool(self.expected_config or self.nitpick_file_dict)
+        has_config_dict = bool(self.expected_config)
         if not has_config_dict:
             return
 
diff --git a/src/nitpick/plugins/ini.py b/src/nitpick/plugins/ini.py
index 2d2f8484..e1fc86d2 100644
--- a/src/nitpick/plugins/ini.py
+++ b/src/nitpick/plugins/ini.py
@@ -11,6 +11,7 @@
 
 from nitpick.plugins import hookimpl
 from nitpick.plugins.base import NitpickPlugin
+from nitpick.typedefs import JsonDict, mypy_property
 from nitpick.violations import Fuss, ViolationEnum
 
 if TYPE_CHECKING:
@@ -55,10 +56,15 @@ class IniPlugin(NitpickPlugin):
     updater: ConfigUpdater
     comma_separated_values: set[str]
 
+    @mypy_property
+    def comma_separated_values_dict(self) -> JsonDict:
+        """TOML dict of files which define which config entry is a list of comma separated values."""
+        return self.info.project.nitpick_files_section.get(COMMA_SEPARATED_VALUES, {})
+
     def post_init(self):
         """Post initialization after the instance was created."""
         self.updater = ConfigUpdater()
-        self.comma_separated_values = set(self.nitpick_file_dict.get(COMMA_SEPARATED_VALUES, []))
+        self.comma_separated_values = set(self.comma_separated_values_dict.get(self.filename, []))
 
         if not self.needs_top_section:
             return
diff --git a/src/nitpick/resources/python/flake8.toml b/src/nitpick/resources/python/flake8.toml
index d0bbef57..d5cfe826 100644
--- a/src/nitpick/resources/python/flake8.toml
+++ b/src/nitpick/resources/python/flake8.toml
@@ -24,5 +24,5 @@ additional_dependencies = ["flake8-blind-except", "flake8-bugbear", "flake8-comp
 [".codeclimate.yml".plugins.pep8]  # https://docs.codeclimate.com/docs/pep8 PEP8 already being checked by flake8 plugins on pre-commit
 enabled = false
 
-[nitpick.files."setup.cfg"]
-comma_separated_values = ["flake8.ignore", "flake8.exclude"]
+[nitpick.files.comma_separated_values]
+"setup.cfg" = ["flake8.ignore", "flake8.exclude"]
diff --git a/src/nitpick/resources/python/isort.toml b/src/nitpick/resources/python/isort.toml
index 8cae5606..294bd8c7 100644
--- a/src/nitpick/resources/python/isort.toml
+++ b/src/nitpick/resources/python/isort.toml
@@ -21,5 +21,5 @@ repo = "https://github.com/PyCQA/isort"
 [[".pre-commit-config.yaml".repos.hooks]]
 id = "isort"
 
-[nitpick.files."setup.cfg"]
-comma_separated_values = ["isort.skip", "isort.known_first_party"]
+[nitpick.files.comma_separated_values]
+"setup.cfg" = ["isort.skip", "isort.known_first_party"]
diff --git a/src/nitpick/schemas.py b/src/nitpick/schemas.py
index 745947a6..b14f2db8 100644
--- a/src/nitpick/schemas.py
+++ b/src/nitpick/schemas.py
@@ -8,7 +8,7 @@
 
 from nitpick import fields
 from nitpick.blender import flatten_quotes
-from nitpick.constants import PYTHON_SETUP_CFG, READ_THE_DOCS_URL
+from nitpick.constants import READ_THE_DOCS_URL
 
 
 def flatten_marshmallow_errors(errors: dict) -> str:
@@ -46,25 +46,18 @@ class NitpickStylesSectionSchema(BaseNitpickSchema):
     include = PolyField(deserialization_schema_selector=fields.string_or_list_field)
 
 
-class IniSchema(BaseNitpickSchema):
-    """Validation schema for INI files."""
-
-    error_messages = {  # noqa: RUF012
-        "unknown": help_message("Unknown configuration", "nitpick_section.html#comma-separated-values")
-    }
-
-    comma_separated_values = fields.List(fields.String(validate=fields.validate_section_dot_field))
-
-
 class NitpickFilesSectionSchema(BaseNitpickSchema):
     """Validation schema for the ``[nitpick.files]`` section on the style file."""
 
-    error_messages = {"unknown": help_message("Unknown file", "nitpick_section.html#nitpick-files")}  # noqa: RUF012
+    error_messages = {  # noqa: RUF012
+        "unknown": help_message("Unknown configuration", "nitpick_section.html#nitpick-files")
+    }
 
     absent = fields.Dict(fields.NonEmptyString, fields.String())
     present = fields.Dict(fields.NonEmptyString, fields.String())
-    # TODO: refactor: load this schema dynamically, then add this next field setup_cfg
-    setup_cfg = fields.Nested(IniSchema, data_key=PYTHON_SETUP_CFG)
+    comma_separated_values = fields.Dict(
+        fields.NonEmptyString, fields.List(fields.String(validate=fields.validate_section_dot_field))
+    )
 
 
 class NitpickMetaSchema(BaseNitpickSchema):


Test patch:
diff --git a/tests/test_builtin/real.toml b/tests/test_builtin/real.toml
index aa41462e..fbf2db94 100644
--- a/tests/test_builtin/real.toml
+++ b/tests/test_builtin/real.toml
@@ -36,5 +36,5 @@ include = [
     "py://nitpick/resources/javascript/package-json",
 ]
 
-[nitpick.files."setup.cfg"]
-comma_separated_values = ["flake8.ignore", "flake8.exclude", "isort.skip", "isort.known_first_party"]
+[nitpick.files.comma_separated_values]
+"setup.cfg" = ["flake8.ignore", "flake8.exclude", "isort.skip", "isort.known_first_party"]
diff --git a/tests/test_ini.py b/tests/test_ini.py
index 34303558..f2a11a78 100644
--- a/tests/test_ini.py
+++ b/tests/test_ini.py
@@ -6,7 +6,7 @@
 from configupdater import ConfigUpdater
 
 from nitpick.constants import EDITOR_CONFIG, PYTHON_SETUP_CFG
-from nitpick.plugins.ini import IniPlugin, Violations
+from nitpick.plugins.ini import COMMA_SEPARATED_VALUES, IniPlugin, Violations
 from nitpick.violations import Fuss, SharedViolations
 from tests.helpers import ProjectMock
 
@@ -20,8 +20,8 @@ def test_comma_separated_keys_on_style_file(tmp_path):
     """Comma separated keys on the style file."""
     ProjectMock(tmp_path).style(
         f"""
-        [nitpick.files."{PYTHON_SETUP_CFG}"]
-        comma_separated_values = ["food.eat", "food.drink"]
+        [nitpick.files.{COMMA_SEPARATED_VALUES}]
+        "{PYTHON_SETUP_CFG}" = ["food.eat", "food.drink"]
 
         ["{PYTHON_SETUP_CFG}".food]
         eat = "salt,ham,eggs"
@@ -301,9 +301,7 @@ def test_missing_different_values_editorconfig_with_root(tmp_path, datadir):
             another_missing = 100
             """,
         ),
-    ).assert_file_contents(
-        EDITOR_CONFIG, datadir / "2-expected-editorconfig.ini"
-    )
+    ).assert_file_contents(EDITOR_CONFIG, datadir / "2-expected-editorconfig.ini")
 
 
 def test_invalid_configuration_comma_separated_values(tmp_path):
@@ -316,8 +314,8 @@ def test_invalid_configuration_comma_separated_values(tmp_path):
         ignore = "D100,D101,D102,D103,D104,D105,D106,D107,D202,E203,W503"
         select = "E241,C,E,F,W,B,B9"
 
-        [nitpick.files."{PYTHON_SETUP_CFG}"]
-        comma_separated_values = ["flake8.ignore", "flake8.exclude"]
+        [nitpick.files.{COMMA_SEPARATED_VALUES}]
+        "{PYTHON_SETUP_CFG}" = ["flake8.ignore", "flake8.exclude"]
         """
     ).api_check().assert_violations(
         Fuss(
@@ -340,8 +338,8 @@ def test_invalid_section_dot_fields(tmp_path):
     """Test invalid section/field pairs."""
     ProjectMock(tmp_path).style(
         f"""
-        [nitpick.files."{PYTHON_SETUP_CFG}"]
-        comma_separated_values = ["no_dot", "multiple.dots.here", ".filed_only", "section_only."]
+        [nitpick.files.{COMMA_SEPARATED_VALUES}]
+        "{PYTHON_SETUP_CFG}" = ["no_dot", "multiple.dots.here", ".filed_only", "section_only."]
         """
     ).setup_cfg("").api_check().assert_violations(
         Fuss(
@@ -350,10 +348,10 @@ def test_invalid_section_dot_fields(tmp_path):
             1,
             " has an incorrect style. Invalid config:",
             f"""
-            nitpick.files."{PYTHON_SETUP_CFG}".comma_separated_values.0: Dot is missing. Use <section_name>.<field_name>
-            nitpick.files."{PYTHON_SETUP_CFG}".comma_separated_values.1: There's more than one dot. Use <section_name>.<field_name>
-            nitpick.files."{PYTHON_SETUP_CFG}".comma_separated_values.2: Empty section name. Use <section_name>.<field_name>
-            nitpick.files."{PYTHON_SETUP_CFG}".comma_separated_values.3: Empty field name. Use <section_name>.<field_name>
+            nitpick.files.{COMMA_SEPARATED_VALUES}."{PYTHON_SETUP_CFG}".value.0: Dot is missing. Use <section_name>.<field_name>
+            nitpick.files.{COMMA_SEPARATED_VALUES}."{PYTHON_SETUP_CFG}".value.1: There's more than one dot. Use <section_name>.<field_name>
+            nitpick.files.{COMMA_SEPARATED_VALUES}."{PYTHON_SETUP_CFG}".value.2: Empty section name. Use <section_name>.<field_name>
+            nitpick.files.{COMMA_SEPARATED_VALUES}."{PYTHON_SETUP_CFG}".value.3: Empty field name. Use <section_name>.<field_name>
             """,
         )
     )
@@ -368,8 +366,8 @@ def test_invalid_sections_comma_separated_values(tmp_path):
         exclude = "venv*,**/migrations/"
         per-file-ignores = "tests/**.py:FI18,setup.py:FI18"
 
-        [nitpick.files."{PYTHON_SETUP_CFG}"]
-        comma_separated_values = ["flake8.ignore", "flake8.exclude", "falek8.per-file-ignores", "aaa.invalid-section"]
+        [nitpick.files.{COMMA_SEPARATED_VALUES}]
+        "{PYTHON_SETUP_CFG}" = ["flake8.ignore", "flake8.exclude", "falek8.per-file-ignores", "aaa.invalid-section"]
         """
     ).setup_cfg(
         """
@@ -401,9 +399,7 @@ def test_multiline_comment(tmp_path, datadir):
             new = value
             """,
         )
-    ).assert_file_contents(
-        PYTHON_SETUP_CFG, datadir / "3-expected-setup.cfg"
-    )
+    ).assert_file_contents(PYTHON_SETUP_CFG, datadir / "3-expected-setup.cfg")
 
 
 def test_duplicated_option(tmp_path):
@@ -427,9 +423,7 @@ def test_duplicated_option(tmp_path):
             f": parsing error (DuplicateOptionError): While reading from {project.path_for(PYTHON_SETUP_CFG)!r} "
             f"[line  3]: option 'easy' in section 'abc' already exists",
         )
-    ).assert_file_contents(
-        PYTHON_SETUP_CFG, original_file
-    )
+    ).assert_file_contents(PYTHON_SETUP_CFG, original_file)
 
 
 @mock.patch.object(ConfigUpdater, "update_file")
@@ -463,9 +457,7 @@ def test_simulate_parsing_error_when_saving(update_file, tmp_path):
             Violations.PARSING_ERROR.code,
             ": parsing error (ParsingError): Source contains parsing errors: 'simulating a captured error'",
         ),
-    ).assert_file_contents(
-        PYTHON_SETUP_CFG, original_file
-    )
+    ).assert_file_contents(PYTHON_SETUP_CFG, original_file)
 
 
 def test_generic_ini_with_missing_header(tmp_path):
@@ -493,9 +485,7 @@ def test_generic_ini_with_missing_header(tmp_path):
             f"file: {project.path_for('generic.ini')!r}, line: 1\n"
             "'this_key_is_invalid = for a generic .ini (it should always have a section)\\n'",
         )
-    ).assert_file_contents(
-        "generic.ini", expected_generic_ini
-    )
+    ).assert_file_contents("generic.ini", expected_generic_ini)
 
 
 def test_falsy_values_should_be_reported_and_fixed(tmp_path, datadir):
diff --git a/tests/test_plugin.py b/tests/test_plugin.py
index 645c12c3..9d35e7b8 100644
--- a/tests/test_plugin.py
+++ b/tests/test_plugin.py
@@ -68,7 +68,7 @@ def test_missing_message(tmp_path):
             "nitpick-style.toml",
             1,
             " has an incorrect style. Invalid config:",
-            f"""nitpick.files."pyproject.toml": Unknown file. See {READ_THE_DOCS_URL}nitpick_section.html#nitpick-files.""",
+            f"""nitpick.files."pyproject.toml": Unknown configuration. See {READ_THE_DOCS_URL}nitpick_section.html#nitpick-files.""",
         )
     )
 
diff --git a/tests/test_style.py b/tests/test_style.py
index 1763a66c..c9886098 100644
--- a/tests/test_style.py
+++ b/tests/test_style.py
@@ -55,9 +55,7 @@ def test_multiple_styles_overriding_values(offline, tmp_path):
         [tool.black]
         something = 22
         """
-    ).flake8(
-        offline=offline
-    ).assert_errors_contain(
+    ).flake8(offline=offline).assert_errors_contain(
         f"""
         NIP318 File pyproject.toml has missing values:{SUGGESTION_BEGIN}
         [tool.black]
@@ -136,9 +134,7 @@ def test_include_styles_overriding_values(offline, tmp_path):
         [tool.nitpick]
         style = "isort1"
         """
-    ).flake8(
-        offline=offline
-    ).assert_errors_contain(
+    ).flake8(offline=offline).assert_errors_contain(
         f"""
         NIP318 File pyproject.toml has missing values:{SUGGESTION_BEGIN}
         [tool.black]
@@ -185,9 +181,7 @@ def test_minimum_version(mocked_version, offline, tmp_path):
         [tool.black]
         line-length = 100
         """
-    ).flake8(
-        offline=offline
-    ).assert_single_error(
+    ).flake8(offline=offline).assert_single_error(
         "NIP203 The style file you're using requires nitpick>=1.0 (you have 0.5.3). Please upgrade"
     )
 
@@ -286,16 +280,12 @@ def test_symlink_subdir(offline, tmp_path):
         ["pyproject.toml".tool.black]
         line-length = 86
         """,
-    ).create_symlink(
-        "symlinked-style.toml", target_dir, "parent.toml"
-    ).pyproject_toml(
+    ).create_symlink("symlinked-style.toml", target_dir, "parent.toml").pyproject_toml(
         """
         [tool.nitpick]
         style = "symlinked-style"
         """
-    ).flake8(
-        offline=offline
-    ).assert_single_error(
+    ).flake8(offline=offline).assert_single_error(
         f"""
         NIP318 File pyproject.toml has missing values:{SUGGESTION_BEGIN}
         [tool.black]
@@ -666,9 +656,7 @@ def test_merge_styles_into_single_file(offline, tmp_path):
         [tool.nitpick]
         style = ["black", "isort", "isort_overrides"]
         """
-    ).flake8(
-        offline=offline
-    ).assert_merged_style(
+    ).flake8(offline=offline).assert_merged_style(
         f'''
         ["pyproject.toml".tool.black]
         line-length = 120
@@ -774,9 +762,7 @@ def test_invalid_nitpick_files(offline, tmp_path):
         [tool.nitpick]
         style = ["some_style", "wrong_files"]
         """
-    ).flake8(
-        offline=offline
-    ).assert_errors_contain(
+    ).flake8(offline=offline).assert_errors_contain(
         f"""
         NIP001 File some_style.toml has an incorrect style. Invalid config:{SUGGESTION_BEGIN}
         xxx: Unknown file. See {READ_THE_DOCS_URL}plugins.html.{SUGGESTION_END}
@@ -784,7 +770,7 @@ def test_invalid_nitpick_files(offline, tmp_path):
     ).assert_errors_contain(
         f"""
         NIP001 File wrong_files.toml has an incorrect style. Invalid config:{SUGGESTION_BEGIN}
-        nitpick.files.whatever: Unknown file. See {READ_THE_DOCS_URL}nitpick_section.html#nitpick-files.{SUGGESTION_END}
+        nitpick.files.whatever: Unknown configuration. See {READ_THE_DOCS_URL}nitpick_section.html#nitpick-files.{SUGGESTION_END}
         """,
         2,
     )
@@ -961,3 +947,45 @@ def test_protocol_not_supported(tmp_path):
     with pytest.raises(RuntimeError) as exc_info:
         project.api_check()
     assert str(exc_info.value) == "URL protocol 'abc' is not supported"
+
+
+def test_old_comma_separated_values_config_should_be_invalid(tmp_path):
+    """Invalid [nitpick.files] section."""
+    ProjectMock(tmp_path).named_style(
+        "some_style",
+        """
+        [nitpick.files.".flake8"]
+        comma_separated_values = [
+           "flake8.ignore",
+        ]
+        """,
+    ).pyproject_toml(
+        """
+        [tool.nitpick]
+        style = ["some_style"]
+        """
+    ).flake8().assert_errors_contain(
+        f"""
+        NIP001 File some_style.toml has an incorrect style. Invalid config:{SUGGESTION_BEGIN}
+        nitpick.files.".flake8": Unknown configuration. See {READ_THE_DOCS_URL}nitpick_section.html#nitpick-files.{SUGGESTION_END}
+        """,
+        1,
+    )
+
+
+def test_new_comma_separated_values_config_should_be_valid(tmp_path):
+    """Valid [nitpick.files] section."""
+    ProjectMock(tmp_path).named_style(
+        "some_style",
+        """
+        [nitpick.files.comma_separated_values]
+        ".flake8" = [
+           "flake8.ignore",
+        ]
+        """,
+    ).pyproject_toml(
+        """
+        [tool.nitpick]
+        style = ["some_style"]
+        """
+    ).flake8().assert_no_errors()