Skip to main content

ignoretree

CI Codecov PyPI - Version PyPI - Python Version License: MIT

Layered gitignore-compatible ignore pattern resolution for Python.

I built this because I needed to resolve ignore patterns across multiple layers (default patterns, .gitignore, .git/info/exclude, custom ignore files) in my projects, and couldn't find a library that did that. pathspec handles pattern matching well, but you're on your own for layered precedence, nested .gitignore scoping, and figuring out which pattern caused a file to be ignored.

Features

  • Four-layer precedence: defaults < .git/info/exclude < .gitignore (root to deepest) < custom ignore files. Last match wins.
  • Nested .gitignore files are scoped to their directory, matching git behavior.
  • explain() tells you exactly which pattern in which file caused the decision.
  • Backed by pathspec's GitIgnoreSpec for correct gitignore semantics.
  • Fully typed (PEP 561).

Installation

pip install ignoretree

Or with uv:

uv add ignoretree

Requires Python 3.11+.

Quick Start

from pathlib import Path
from ignoretree import IgnoreResolver

resolver = IgnoreResolver(
    root=Path("/path/to/repo"),
    default_patterns=["*.pyc", "__pycache__/", ".git/"],  # optional
    custom_ignore_filenames=[".myignore"],  # optional
)

# Check a single file (`auto_enter=True` loads .gitignore files along the path automatically):
resolver.is_ignored("src/debug.log", auto_enter=True)  # True or False

Bulk load

If you're going to check many files, load all .gitignore files upfront:

resolver.load_all()  # walks the repo, discovers all .gitignore files

resolver.is_ignored("src/debug.log")
resolver.is_ignored("tests/conftest.py")

load_all() captures a repository snapshot. Repeated calls on the same resolver return without walking again. Create a new resolver to observe ignore files or directories added after the snapshot.

Directories named .git are always skipped during bulk discovery, even when caller defaults do not ignore them.

Walker integration

For full control during directory traversal, call enter_directory() as you go. This lets you prune ignored directories so os.walk doesn't descend into them:

import os

root = Path("/path/to/repo")
resolver = IgnoreResolver(root, default_patterns=["*.pyc", "__pycache__/", ".git/"])

for dirpath, dirnames, filenames in os.walk(root):
    rel_dir = os.path.relpath(dirpath, root).replace(os.sep, "/")
    if rel_dir == ".":
        rel_dir = ""

    resolver.enter_directory(rel_dir)

    # Prune ignored directories so os.walk doesn't descend into them.
    dirnames[:] = [
        d for d in dirnames if not resolver.is_dir_ignored(f"{rel_dir}/{d}" if rel_dir else d)
    ]

    for fname in filenames:
        rel_path = f"{rel_dir}/{fname}" if rel_dir else fname
        if not resolver.is_ignored(rel_path):
            print(rel_path)

On Python 3.12+, you can use Path.walk() instead of os.walk().

Debugging with explain()

When you need to know why a file is ignored (or not):

decision = resolver.explain("src/debug.log")
print(decision)
# IgnoreDecision(ignored=True, source=PatternSource(file='.gitignore', line=3, pattern='*.log'))

decision = resolver.explain("src/main.py")
print(decision)
# IgnoreDecision(ignored=False, source=None)

Both explain() and explain_dir() return an IgnoreDecision with the winning pattern source. They also support auto_enter=True for on-demand loading.

Works well with standard logging:

import logging

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

for path in paths_to_check:
    decision = resolver.explain(path)
    logger.debug(f"Ignore decision for {path}: {decision}")

See the examples/ directory for runnable scripts.

Usage Modes

Mode Method When to use
On-demand is_ignored(..., auto_enter=True) Checking one or a few files. Loads .gitignore files along the path on demand.
Bulk load_all() + is_ignored() Checking many files. Discovers all .gitignore files upfront.
Walker enter_directory() + is_ignored() During os.walk() traversal. Maximum control over pruning.

All three modes support defaults, .git/info/exclude, nested .gitignore scoping, and custom ignore files. The difference is how and when .gitignore files are loaded.

Layer Precedence

