From 54cdbefa55f9aecbc4f2da235838dd99211b3d41 Mon Sep 17 00:00:00 2001 From: Keyoonz <89485044+Keyoonz@users.noreply.github.com> Date: Fri, 20 Feb 2026 16:46:58 +0100 Subject: [PATCH] feat: normignore file --- README.md | 6 ++++ norminette/__main__.py | 16 ++++++++++ norminette/tools/normignore.py | 28 ++++++++++++++++++ poetry.lock | 22 ++++++++++++-- pyproject.toml | 1 + tests/test_normignore.py | 54 ++++++++++++++++++++++++++++++++++ 6 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 norminette/tools/normignore.py create mode 100644 tests/test_normignore.py diff --git a/README.md b/README.md index be409e35..50f29333 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,12 @@ norminette -d norminette -dd ``` +### Ignore file + +You can create a `.normignore` file that works exactly like `.gitignore` but for norminette. + +You can disable this feature by using the `--no-normignore` flag. + ## Docker usage ``` diff --git a/norminette/__main__.py b/norminette/__main__.py index 3272f36f..8e1311cf 100644 --- a/norminette/__main__.py +++ b/norminette/__main__.py @@ -4,6 +4,7 @@ import platform import subprocess import sys +import os from importlib.metadata import version from norminette.context import Context @@ -13,6 +14,7 @@ from norminette.lexer import Lexer from norminette.registry import Registry from norminette.tools.colors import colors +from norminette.tools.normignore import NormIgnoreSpec version_text = f"norminette {version('norminette')}" version_text += f", Python {platform.python_version()}" @@ -66,6 +68,11 @@ def main(): action="store_true", help="Parse only source files not match to .gitignore", ) + parser.add_argument( + "--no-normignore", + action="store_true", + help="Ignore .normignore file", + ) parser.add_argument( "-f", "--format", @@ -130,6 +137,15 @@ def main(): ) sys.exit(0) files = tmp_targets + + if not args.no_normignore: + spec = NormIgnoreSpec(os.getcwd()) + tmp_targets = [] + for target in files: + if not spec.is_ignored(target.path): + tmp_targets.append(target) + files = tmp_targets + for file in files: try: lexer = Lexer(file) diff --git a/norminette/tools/normignore.py b/norminette/tools/normignore.py new file mode 100644 index 00000000..a1e16ad1 --- /dev/null +++ b/norminette/tools/normignore.py @@ -0,0 +1,28 @@ +import os +import pathspec + + +class NormIgnoreSpec: + def __init__(self, cwd): + self.spec = self._load(cwd) + self.filepath = os.path.join(os.getcwd(), ".normignore") + + def _load(self, cwd): + try: + with open(os.path.join(cwd, ".normignore")) as f: + lines = f.read().splitlines() + except FileNotFoundError: + return None + except PermissionError: + print("Warning: unable to access normignore file") + return None + + return pathspec.PathSpec.from_lines( + "gitignore", + lines + ) + + def is_ignored(self, target): + if not self.spec: + return False + return self.spec.match_file(target) diff --git a/poetry.lock b/poetry.lock index 7c0837e7..3f8bac35 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. [[package]] name = "argparse" @@ -146,6 +146,24 @@ files = [ {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, ] +[[package]] +name = "pathspec" +version = "1.0.4" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723"}, + {file = "pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645"}, +] + +[package.extras] +hyperscan = ["hyperscan (>=0.7)"] +optional = ["typing-extensions (>=4)"] +re2 = ["google-re2 (>=1.1)"] +tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] + [[package]] name = "platformdirs" version = "4.3.6" @@ -311,4 +329,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.1" python-versions = ">=3.10" -content-hash = "edcc9fd82415a834fad3a3e55bcf86d0a8bf1f5d9d01773b1e2da8a11cd3955f" +content-hash = "5c9bf3b5c91f68d5b3aa0711f67da9ccf2a7725698022f4ed84af096da92d853" diff --git a/pyproject.toml b/pyproject.toml index f4075288..449c0cb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,6 +38,7 @@ commands = [tool.poetry.dependencies] python = ">=3.10" argparse = "^1.4.0" +pathspec = ">=1.0.4" [tool.poetry.group.dev.dependencies] pytest = "^7.3.2" diff --git a/tests/test_normignore.py b/tests/test_normignore.py new file mode 100644 index 00000000..8af83b51 --- /dev/null +++ b/tests/test_normignore.py @@ -0,0 +1,54 @@ +from norminette.tools.normignore import NormIgnoreSpec +import pytest + + +def test_single_file(tmp_path): + normignore_file = tmp_path / ".normignore" + normignore_file.write_text("ignored.c\n") + + file1 = tmp_path / "ignored.c" + file2 = tmp_path / "not_ignored.c" + file1.write_text("") + file2.write_text("") + + ignore = NormIgnoreSpec(tmp_path) + + files = [file1, file2] + filtered = [f for f in files if not ignore.is_ignored(f)] + assert file2 in filtered + assert file1 not in filtered + + +def test_directory(tmp_path): + normignore_file = tmp_path / ".normignore" + normignore_file.write_text("build/\n") + + (tmp_path / "build").mkdir() + file1 = tmp_path / "build" / "file.c" + file1.write_text("") + file2 = tmp_path / "main.c" + file2.write_text("") + + ignore = NormIgnoreSpec(tmp_path) + files = [file1, file2] + filtered = [f for f in files if not ignore.is_ignored(f)] + + assert file2 in filtered + assert file1 not in filtered + + +@pytest.mark.parametrize("pattern, filename, ignored", [ + ("*.c", "main.c", True), + ("*.c", "readme.md", False), + ("build/", "build/file.c", True), + ("build/", "src/file.c", False), +]) +def test_multiple_patterns(tmp_path, pattern, filename, ignored): + (tmp_path / ".normignore").write_text(pattern + "\n") + file = tmp_path / filename + file.parent.mkdir(parents=True, exist_ok=True) + file.write_text("") + + ignore = NormIgnoreSpec(tmp_path) + result = ignore.is_ignored(file) + assert result == ignored