Skip to content

Commit 1c41efb

Browse files
feat: Support explicit file tags (#30)
Co-authored-by: deepsource-autofix[bot] <62050782+deepsource-autofix[bot]@users.noreply.github.com>
1 parent 339b983 commit 1c41efb

File tree

6 files changed

+96
-20
lines changed

6 files changed

+96
-20
lines changed

docs/nitpick_section.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ To enforce that certain files should not exist in the project, you can add them
4747
Multiple files can be configured as above.
4848
The message is optional.
4949

50+
Files that are unknown
51+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
52+
53+
Files that don't have a valid extension nor recognized by ``identity`` lib can end up causing unknown file error in ``nitpick``. In such cases, you can explicitly declare the plugins (tags) to use for such files.
54+
55+
.. code-block:: toml
56+
57+
[nitpick.files.tags]
58+
".shellcheckrc" = ["ini"]
59+
5060
Comma separated values
5161
^^^^^^^^^^^^^^^^^^^^^^
5262

src/nitpick/core.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,7 @@
4242
from nitpick.generic import filter_names, glob_files, glob_non_ignored_files, relative_to_current_dir
4343
from nitpick.plugins.info import FileInfo
4444
from nitpick.schemas import BaseNitpickSchema, flatten_marshmallow_errors, help_message
45-
from nitpick.style import (
46-
BuiltinStyle,
47-
StyleManager,
48-
builtin_styles,
49-
)
45+
from nitpick.style import BuiltinStyle, StyleManager, builtin_styles
5046
from nitpick.violations import Fuss, ProjectViolations, Reporter, StyleViolations
5147

5248
if TYPE_CHECKING:
@@ -147,7 +143,8 @@ def enforce_style(self, *partial_names: str, autofix=True) -> Iterator[Fuss]:
147143
logger.debug(f"{config_key}: Finding plugins to enforce style")
148144

149145
# 2.
150-
info = FileInfo.create(self.project, config_key)
146+
tags = self.project.nitpick_files_section.get("tags", {}).get(config_key, [])
147+
info = FileInfo.create(self.project, config_key, tags)
151148
# pylint: disable=no-member
152149
for plugin_class in self.project.plugin_manager.hook.can_handle(info=info):
153150
yield from plugin_class(info, config_dict, autofix).entry_point()

src/nitpick/plugins/info.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from dataclasses import dataclass, field
66
from pathlib import Path
7-
from typing import TYPE_CHECKING
7+
from typing import TYPE_CHECKING, Iterable
88

99
from identify import identify
1010
from loguru import logger
@@ -27,21 +27,27 @@ class FileInfo:
2727
tags: set[str] = field(default_factory=set)
2828

2929
@classmethod
30-
def create(cls, project: Project, path_from_root: str) -> FileInfo:
30+
def create(cls, project: Project, path_from_root: str, tags: Iterable[str] | None = None) -> FileInfo:
3131
"""Clean the file name and get its tags."""
3232
if Deprecation.pre_commit_without_dash(path_from_root):
3333
clean_path = DOT + path_from_root
3434
else:
3535
clean_path = DOT + path_from_root[1:] if path_from_root.startswith("-") else path_from_root
36-
tags = FileInfo.tags_from_filename(clean_path)
36+
37+
if not tags:
38+
# Auto-detect a list of tags associated with the file
39+
tags = identify.tags_from_filename(clean_path)
40+
41+
tags = FileInfo.remove_conflicting_tags(clean_path, tags)
3742
return cls(project, clean_path, tags)
3843

3944
@staticmethod
40-
def tags_from_filename(path: str) -> set[str]:
41-
"""Get a list of tags associated with the file."""
42-
tags = identify.tags_from_filename(path)
45+
def remove_conflicting_tags(path: str, input_tags: Iterable[str] | None) -> set[str]:
46+
"""Check and remove conflicting tags, there can be only one of them."""
47+
if not input_tags:
48+
return set()
4349

44-
# Check for conflicting tags, there can be only one of them
50+
tags = set(input_tags)
4551
found_tags = CONFLICTING_TAGS.intersection(tags)
4652
if len(found_tags) > 1 and (ext := Path(path).suffix):
4753
# The file has a valid extension and not just some ".dotfile"

src/nitpick/schemas.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ class NitpickFilesSectionSchema(BaseNitpickSchema):
5555

5656
absent = fields.Dict(fields.NonEmptyString, fields.String())
5757
present = fields.Dict(fields.NonEmptyString, fields.String())
58+
tags = fields.Dict(fields.NonEmptyString, fields.List(fields.String()))
5859
comma_separated_values = fields.Dict(
5960
fields.NonEmptyString, fields.List(fields.String(validate=fields.validate_section_dot_field))
6061
)

src/nitpick/style.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,8 @@ def validate(self, config_dict: dict) -> tuple[dict, dict]:
346346
validation_errors = {}
347347
toml_dict = {}
348348
for key, value_dict in config_dict.items():
349-
info = FileInfo.create(self.project, key)
349+
tags = toml_dict.get("nitpick", {}).get("files", {}).get("tags", {}).get(key, [])
350+
info = FileInfo.create(self.project, key, tags)
350351
toml_dict[info.path_from_root] = value_dict
351352
validation_errors.update(self._validate_item(key, info, value_dict))
352353
return toml_dict, validation_errors
@@ -803,10 +804,7 @@ def from_path(cls, resource_path: Path, library_dir: Path | None = None) -> Buil
803804
if library_dir:
804805
# Style in a directory
805806
from_resources_root = without_suffix.relative_to(library_dir)
806-
bis = BuiltinStyle(
807-
formatted=str(without_suffix),
808-
path_from_resources_root=from_resources_root.as_posix(),
809-
)
807+
bis = BuiltinStyle(formatted=str(without_suffix), path_from_resources_root=from_resources_root.as_posix())
810808
else:
811809
# Style from the built-in library
812810
package_path = resource_path.relative_to(builtin_resources_root().parent.parent)

tests/test_generic.py

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
import pytest
1313
from furl import furl
14+
from src.nitpick.plugins.ini import Violations
15+
from src.nitpick.violations import Fuss
1416
from testfixtures import compare
1517

1618
from nitpick.constants import EDITOR_CONFIG, GIT_DIR, GIT_IGNORE, PYTHON_TOX_INI
@@ -22,6 +24,7 @@
2224
relative_to_current_dir,
2325
)
2426
from nitpick.plugins import FileInfo
27+
from tests.helpers import ProjectMock
2528

