Development Guide¶
This guide provides information for developers who want to contribute to or extend Recursivist.
Setting Up Development Environment¶
Prerequisites¶
- Python 3.9 or higher
- Git
- uv (Python package and environment manager)
Clone the Repository¶
Create a Virtual Environment¶
Install Development Dependencies¶
This installs Recursivist in editable mode, so your changes to the source code will be reflected immediately without reinstalling.
Install Pre-commit Hooks¶
The hooks run Ruff (lint + format) and mypy/pyright type checks automatically on every commit.
Project Structure¶
Recursivist is organized into several key modules:
recursivist/
├── __init__.py # Package initialization, version info
├── cli.py # Command-line interface (Typer-based)
├── core.py # Core functionality (directory traversal, tree building)
├── exports.py # Export functionality (TXT, JSON, HTML, MD, JSX)
├── compare.py # Comparison functionality (side-by-side diff)
└── jsx_export.py # React component generation
Module Responsibilities¶
- cli.py: Defines the command-line interface using Typer, handles command-line arguments and option parsing, and invokes core functionality
- core.py: Implements the core directory traversal, pattern matching, and tree building functionality
- exports.py: Contains the
DirectoryExporterclass for exporting directory structures to various formats - compare.py: Implements functionality for comparing two directory structures side by side
- jsx_export.py: Provides specialized functionality for generating React components
Development Workflow¶
Making Changes¶
- Create a new branch for your feature or bug fix:
-
Make your changes to the codebase.
-
Run the tests to ensure your changes don't break existing functionality:
- Add and commit your changes:
Pre-commit hooks will run automatically on commit. If they flag issues, fix them and commit again.
- Push your changes:
- Create a pull request.
Code Style¶
We use Ruff for both linting and formatting. You can run it directly or via Nox:
For type checking, we run both mypy (strict mode) and pyright:
Ruff and type checks are also run automatically by the pre-commit hooks on every commit.
Adding a New Feature¶
Adding a New Command¶
To add a new command to the CLI:
- Open
cli.py - Add your new command using the Typer decorator pattern:
@app.command()
def your_command(
directory: Path = typer.Argument(
".", help="Directory path to process"
),
# Add more parameters as needed
):
"""
Your command description.
Detailed information about what the command does and how to use it.
"""
# Implement your command logic here
pass
- Implement the core functionality in the appropriate module.
- Add tests for your new command.
Adding a New Export Format¶
To add a new export format:
- Open
exports.py - Add a new method to the
DirectoryExporterclass:
def to_your_format(self, output_path: str) -> None:
"""Export directory structure to your format.
Args:
output_path: Path where the export file will be saved
"""
try:
with open(output_path, "w", encoding="utf-8") as f:
# Write your formatted output
pass
except Exception as e:
logger.error(f"Error exporting to YOUR_FORMAT: {e}")
raise
- Update the format map in the
export_structurefunction incore.py:
format_map = {
"txt": exporter.to_txt,
"json": exporter.to_json,
"html": exporter.to_html,
"md": exporter.to_markdown,
"jsx": exporter.to_jsx,
"svg": exporter.to_svg,
"your_format": exporter.to_your_format, # Add your format here
}
- Add tests for your new export format.
Adding New File Statistics¶
To add a new statistic (beyond LOC, size, and mtime):
- Update the
get_directory_structurefunction incore.pyto collect your new statistic. - Add appropriate parameters to the function signature for enabling/sorting by the new statistic.
- Update the
build_treefunction to display the new statistic. - Update export formats to include the new statistic.
- Add CLI options in
cli.pyto enable the new statistic.
Testing¶
For detailed information about testing, see the Testing Guide.
Basic Testing¶
We use pytest via Nox. Coverage reporting is enabled by default (configured in pyproject.toml).
# Run tests across all supported Python versions (3.9–3.13)
nox -s tests
# Pass extra arguments to pytest (e.g. run a specific test)
nox -s tests -- -k "pattern"
# Run pytest directly in your active environment
pytest
# Run a specific test file
pytest tests/test_core.py
Debugging¶
Verbose Output¶
Use the --verbose flag during development to enable detailed logging:
This provides more information about what's happening during execution, which can be helpful for debugging.
Using a Debugger¶
Use the built-in breakpoint() to drop into the debugger at any point:
With modern IDEs like VSCode or PyCharm, you can also set breakpoints and use their built-in debuggers without modifying code.
Documentation¶
Docstrings¶
Use Google-style docstrings for all functions, classes, and methods:
def function_name(param1: str, param2: int) -> bool:
"""Short description of the function.
More detailed description if needed.
Args:
param1: Description of param1
param2: Description of param2
Returns:
Description of return value
Raises:
ValueError: When and why this exception is raised
"""
# Function implementation
Command-Line Help¶
Update the command-line help text when you add or modify commands or options:
@app.command()
def your_command(
param: str = typer.Option(
None, "--param", "-p", help="Clear description of the parameter"
)
):
"""
Clear, concise description of what the command does.
More detailed explanation with examples:
Examples:
recursivist your_command --param value
"""
# Implementation
Performance Considerations¶
Large Directory Structures¶
When working with large directories:
- Use generators and iterators where possible to minimize memory usage.
- Implement early filtering to reduce the number of files and directories processed.
- Use progress indicators (like the
Progressclass from Rich) for long-running operations. - Test with large directories to ensure acceptable performance.
Profiling¶
Use the cProfile module to profile performance:
import cProfile
cProfile.run('your_function_call()', 'profile_results')
# To analyze the results
import pstats
p = pstats.Stats('profile_results')
p.sort_stats('cumulative').print_stats(20)
Extending Pattern Matching¶
Recursivist currently supports glob patterns (default) and regular expressions. To add a new pattern type:
- Update the
should_excludefunction incore.pyto handle the new pattern type. - Add a new flag to the command-line arguments in
cli.py. - Add appropriate documentation for the new pattern type.
- Add tests specifically for the new pattern functionality.
Release Process¶
Version Numbering¶
Recursivist follows Semantic Versioning (SemVer):
- MAJOR version for incompatible API changes
- MINOR version for backwards-compatible feature additions
- PATCH version for backwards-compatible bug fixes
Creating a Release¶
- Update the version in
pyproject.toml:
- Commit and push to
main:
-
The
tag-releaseGitHub Actions workflow will automatically detect the version change, verify the tag doesn't already exist, and create and push the Git tag. No manual tagging is required. -
Build and upload to PyPI (maintainers only):
Common Development Tasks¶
Adding a New Command-Line Option¶
- Add the option to the appropriate command functions in
cli.py:
@app.command()
def visualize(
# Existing options...
new_option: bool = typer.Option(
False, "--new-option", "-n", help="Description of the new option"
),
):
# Pass the new option to the core function
display_tree(
# Existing parameters...
new_option=new_option
)
- Update the core function to handle the new option:
def display_tree(
# Existing parameters...
new_option: bool = False,
):
if new_option:
# Do something
pass
Improving Colorization¶
The file extension colorization is handled by the generate_color_for_extension function in core.py:
def generate_color_for_extension(extension: str) -> str:
"""Generate a consistent color for a file extension."""
# Current implementation uses hash-based approach
# You can modify this to use predefined colors for common extensions
If you want to add predefined colors for common file types:
- Create a mapping of extensions to colors:
EXTENSION_COLORS = {
".py": "#3776AB", # Python blue
".js": "#F7DF1E", # JavaScript yellow
".html": "#E34C26", # HTML orange
".css": "#264DE4", # CSS blue
# Add more extensions and colors
}
- Update the
generate_color_for_extensionfunction to use this mapping:
def generate_color_for_extension(extension: str) -> str:
"""Generate a consistent color for a file extension."""
extension = extension.lower()
if extension in EXTENSION_COLORS:
return EXTENSION_COLORS[extension]
# Fall back to the hash-based approach for unknown extensions
# ...
This will give common file types consistent, recognizable colors while maintaining the existing behavior for other file types.