Model: claude-3-opus-20240229
files_to_prompt/__init__.py
---
---
files_to_prompt/cli.py
---
import os
import click
from pathlib import Path
from fnmatch import fnmatch
def should_ignore(path, gitignore_rules):
for rule in gitignore_rules:
if fnmatch(os.path.basename(path), rule):
return True
if os.path.isdir(path) and fnmatch(os.path.basename(path) + "/", rule):
return True
return False
def read_gitignore(path):
gitignore_path = os.path.join(path, ".gitignore")
if os.path.isfile(gitignore_path):
with open(gitignore_path, "r") as f:
return [
line.strip() for line in f if line.strip() and not line.startswith("#")
]
return []
def process_path(path, include_hidden, ignore_gitignore, gitignore_rules):
if os.path.isfile(path):
with open(path, "r") as f:
file_contents = f.read()
click.echo(path)
click.echo("---")
click.echo(file_contents)
click.echo()
click.echo("---")
elif os.path.isdir(path):
for root, dirs, files in os.walk(path):
if not include_hidden:
dirs[:] = [d for d in dirs if not d.startswith(".")]
files = [f for f in files if not f.startswith(".")]
if not ignore_gitignore:
gitignore_rules.extend(read_gitignore(root))
dirs[:] = [
d
for d in dirs
if not should_ignore(os.path.join(root, d), gitignore_rules)
]
files = [
f
for f in files
if not should_ignore(os.path.join(root, f), gitignore_rules)
]
for file in files:
file_path = os.path.join(root, file)
with open(file_path, "r") as f:
file_contents = f.read()
click.echo(file_path)
click.echo("---")
click.echo(file_contents)
click.echo()
click.echo("---")
@click.command()
@click.argument("paths", nargs=-1, type=click.Path(exists=True))
@click.option(
"--include-hidden",
is_flag=True,
help="Include files and folders starting with .",
)
@click.option(
"--ignore-gitignore",
is_flag=True,
help="Ignore .gitignore files and include all files",
)
@click.version_option()
def cli(paths, include_hidden, ignore_gitignore):
"""
Takes one or more paths to files or directories and outputs every file,
recursively, each one preceded with its filename like this:
path/to/file.py
----
Contents of file.py goes here
---
path/to/file2.py
---
...
"""
gitignore_rules = []
for path in paths:
if not os.path.exists(path):
raise click.BadArgumentUsage(f"Path does not exist: {path}")
if not ignore_gitignore:
gitignore_rules.extend(read_gitignore(os.path.dirname(path)))
process_path(path, include_hidden, ignore_gitignore, gitignore_rules)
---
files_to_prompt/__main__.py
---
from .cli import cli
if __name__ == "__main__":
cli()
---
tests/test_files_to_prompt.py
---
import os
from click.testing import CliRunner
from files_to_prompt.cli import cli
def test_basic_functionality(tmpdir):
runner = CliRunner()
with tmpdir.as_cwd():
os.makedirs("test_dir")
with open("test_dir/file1.txt", "w") as f:
f.write("Contents of file1")
with open("test_dir/file2.txt", "w") as f:
f.write("Contents of file2")
result = runner.invoke(cli, ["test_dir"])
assert result.exit_code == 0
assert "test_dir/file1.txt" in result.output
assert "Contents of file1" in result.output
assert "test_dir/file2.txt" in result.output
assert "Contents of file2" in result.output
def test_include_hidden(tmpdir):
runner = CliRunner()
with tmpdir.as_cwd():
os.makedirs("test_dir")
with open("test_dir/.hidden.txt", "w") as f:
f.write("Contents of hidden file")
result = runner.invoke(cli, ["test_dir"])
assert result.exit_code == 0
assert "test_dir/.hidden.txt" not in result.output
result = runner.invoke(cli, ["test_dir", "--include-hidden"])
assert result.exit_code == 0
assert "test_dir/.hidden.txt" in result.output
assert "Contents of hidden file" in result.output
def test_ignore_gitignore(tmpdir):
runner = CliRunner()
with tmpdir.as_cwd():
os.makedirs("test_dir")
with open("test_dir/.gitignore", "w") as f:
f.write("ignored.txt")
with open("test_dir/ignored.txt", "w") as f:
f.write("This file should be ignored")
with open("test_dir/included.txt", "w") as f:
f.write("This file should be included")
result = runner.invoke(cli, ["test_dir"])
assert result.exit_code == 0
assert "test_dir/ignored.txt" not in result.output
assert "test_dir/included.txt" in result.output
result = runner.invoke(cli, ["test_dir", "--ignore-gitignore"])
assert result.exit_code == 0
assert "test_dir/ignored.txt" in result.output
assert "This file should be ignored" in result.output
assert "test_dir/included.txt" in result.output
def test_multiple_paths(tmpdir):
runner = CliRunner()
with tmpdir.as_cwd():
os.makedirs("test_dir1")
with open("test_dir1/file1.txt", "w") as f:
f.write("Contents of file1")
os.makedirs("test_dir2")
with open("test_dir2/file2.txt", "w") as f:
f.write("Contents of file2")
with open("single_file.txt", "w") as f:
f.write("Contents of single file")
result = runner.invoke(cli, ["test_dir1", "test_dir2", "single_file.txt"])
assert result.exit_code == 0
assert "test_dir1/file1.txt" in result.output
assert "Contents of file1" in result.output
assert "test_dir2/file2.txt" in result.output
assert "Contents of file2" in result.output
assert "single_file.txt" in result.output
assert "Contents of single file" in result.output
add one last test which tests .gitignore and include_hidden against an example that mixes single files and directories of files together in one invocation
Here's an additional test that mixes single files and directories while testing the .gitignore and include_hidden options:
def test_mixed_paths_with_options(tmpdir):
runner = CliRunner()
with tmpdir.as_cwd():
os.makedirs("test_dir")
with open("test_dir/.gitignore", "w") as f:
f.write("ignored.txt\n.hidden_ignored.txt")
with open("test_dir/ignored.txt", "w") as f:
f.write("This file should be ignored")
with open("test_dir/.hidden_ignored.txt", "w") as f:
f.write("This hidden file should be ignored")
with open("test_dir/included.txt", "w") as f:
f.write("This file should be included")
with open("test_dir/.hidden_included.txt", "w") as f:
f.write("This hidden file should be included")
with open("single_file.txt", "w") as f:
f.write("Contents of single file")
result = runner.invoke(cli, ["test_dir", "single_file.txt"])
assert result.exit_code == 0
assert "test_dir/ignored.txt" not in result.output
assert "test_dir/.hidden_ignored.txt" not in result.output
assert "test_dir/included.txt" in result.output
assert "test_dir/.hidden_included.txt" not in result.output
assert "single_file.txt" in result.output
assert "Contents of single file" in result.output
result = runner.invoke(cli, ["test_dir", "single_file.txt", "--include-hidden"])
assert result.exit_code == 0
assert "test_dir/ignored.txt" not in result.output
assert "test_dir/.hidden_ignored.txt" not in result.output
assert "test_dir/included.txt" in result.output
assert "test_dir/.hidden_included.txt" in result.output
assert "single_file.txt" in result.output
assert "Contents of single file" in result.output
result = runner.invoke(cli, ["test_dir", "single_file.txt", "--ignore-gitignore"])
assert result.exit_code == 0
assert "test_dir/ignored.txt" in result.output
assert "test_dir/.hidden_ignored.txt" not in result.output
assert "test_dir/included.txt" in result.output
assert "test_dir/.hidden_included.txt" not in result.output
assert "single_file.txt" in result.output
assert "Contents of single file" in result.output
This test mixes a directory (test_dir
) and a single file (single_file.txt
) as paths. It also tests the behavior of the .gitignore
file and the --include-hidden
and --ignore-gitignore
options.
The test creates a .gitignore
file inside test_dir
that ignores ignored.txt
and .hidden_ignored.txt
. It then creates several files inside test_dir
, including both ignored and included files, as well as hidden files.
The test runs the CLI command with different combinations of options:
- Without any options: It checks that the ignored files (
ignored.txt
and.hidden_ignored.txt
) are not included in the output, while the included files (included.txt
andsingle_file.txt
) are present. - With the
--include-hidden
option: It checks that the hidden included file (.hidden_included.txt
) is now present in the output. - With the
--ignore-gitignore
option: It checks that the ignored file (ignored.txt
) is now included in the output, but the hidden ignored file (.hidden_ignored.txt
) is still not included.
This test covers the combination of using single files and directories as paths while also testing the behavior of the .gitignore
and the related command-line options.