2629

2730
@mock.patch.object(Path, "cwd")
@@ -177,7 +180,7 @@ def test_error_when_calling_git_config(mock_check_output, capsys, exception: Exc
177180
("foo/pylintrc", {"pylintrc", "text", "ini"}),
178181
],
179182
)
180-
def test_different_types_of_pylintrc_config_should_work(filepath: str, expected_tags: set[str]):
183+
def test_different_types_of_pylintrc_config_should_work(filepath: str, expected_tags: set[str], tmp_path: Path):
181184
"""Different pylintrc configs should have different expected tags.
182185
183186
'pylintrc' is a special case where the config can either be a TOML or INI file.
@@ -187,4 +190,65 @@ def test_different_types_of_pylintrc_config_should_work(filepath: str, expected_
187190
More on how pylint search for and use configuration files:
188191
https://pylint.pycqa.org/en/latest/user_guide/usage/run.html#command-line-options
189192
"""
190-
assert FileInfo.tags_from_filename(filepath) == expected_tags
193+
project = ProjectMock(tmp_path)
194+
info = FileInfo.create(project, filepath)
195+
assert info.tags == expected_tags
196+
197+
198+
def test_unknown_file_should_raise_unknown_file_error(tmp_path):
199+
"""Test a file with unknown extension and name, which should cause unknown file error."""
200+
filepath = "conf/.shellcheckrc"
201+
ProjectMock(tmp_path).save_file(
202+
filepath,
203+
"""
204+
; empty ini file, nothing but a comment
205+
""",
206+
).style(
207+
f"""
208+
["{filepath}".conf]
209+
enable = "all"
210+
"""
211+
).api_check_then_fix(
212+
Fuss(
213+
fixed=False,
214+
filename="nitpick-style.toml",
215+
code=1,
216+
message=" has an incorrect style. Invalid config:",
217+
suggestion="conf/.shellcheckrc: Unknown file. See https://nitpick.rtfd.io/en/latest/plugins.html.",
218+
)
219+
)
220+
221+
222+
def test_explicit_files_tags_can_be_used_to_handle_unknown_file(tmp_path):
223+
"""Test explicit files tags can be used to handle unknown file whose extension and name are not recognized."""
224+
filepath = "conf/.shellcheckrc"
225+
ProjectMock(tmp_path).save_file(
226+
filepath,
227+
"""
228+
; empty ini file, nothing but a comment
229+
""",
230+
).style(
231+
f"""
232+
[nitpick.files.tags]
233+
"{filepath}" = ["ini"]
234+
235+
["{filepath}".conf]
236+
enable = "all"
237+
"""
238+
).api_check_then_fix(
239+
Fuss(
240+
fixed=True,
241+
filename=filepath,
242+
code=Violations.MISSING_SECTIONS.code,
243+
message=" has some missing sections. Use this:",
244+
suggestion="[conf]\nenable = all",
245+
)
246+
).assert_file_contents(
247+
filepath,
248+
"""
249+
; empty ini file, nothing but a comment
250+
251+
[conf]
252+
enable = all
253+
""",
254+
)

0 commit comments

Comments
 (0)