Patterns are evaluated across four layers, from lowest to highest priority:

Priority Layer Source
1 (lowest) Defaults default_patterns argument
2 Exclude .git/info/exclude
3 Gitignore .gitignore files (root to deepest directory)
4 (highest) Custom Files listed in custom_ignore_filenames

Within each layer, negation patterns (!) work per gitignore rules. Across layers, the last layer with a matching pattern wins.

For normal repositories and linked Git worktrees, ignoretree locates the effective common info/exclude file directly from Git metadata. It does not invoke Git at runtime. The public source label remains .git/info/exclude in both layouts.

The repository root must be an existing directory. Ignoretree resolves it to an absolute path when the resolver is created. The directory does not need to be a Git repository.

Paths are matched as written and do not need to exist. Pass them as nonempty str values using root-relative POSIX syntax. These forms are not accepted:

  • Absolute paths and Windows drive or UNC paths
  • Backslashes and NUL characters
  • Repeated separators
  • . and .. path components

Use enter_directory("") for the repository root. This is the only method that accepts an empty path. Directory methods accept one trailing slash. File methods do not.

Ignoretree does not follow symlinks when looking for ignore files. Rules from safe parent directories can still match a symlink path. Ignore files reached through a symlink are not read.

Custom ignore filenames must be unique root-level names. They cannot be .git or .gitignore.

Ignore pattern parsing

Ignore files are read as UTF-8. A UTF-8 BOM is accepted at the start of a file, and both LF and CRLF line endings are supported.

Leading spaces and tabs are part of a pattern. Unescaped trailing spaces follow Git rules and are ignored during matching. Escape a trailing space with a backslash when it is part of a filename.

Malformed patterns and Git no-op patterns are skipped. They do not prevent valid rules later in the same file or source from applying. This behavior is consistent for defaults, .git/info/exclude, nested .gitignore files, and custom ignore files.

Case sensitivity

Matching is case-sensitive by default. Pass case_sensitive=False when creating a resolver to match the ignore behavior of a repository configured with core.ignoreCase=true.

resolver = IgnoreResolver(root, case_sensitive=False)

Ignoretree does not read Git configuration automatically. Case-insensitive mode folds only the ASCII letters A through Z. Pattern provenance keeps its original spelling.

ignoretree matches Unicode path strings lexically and does not emulate core.precomposeUnicode.

Development

Clone and install dependencies:

git clone https://github.com/SergiPantoja/ignoretree.git
cd ignoretree
uv sync

Running checks

uv run pytest                          # tests with coverage
uv run ruff check src/ tests/         # lint
uv run ruff format --check src/ tests/ # format check
uv run mypy src/                       # type check

Pre-commit hooks (optional)

If you tend to forget (like me) to run linting before committing:

uv run pre-commit install

This sets up hooks that run ruff (lint + format) and check uv.lock consistency on every commit.

Code style

License

MIT

Metadata

Release files for ignoretree 0.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ignoretree 0.3.0
File Size Uploaded
ignoretree-0.3.0.tar.gz 31.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ignoretree 0.3.0
File Interpreter ABI Platform
ignoretree-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 46.5 kB

Release files / ignoretree-0.3.0.tar.gz

Download URL ignoretree-0.3.0.tar.gz
Size 31.3 kB
Tags Source
SHA-256 checksum
How to use checksums
c7ceba9d87cddc06e996d4b97307ed471b0cf1463d35bdac15cd85707efb0a02
BLAKE2b-256 checksum
How to use checksums
e6ab0bb8b4d46cfbd2ecce30797d788ba8986e45a2dc3c60ab59d01d178a42ea
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.

Transparency log

Release files / ignoretree-0.3.0-py3-none-any.whl

Download URL ignoretree-0.3.0-py3-none-any.whl
Size 15.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
450ea94337dd34f1a4006a64b9141db99eee0513bbc20f24c559d1ab1f541b7e
BLAKE2b-256 checksum
How to use checksums
0f58f451a4f9842e24a741a2d4ac8b986bc730b13ec8b76597d18058aaf648bb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 17, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.3.0 This release

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page