Layered gitignore-compatible ignore pattern resolution for Python
Project description
ignoretree
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
.gitignorefiles are scoped to their directory, matching git behavior. explain()tells you exactly which pattern in which file caused the decision.- Backed by pathspec's
GitIgnoreSpecfor 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")
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 ofos.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.
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
- Ruff for linting and formatting.
- Google-style docstrings.
- mypy in strict mode.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ignoretree-0.2.0.tar.gz.
File metadata
- Download URL: ignoretree-0.2.0.tar.gz
- Upload date:
- Size: 55.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
85075b6a5ffbe0878570c18864559dfd879d859e2aee314a120c9423a979d59a
|
|
| MD5 |
ec5ededeba63213fb80ad8751513d9e8
|
|
| BLAKE2b-256 |
b156fd30bde4e5c615d46265efb84a03cbb57eb26ee1cc87f620240c5bbbf05b
|
Provenance
The following attestation bundles were made for ignoretree-0.2.0.tar.gz:
Publisher:
release.yml on SergiPantoja/ignoretree
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ignoretree-0.2.0.tar.gz -
Subject digest:
85075b6a5ffbe0878570c18864559dfd879d859e2aee314a120c9423a979d59a - Sigstore transparency entry: 1259048167
- Sigstore integration time:
-
Permalink:
SergiPantoja/ignoretree@01b57ac481a0fcaf9704323b56023c00d2d41d7a -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/SergiPantoja
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@01b57ac481a0fcaf9704323b56023c00d2d41d7a -
Trigger Event:
push
-
Statement type:
File details
Details for the file ignoretree-0.2.0-py3-none-any.whl.
File metadata
- Download URL: ignoretree-0.2.0-py3-none-any.whl
- Upload date:
- Size: 9.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e09e68dd4145ecbd150d3f71dfffeb8c3ba7c8263b983f7c8eba9ab32e141783
|
|
| MD5 |
cad1c091771b4dd85a930f084615ec7d
|
|
| BLAKE2b-256 |
c73990f5d264dbe8deeea12a3ed834e4a63a07c0a3f288587288956b3a182bd8
|
Provenance
The following attestation bundles were made for ignoretree-0.2.0-py3-none-any.whl:
Publisher:
release.yml on SergiPantoja/ignoretree
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ignoretree-0.2.0-py3-none-any.whl -
Subject digest:
e09e68dd4145ecbd150d3f71dfffeb8c3ba7c8263b983f7c8eba9ab32e141783 - Sigstore transparency entry: 1259048225
- Sigstore integration time:
-
Permalink:
SergiPantoja/ignoretree@01b57ac481a0fcaf9704323b56023c00d2d41d7a -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/SergiPantoja
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@01b57ac481a0fcaf9704323b56023c00d2d41d7a -
Trigger Event:
push
-
Statement type: