Scanning and Filtering¶
Scanning walks a directory into a tree of Directory nodes; filtering decides which entries are left out along the way.
Directory¶
recursivist._models.Directory
dataclass
¶
A single directory within a scanned directory structure.
A scan yields a tree of these nodes: the root directory, with every subdirectory
nested under its name in subdirectories. Subdirectory names are the only keys of
that mapping, so a directory may be called anything the filesystem allows.
Attributes:
| Name | Type | Description |
|---|---|---|
files |
list[FileEntry]
|
The directory's own files. |
subdirectories |
dict[str, Directory]
|
The directory's subdirectories, keyed by name. |
loc |
int | None
|
Total lines of code in the directory and everything below it, or |
size |
int | None
|
Total size in bytes of the directory and everything below it, or |
mtime |
float | None
|
Latest modification time (seconds since epoch) in the directory and
everything below it, or |
max_depth_reached |
bool
|
Whether traversal stopped at the depth limit, leaving the directory's contents unread. |
hidden_contents |
bool
|
Whether a directory cut short by the depth limit is not empty, so renderers can tell it apart from one that holds nothing. |
symlink_loop |
bool
|
Whether the directory was not descended into because it resolves to one of its own ancestors, i.e. a symlink (or other) cycle back up the tree. |
git_markers |
dict[str, str]
|
|
Source code in recursivist/_models.py
FileEntry¶
recursivist._models.FileEntry
¶
Bases: NamedTuple
A single file within a scanned directory structure.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Bare filename (e.g. |
path |
str
|
The string to display for this file — an absolute, forward-slash path when
full-path display is enabled, otherwise just |
loc |
int
|
Lines of code. Populated only when LOC counting is enabled during scanning;
|
size |
int
|
File size in bytes. Populated only when size tracking is enabled; |
mtime |
float
|
Modification time (seconds since epoch). Populated only when mtime
tracking is enabled; |
Source code in recursivist/_models.py
Scanner¶
recursivist.scanner
¶
Directory traversal.
Recursively walks a directory, applies the exclusion rules from
recursivist.filtering, collects optional per-file metrics
from recursivist.metrics, and returns the tree of
Directory nodes consumed by the renderers and
exporters.
has_contents
¶
has_contents(directory: Directory) -> bool
Return whether a scanned directory holds anything to display.
A directory counts as non-empty when it has files, has subdirectories, links back to one of its ancestors, or was cut short by the depth limit with contents left unexplored. Renderers use this to pick between the open and closed folder icons.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
directory
|
Directory
|
A directory from a structure produced by
|
required |
Returns:
| Type | Description |
|---|---|
bool
|
|
Source code in recursivist/scanner.py
get_directory_structure
¶
get_directory_structure(root_dir: str, exclude_dirs: Sequence[str] | None = None, ignore_file: str | None = None, exclude_extensions: set[str] | None = None, parent_ignore_patterns: Sequence[tuple[str, tuple[str, ...]]] | None = None, exclude_patterns: Sequence[str | Pattern[str]] | None = None, include_patterns: Sequence[str | Pattern[str]] | None = None, max_depth: int = 0, current_depth: int = 0, current_path: str = '', show_full_path: bool = False, sort_by_loc: bool = False, sort_by_size: bool = False, sort_by_mtime: bool = False, show_git_status: bool = False, git_status_map: dict[str, str] | None = None, ancestor_ids: frozenset[tuple[int, int]] | None = None, pattern_tracker: PatternMatchTracker | None = None) -> tuple[Directory, set[str]]
Build the tree of directory nodes representing a directory structure.
Recursively traverses root_dir, applying the exclusion rules and optionally
collecting per-file metrics, and returns the root
Directory consumed by the renderers and
exporters. Each subdirectory is a nested
Directory stored under its name in the parent's
subdirectories; iterate them in display order with
iter_subdirectories.
Fields set on each directory of the returned structure:
files: aFileEntryfor each of the directory's files.subdirectories: the nested directories, keyed by name.loc: total lines of code (when sort_by_loc is set).size: total size in bytes (when sort_by_size is set).mtime: latest modification time (when sort_by_mtime is set).max_depth_reached:Truewhen traversal stopped at max_depth.hidden_contents:Truealongsidemax_depth_reachedwhen the untraversed directory is not empty, so renderers can still tell it apart from one that holds nothing.symlink_loop:Truewhen a directory was not recursed into because it resolves to one of its own ancestors, i.e. a symlink (or other) cycle back up the tree.git_markers:{filename: status_char}(when show_git_status is set).
A metric that was not requested is left as None, as are all three on a directory
that was not traversed (one cut short by the depth limit, or a symlink loop).
When show_git_status is set, files Git reports as deleted are listed even though they are no longer on disk, together with any directory that disappeared along with them. They are subject to the same exclusion rules as every other entry, and a deleted directory is listed only if at least one of its deleted files survives them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root_dir
|
str
|
Directory to scan. |
required |
exclude_dirs
|
Sequence[str] | None
|
Directory names to skip entirely. |
None
|
ignore_file
|
str | None
|
Name of an ignore file to honor within each directory (e.g.
|
None
|
exclude_extensions
|
set[str] | None
|
Lowercase, dot-prefixed extensions to exclude. |
None
|
parent_ignore_patterns
|
Sequence[tuple[str, tuple[str, ...]]] | None
|
Ignore files inherited from parent directories as a
shallowest-first stack of |
None
|
exclude_patterns
|
Sequence[str | Pattern[str]] | None
|
Glob or compiled-regex patterns to exclude. |
None
|
include_patterns
|
Sequence[str | Pattern[str]] | None
|
Glob or compiled-regex patterns to include. When given, only files whose names match one are kept, and a match overrides ignore-file rules for that file. They do not override exclude_dirs, exclude_extensions, or exclude_patterns. |
None
|
max_depth
|
int
|
Maximum depth to traverse, or |
0
|
current_depth
|
int
|
Current recursion depth. Set internally. |
0
|
current_path
|
str
|
Path of the current directory relative to the scan root. Set internally. |
''
|
show_full_path
|
bool
|
Whether to store absolute paths instead of bare filenames. |
False
|
sort_by_loc
|
bool
|
Whether to count and total lines of code. |
False
|
sort_by_size
|
bool
|
Whether to measure and total file sizes. |
False
|
sort_by_mtime
|
bool
|
Whether to record file modification times. |
False
|
show_git_status
|
bool
|
Whether to annotate files with Git status markers. |
False
|
git_status_map
|
dict[str, str] | None
|
Pre-computed |
None
|
ancestor_ids
|
frozenset[tuple[int, int]] | None
|
|
None
|
pattern_tracker
|
PatternMatchTracker | None
|
Records which of the exclude/include filters matched a scanned entry. When omitted on the top-level call, one is created and each filter that matched nothing is logged as a warning once the scan finishes. Pass one explicitly to aggregate several scans (as a comparison does) and report it yourself. |
None
|
Returns:
| Type | Description |
|---|---|
Directory
|
A |
set[str]
|
node and extensions is the set of lowercase file extensions encountered. |
Source code in recursivist/scanner.py
212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 | |
iter_subdirectories
¶
Yield (name, subdirectory) for each subdirectory of directory.
Entries are yielded in case-sensitive name order, which is the order every renderer and exporter lists subdirectories in.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
directory
|
Directory
|
A directory from a structure produced by
|
required |
Yields:
| Type | Description |
|---|---|
tuple[str, Directory]
|
|
Source code in recursivist/scanner.py
collect_extensions
¶
collect_extensions(directory: Directory) -> set[str]
Return the lowercase file extensions of every file in directory.
Walks the whole structure and gathers the same set that
get_directory_structure returns
alongside it, for callers that hold only the structure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
directory
|
Directory
|
The root of a structure produced by
|
required |
Returns:
| Type | Description |
|---|---|
set[str]
|
The set of extensions, each lowercase with its leading dot (e.g. |
set[str]
|
Files without an extension contribute nothing. |
Source code in recursivist/scanner.py
Filtering¶
recursivist.filtering
¶
File and directory filtering: ignore files, glob/regex patterns, and gitignore-style exclusion rules.
Provides the predicate should_exclude used by
the scanner. Git-style ignore matching is delegated to pathspec (its gitignore
matcher), which implements the full gitignore specification: anchoring, **
wildcards, directory-only (trailing /) patterns, ! negation with
last-match-wins, character classes, backslash escapes, and trailing-whitespace handling.
The glob and regex matching used by --exclude-pattern/--include-pattern is
unrelated and uses only the standard library.
Like Git, each ignore file is evaluated relative to the directory that contains it
rather than relative to the scan root. The active ignore files are kept as a stack
(shallowest first); a path is tested against every level with its own anchoring, and a
deeper file's verdict overrides a shallower one, so an anchored pattern such as
/build in a nested .gitignore matches only within that subdirectory and does not
leak up to the scan root or down past the anchor.
InvalidPatternError
¶
PatternMatchTracker
¶
Record which user-supplied filters matched at least one scanned entry.
The scanner reports every directory entry it lists to
observe before applying the
exclusion rules, so a filter counts as matched even when an earlier rule already
removed the entry. Once a filter has matched it is dropped from the pending set, and
once nothing is pending observation is a no-op, so the bookkeeping costs nothing on
a scan where every filter is used.
Matching mirrors the scanner exactly: --exclude names are compared with the
entry name, --exclude-ext applies to files only, --exclude-pattern applies
to files and directories, and --include-pattern applies to files only.
Entries the scanner never lists — the contents of excluded or ignored directories,
and anything below --depth — are not observed, so a filter reported as
unmatched matched nothing among the scanned entries.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
exclude_dirs
|
Sequence[str] | None
|
Names given to |
None
|
exclude_extensions
|
Iterable[str] | None
|
Normalized extensions given to |
None
|
exclude_patterns
|
Sequence[str | Pattern[str]] | None
|
Patterns given to |
None
|
include_patterns
|
Sequence[str | Pattern[str]] | None
|
Patterns given to |
None
|
Source code in recursivist/filtering.py
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 | |
observe
¶
Mark every pending filter that matches the entry at path as used.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Filesystem path of a directory entry the scanner listed. |
required |
is_dir
|
bool
|
Whether path is a directory. |
required |
Source code in recursivist/filtering.py
unmatched
¶
Return (flag, value) pairs for every filter that matched nothing.
Source code in recursivist/filtering.py
report
¶
Log a warning for each filter that matched nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
where
|
str
|
Optional phrase naming what was scanned (e.g. |
''
|
Source code in recursivist/filtering.py
parse_ignore_file
¶
Read an ignore file and return its lines as gitignore patterns.
Lines are returned verbatim with only their terminators removed, preserving order and every character that is significant to the gitignore grammar (comments, blank lines, backslash escapes, and escaped trailing whitespace). Interpretation is left entirely to the gitignore matcher, so callers must not strip or filter the returned lines.
A leading UTF-8 byte order mark is dropped, as Git does, so that it does not become part of the first pattern and silently stop that pattern from matching.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ignore_file_path
|
str
|
Path to the ignore file (e.g. |
required |
Returns:
| Type | Description |
|---|---|
list[str]
|
The list of pattern lines, or an empty list when the path does not exist, is |
list[str]
|
not a regular file (a directory, or a named pipe that would block when read), |
list[str]
|
or cannot be read. |
Source code in recursivist/filtering.py
normalize_extensions
¶
Return extensions in the lowercase, dot-prefixed form the scanner matches on.
An extension may be written with or without its leading dot and in any case, so
"pyc", ".pyc" and ".PYC" all become ".pyc". The result is the form
expected wherever an exclude_extensions argument is described as normalized.
Duplicates collapse, and normalizing an already-normalized set returns an equal
set.
Source code in recursivist/filtering.py
compile_regex_patterns
¶
Compile patterns to regex objects when regex matching is requested.
When is_regex is False the patterns are returned unchanged for glob matching.
When True each pattern is compiled to a re.Pattern, and a pattern that fails
to compile is an error.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
patterns
|
Sequence[str]
|
Patterns to process. |
required |
is_regex
|
bool
|
Whether to treat the patterns as regular expressions ( |
False
|
Returns:
| Type | Description |
|---|---|
list[str | Pattern[str]]
|
A list whose items are plain strings for glob patterns or compiled |
list[str | Pattern[str]]
|
objects for regexes. |
Raises:
| Type | Description |
|---|---|
InvalidPatternError
|
If is_regex is |
Source code in recursivist/filtering.py
should_exclude
¶
should_exclude(path: str, ignore_context: dict[str, Any], exclude_extensions: set[str] | None = None, exclude_patterns: Sequence[str | Pattern[str]] | None = None, include_patterns: Sequence[str | Pattern[str]] | None = None, *, is_dir: bool | None = None) -> bool
Decide whether a path should be excluded from the scan.
The filtering rules are applied in priority order:
- If include_patterns are given and none match a file, exclude it.
- If any exclude_patterns match, exclude the path (this overrides include patterns).
- If a non-directory's extension is in exclude_extensions, exclude it.
- If an include pattern matched a file, include it (this overrides the gitignore-style patterns below). Directories are never tested against include patterns, so they always fall through to the ignore rules.
- Otherwise apply the gitignore-style rules from ignore_context via
pathspec, honoring anchoring,**wildcards, directory-only (trailing/) patterns,!negation, character classes and escapes per the gitignore specification. Each ignore file in the stack is matched relative to its own directory and deeper files override shallower ones, so a nested file's anchored patterns stay scoped to its subtree.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str
|
Filesystem path to test. |
required |
ignore_context
|
dict[str, Any]
|
Mapping describing the active ignore rules. Recognized keys are
|
required |
exclude_extensions
|
set[str] | None
|
Lowercase, dot-prefixed extensions to exclude. |
None
|
exclude_patterns
|
Sequence[str | Pattern[str]] | None
|
Glob or compiled-regex patterns to exclude, matched against the entry's name. |
None
|
include_patterns
|
Sequence[str | Pattern[str]] | None
|
Glob or compiled-regex patterns to include, matched against the entry's name, which override the gitignore-style exclusions. |
None
|
is_dir
|
bool | None
|
Whether path is a directory, when the caller already knows. Left as
|
None
|
Returns:
| Type | Description |
|---|---|
bool
|
|