pilepack 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pilepack-0.1.0/LICENSE +19 -0
- pilepack-0.1.0/PKG-INFO +148 -0
- pilepack-0.1.0/README.md +133 -0
- pilepack-0.1.0/pilepack/__init__.py +0 -0
- pilepack-0.1.0/pilepack/__main__.py +5 -0
- pilepack-0.1.0/pilepack/cli.py +103 -0
- pilepack-0.1.0/pilepack/collector.py +37 -0
- pilepack-0.1.0/pilepack/formatter.py +68 -0
- pilepack-0.1.0/pilepack/ignorer.py +28 -0
- pilepack-0.1.0/pilepack/reader.py +46 -0
- pilepack-0.1.0/pilepack.egg-info/PKG-INFO +148 -0
- pilepack-0.1.0/pilepack.egg-info/SOURCES.txt +21 -0
- pilepack-0.1.0/pilepack.egg-info/dependency_links.txt +1 -0
- pilepack-0.1.0/pilepack.egg-info/entry_points.txt +2 -0
- pilepack-0.1.0/pilepack.egg-info/requires.txt +5 -0
- pilepack-0.1.0/pilepack.egg-info/top_level.txt +1 -0
- pilepack-0.1.0/pyproject.toml +29 -0
- pilepack-0.1.0/setup.cfg +4 -0
- pilepack-0.1.0/tests/test_cli.py +33 -0
- pilepack-0.1.0/tests/test_collector.py +32 -0
- pilepack-0.1.0/tests/test_formatter.py +31 -0
- pilepack-0.1.0/tests/test_ignorer.py +30 -0
- pilepack-0.1.0/tests/test_reader.py +35 -0
pilepack-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2026 Vasili S. Pribylov
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
pilepack-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pilepack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pack your codebase into a single file for AI analysis
|
|
5
|
+
Author-email: "Vasili S. Pribylov" <dartmew@yandex.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: pathspec>=0.10.0
|
|
11
|
+
Provides-Extra: test
|
|
12
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
13
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# PilePack
|
|
17
|
+
|
|
18
|
+
[](https://www.python.org/downloads/)
|
|
19
|
+
[](LICENSE)
|
|
20
|
+
[](https://pypi.org/project/pilepack/)
|
|
21
|
+
[](tests/)
|
|
22
|
+
[](tests/)
|
|
23
|
+
[]()
|
|
24
|
+
|
|
25
|
+
**Pack your codebase into a single file for AI analysis**
|
|
26
|
+
Combine all your project files into one text file — perfect for sending to LLMs (ChatGPT, Claude, Copilot, Deepseek, etc.).
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## ✨ Features
|
|
31
|
+
|
|
32
|
+
- 📁 **Recursive scanning** – walks through all files in a directory.
|
|
33
|
+
- 🚫 **Respects .gitignore** – optionally disable with `--no-gitignore`.
|
|
34
|
+
- 🌳 **Tree structure** – displays project hierarchy.
|
|
35
|
+
- 📄 **Embedded content** – each file is shown with its path header.
|
|
36
|
+
- 🔐 **Secrets masking** – hides passwords, tokens, keys (`--mask-secrets`).
|
|
37
|
+
- 🖨️ **Two output formats** – plain text (`txt`) or Markdown (`md`).
|
|
38
|
+
- 💾 **Save to file** – use `-o output.txt`.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 📦 Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install pilepack
|
|
46
|
+
```
|
|
47
|
+
From source:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone https://github.com/dartmew/pilepack.git
|
|
51
|
+
cd pilepack
|
|
52
|
+
pip install -e .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 🚀 Usage
|
|
56
|
+
Basic command – pass a path to your project:
|
|
57
|
+
```bash
|
|
58
|
+
pilepack /path/to/your/project
|
|
59
|
+
```
|
|
60
|
+
Redirect output to a file:
|
|
61
|
+
```bash
|
|
62
|
+
pilepack . > report.txt
|
|
63
|
+
```
|
|
64
|
+
Example output (txt)
|
|
65
|
+
```text
|
|
66
|
+
myproject
|
|
67
|
+
├── main.py
|
|
68
|
+
├── utils/
|
|
69
|
+
│ ├── helpers.py
|
|
70
|
+
│ └── __init__.py
|
|
71
|
+
└── README.md
|
|
72
|
+
|
|
73
|
+
================================================================================
|
|
74
|
+
|
|
75
|
+
--- FILE: main.py ---
|
|
76
|
+
import utils.helpers
|
|
77
|
+
|
|
78
|
+
def main():
|
|
79
|
+
print("Hello")
|
|
80
|
+
|
|
81
|
+
--- FILE: utils/helpers.py ---
|
|
82
|
+
def greet(name):
|
|
83
|
+
return f"Hi {name}"
|
|
84
|
+
```
|
|
85
|
+
Markdown format
|
|
86
|
+
```bash
|
|
87
|
+
pilepack . -f md -o report.md
|
|
88
|
+
```
|
|
89
|
+
Produces a Markdown file with syntax highlighting.
|
|
90
|
+
|
|
91
|
+
Show only structure (no file contents)
|
|
92
|
+
```bash
|
|
93
|
+
pilepack . --no-content
|
|
94
|
+
```
|
|
95
|
+
Mask secrets
|
|
96
|
+
```bash
|
|
97
|
+
pilepack . --mask-secrets
|
|
98
|
+
```
|
|
99
|
+
Replaces values of password=, api_key=, token=, and long strings (base64/hex) with ***.
|
|
100
|
+
|
|
101
|
+
Disable .gitignore
|
|
102
|
+
```bash
|
|
103
|
+
pilepack . --no-gitignore
|
|
104
|
+
```
|
|
105
|
+
## 📋 CLI Options
|
|
106
|
+
| Option | Description |
|
|
107
|
+
|--------|-------------|
|
|
108
|
+
| `root` | Directory to scan (default: current directory) |
|
|
109
|
+
| `--no-content` | Show tree structure only, skip file contents |
|
|
110
|
+
| `--mask-secrets` | Mask passwords, tokens, API keys |
|
|
111
|
+
| `-o, --output` | Write report to a file instead of stdout |
|
|
112
|
+
| `--no-gitignore` | Do not respect `.gitignore` (include all files) |
|
|
113
|
+
| `-f, --format` | Output format: `txt` (default) or `md` |
|
|
114
|
+
|
|
115
|
+
## 🧪 Testing
|
|
116
|
+
Install test dependencies and run:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pip install -e .[test]
|
|
120
|
+
pytest
|
|
121
|
+
```
|
|
122
|
+
With coverage:
|
|
123
|
+
```bash
|
|
124
|
+
pytest --cov=pilepack
|
|
125
|
+
```
|
|
126
|
+
Current coverage: 84% (20 tests, all passing).
|
|
127
|
+
```bash
|
|
128
|
+
Name Stmts Miss Cover
|
|
129
|
+
-------------------------------------------
|
|
130
|
+
pilepack\__init__.py 0 0 100%
|
|
131
|
+
pilepack\__main__.py 3 3 0%
|
|
132
|
+
pilepack\cli.py 51 7 86%
|
|
133
|
+
pilepack\collector.py 30 1 97%
|
|
134
|
+
pilepack\formatter.py 44 3 93%
|
|
135
|
+
pilepack\ignorer.py 21 3 86%
|
|
136
|
+
pilepack\reader.py 31 12 61%
|
|
137
|
+
-------------------------------------------
|
|
138
|
+
TOTAL 180 29 84%
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## 📄 License
|
|
142
|
+
[MIT](LICENSE) © 2026 Vasili S. Pribylov
|
|
143
|
+
|
|
144
|
+
## 🤝 Contributing
|
|
145
|
+
Issues and pull requests are welcome! For major changes, please open an issue first to discuss.
|
|
146
|
+
|
|
147
|
+
## 🙏 Acknowledgements
|
|
148
|
+
Inspired by the need to easily feed code into large language models.
|
pilepack-0.1.0/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# PilePack
|
|
2
|
+
|
|
3
|
+
[](https://www.python.org/downloads/)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[](https://pypi.org/project/pilepack/)
|
|
6
|
+
[](tests/)
|
|
7
|
+
[](tests/)
|
|
8
|
+
[]()
|
|
9
|
+
|
|
10
|
+
**Pack your codebase into a single file for AI analysis**
|
|
11
|
+
Combine all your project files into one text file — perfect for sending to LLMs (ChatGPT, Claude, Copilot, Deepseek, etc.).
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ✨ Features
|
|
16
|
+
|
|
17
|
+
- 📁 **Recursive scanning** – walks through all files in a directory.
|
|
18
|
+
- 🚫 **Respects .gitignore** – optionally disable with `--no-gitignore`.
|
|
19
|
+
- 🌳 **Tree structure** – displays project hierarchy.
|
|
20
|
+
- 📄 **Embedded content** – each file is shown with its path header.
|
|
21
|
+
- 🔐 **Secrets masking** – hides passwords, tokens, keys (`--mask-secrets`).
|
|
22
|
+
- 🖨️ **Two output formats** – plain text (`txt`) or Markdown (`md`).
|
|
23
|
+
- 💾 **Save to file** – use `-o output.txt`.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 📦 Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install pilepack
|
|
31
|
+
```
|
|
32
|
+
From source:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git clone https://github.com/dartmew/pilepack.git
|
|
36
|
+
cd pilepack
|
|
37
|
+
pip install -e .
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## 🚀 Usage
|
|
41
|
+
Basic command – pass a path to your project:
|
|
42
|
+
```bash
|
|
43
|
+
pilepack /path/to/your/project
|
|
44
|
+
```
|
|
45
|
+
Redirect output to a file:
|
|
46
|
+
```bash
|
|
47
|
+
pilepack . > report.txt
|
|
48
|
+
```
|
|
49
|
+
Example output (txt)
|
|
50
|
+
```text
|
|
51
|
+
myproject
|
|
52
|
+
├── main.py
|
|
53
|
+
├── utils/
|
|
54
|
+
│ ├── helpers.py
|
|
55
|
+
│ └── __init__.py
|
|
56
|
+
└── README.md
|
|
57
|
+
|
|
58
|
+
================================================================================
|
|
59
|
+
|
|
60
|
+
--- FILE: main.py ---
|
|
61
|
+
import utils.helpers
|
|
62
|
+
|
|
63
|
+
def main():
|
|
64
|
+
print("Hello")
|
|
65
|
+
|
|
66
|
+
--- FILE: utils/helpers.py ---
|
|
67
|
+
def greet(name):
|
|
68
|
+
return f"Hi {name}"
|
|
69
|
+
```
|
|
70
|
+
Markdown format
|
|
71
|
+
```bash
|
|
72
|
+
pilepack . -f md -o report.md
|
|
73
|
+
```
|
|
74
|
+
Produces a Markdown file with syntax highlighting.
|
|
75
|
+
|
|
76
|
+
Show only structure (no file contents)
|
|
77
|
+
```bash
|
|
78
|
+
pilepack . --no-content
|
|
79
|
+
```
|
|
80
|
+
Mask secrets
|
|
81
|
+
```bash
|
|
82
|
+
pilepack . --mask-secrets
|
|
83
|
+
```
|
|
84
|
+
Replaces values of password=, api_key=, token=, and long strings (base64/hex) with ***.
|
|
85
|
+
|
|
86
|
+
Disable .gitignore
|
|
87
|
+
```bash
|
|
88
|
+
pilepack . --no-gitignore
|
|
89
|
+
```
|
|
90
|
+
## 📋 CLI Options
|
|
91
|
+
| Option | Description |
|
|
92
|
+
|--------|-------------|
|
|
93
|
+
| `root` | Directory to scan (default: current directory) |
|
|
94
|
+
| `--no-content` | Show tree structure only, skip file contents |
|
|
95
|
+
| `--mask-secrets` | Mask passwords, tokens, API keys |
|
|
96
|
+
| `-o, --output` | Write report to a file instead of stdout |
|
|
97
|
+
| `--no-gitignore` | Do not respect `.gitignore` (include all files) |
|
|
98
|
+
| `-f, --format` | Output format: `txt` (default) or `md` |
|
|
99
|
+
|
|
100
|
+
## 🧪 Testing
|
|
101
|
+
Install test dependencies and run:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install -e .[test]
|
|
105
|
+
pytest
|
|
106
|
+
```
|
|
107
|
+
With coverage:
|
|
108
|
+
```bash
|
|
109
|
+
pytest --cov=pilepack
|
|
110
|
+
```
|
|
111
|
+
Current coverage: 84% (20 tests, all passing).
|
|
112
|
+
```bash
|
|
113
|
+
Name Stmts Miss Cover
|
|
114
|
+
-------------------------------------------
|
|
115
|
+
pilepack\__init__.py 0 0 100%
|
|
116
|
+
pilepack\__main__.py 3 3 0%
|
|
117
|
+
pilepack\cli.py 51 7 86%
|
|
118
|
+
pilepack\collector.py 30 1 97%
|
|
119
|
+
pilepack\formatter.py 44 3 93%
|
|
120
|
+
pilepack\ignorer.py 21 3 86%
|
|
121
|
+
pilepack\reader.py 31 12 61%
|
|
122
|
+
-------------------------------------------
|
|
123
|
+
TOTAL 180 29 84%
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## 📄 License
|
|
127
|
+
[MIT](LICENSE) © 2026 Vasili S. Pribylov
|
|
128
|
+
|
|
129
|
+
## 🤝 Contributing
|
|
130
|
+
Issues and pull requests are welcome! For major changes, please open an issue first to discuss.
|
|
131
|
+
|
|
132
|
+
## 🙏 Acknowledgements
|
|
133
|
+
Inspired by the need to easily feed code into large language models.
|
|
File without changes
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from .collector import collect_files, build_tree
|
|
6
|
+
from .reader import read_file
|
|
7
|
+
from .formatter import render
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _generate_report(root_path: Path, include_content=True, mask_secrets=False, follow_gitignore=True, fmt="txt"):
|
|
11
|
+
print("Scanning files...", file=sys.stderr, flush=True)
|
|
12
|
+
files = collect_files(root_path, follow_gitignore=follow_gitignore)
|
|
13
|
+
tree = build_tree(files)
|
|
14
|
+
|
|
15
|
+
files_content = []
|
|
16
|
+
if include_content:
|
|
17
|
+
total = len(files)
|
|
18
|
+
print(f"Reading {total} files...", file=sys.stderr, flush=True)
|
|
19
|
+
for rel_path in files:
|
|
20
|
+
abs_path = root_path / rel_path
|
|
21
|
+
content = read_file(abs_path, mask_secrets=mask_secrets)
|
|
22
|
+
files_content.append((rel_path, content))
|
|
23
|
+
print(f"Done. Read {total} files.", file=sys.stderr, flush=True)
|
|
24
|
+
else:
|
|
25
|
+
files_content = []
|
|
26
|
+
|
|
27
|
+
root_name = root_path.name or str(root_path)
|
|
28
|
+
report = render(root_name, tree, files_content, fmt=fmt)
|
|
29
|
+
return report
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main():
|
|
33
|
+
parser = argparse.ArgumentParser(
|
|
34
|
+
description="Pack your codebase into a single file for AI analysis"
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument(
|
|
37
|
+
"root",
|
|
38
|
+
nargs="?",
|
|
39
|
+
default=".",
|
|
40
|
+
help="Root directory to scan (default: current directory)",
|
|
41
|
+
)
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--no-content",
|
|
44
|
+
dest="include_content",
|
|
45
|
+
action="store_false",
|
|
46
|
+
help="Do not include file contents (only show tree structure)"
|
|
47
|
+
)
|
|
48
|
+
parser.add_argument(
|
|
49
|
+
"--mask-secrets",
|
|
50
|
+
action="store_true",
|
|
51
|
+
help="Mask sensitive information like passwords, tokens, etc."
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"-o", "--output",
|
|
55
|
+
type=Path,
|
|
56
|
+
help="Write output to a file instead of stdout"
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--no-gitignore",
|
|
60
|
+
dest="follow_gitignore",
|
|
61
|
+
action="store_false",
|
|
62
|
+
help="Do NOT respect .gitignore rules (include all files)"
|
|
63
|
+
)
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"-f", "--format",
|
|
66
|
+
choices=["txt", "md"],
|
|
67
|
+
default="txt",
|
|
68
|
+
help="Output format: txt (default) or md",
|
|
69
|
+
metavar='FMT'
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
args = parser.parse_args()
|
|
73
|
+
|
|
74
|
+
root_path = Path(args.root).resolve()
|
|
75
|
+
if not root_path.is_dir():
|
|
76
|
+
print(f"Error: '{root_path}' is not a valid directory.", file=sys.stderr)
|
|
77
|
+
sys.exit(1)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
report = _generate_report(
|
|
81
|
+
root_path,
|
|
82
|
+
include_content=args.include_content,
|
|
83
|
+
mask_secrets=args.mask_secrets,
|
|
84
|
+
follow_gitignore=args.follow_gitignore,
|
|
85
|
+
fmt=args.format,
|
|
86
|
+
)
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(f"Error generating report: {e}", file=sys.stderr)
|
|
89
|
+
sys.exit(1)
|
|
90
|
+
|
|
91
|
+
if args.output:
|
|
92
|
+
try:
|
|
93
|
+
args.output.write_text(report, encoding='utf-8')
|
|
94
|
+
print(f"Report written to {args.output}")
|
|
95
|
+
except IOError as e:
|
|
96
|
+
print(f"Error writing to file: {e}", file=sys.stderr)
|
|
97
|
+
sys.exit(1)
|
|
98
|
+
else:
|
|
99
|
+
print(report)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
if __name__ == "__main__":
|
|
103
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from .ignorer import load_gitignore, is_ignored
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def collect_files(root_path: Path, follow_gitignore: bool = True) -> list[Path]:
|
|
7
|
+
if not root_path.is_dir():
|
|
8
|
+
raise NotADirectoryError(f"{root_path} does not exist or is not a directory")
|
|
9
|
+
|
|
10
|
+
if follow_gitignore:
|
|
11
|
+
spec = load_gitignore(root_path)
|
|
12
|
+
else:
|
|
13
|
+
spec = None
|
|
14
|
+
collected = []
|
|
15
|
+
|
|
16
|
+
for item in root_path.rglob('*'):
|
|
17
|
+
if item.name == '.gitignore':
|
|
18
|
+
continue
|
|
19
|
+
if '.git' in item.parts:
|
|
20
|
+
continue
|
|
21
|
+
if spec and is_ignored(item, root_path, spec):
|
|
22
|
+
continue
|
|
23
|
+
if item.is_file():
|
|
24
|
+
rel = item.relative_to(root_path)
|
|
25
|
+
collected.append(rel)
|
|
26
|
+
return collected
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_tree(files: List[Path]) -> Dict:
|
|
30
|
+
tree = {}
|
|
31
|
+
for path in files:
|
|
32
|
+
parts = path.parts
|
|
33
|
+
current = tree
|
|
34
|
+
for part in parts[:-1]:
|
|
35
|
+
current = current.setdefault(part, {})
|
|
36
|
+
current[parts[-1]] = None
|
|
37
|
+
return tree
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from typing import Dict, List, Optional, Tuple
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _format_tree(tree: Dict, prefix: str = '', is_last: bool = True) -> str:
|
|
6
|
+
lines = []
|
|
7
|
+
items = sorted(tree.items(), key=lambda x: (isinstance(x[1], dict), x[0].lower()), reverse=True)
|
|
8
|
+
|
|
9
|
+
for i, (name, subtree) in enumerate(items):
|
|
10
|
+
is_last_item = (i == len(items) - 1)
|
|
11
|
+
connector = '└── ' if is_last_item else '├── '
|
|
12
|
+
display_name = name + '/' if isinstance(subtree, dict) else name
|
|
13
|
+
lines.append(prefix + connector + display_name)
|
|
14
|
+
|
|
15
|
+
if isinstance(subtree, dict):
|
|
16
|
+
new_prefix = prefix + (' ' if is_last_item else '│ ')
|
|
17
|
+
lines.append(_format_tree(subtree, new_prefix, is_last_item))
|
|
18
|
+
return '\n'.join(lines)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _render_txt(root_name: str, tree: Dict, files_content: List[Tuple[Path, Optional[str]]]) -> str:
|
|
22
|
+
tree_str = _format_tree(tree)
|
|
23
|
+
output = [f"{root_name}\n{tree_str}"]
|
|
24
|
+
|
|
25
|
+
if files_content:
|
|
26
|
+
output.append("\n" + "=" * 80 + "\n")
|
|
27
|
+
|
|
28
|
+
for rel_path, content in files_content:
|
|
29
|
+
|
|
30
|
+
if content is not None:
|
|
31
|
+
output.append(f"--- FILE: {rel_path} ---")
|
|
32
|
+
output.append(content)
|
|
33
|
+
output.append("")
|
|
34
|
+
else:
|
|
35
|
+
output.append(f"--- FILE: {rel_path} [BINARY OR UNREADABLE] ---\n")
|
|
36
|
+
return "\n".join(output)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _render_md(root_name: str, tree: Dict, files_content: List[Tuple[Path, Optional[str]]]) -> str:
|
|
40
|
+
tree_str = _format_tree(tree)
|
|
41
|
+
output = [f"# {root_name}\n```\n{tree_str}\n```"]
|
|
42
|
+
|
|
43
|
+
if files_content:
|
|
44
|
+
output.append("\n---\n")
|
|
45
|
+
|
|
46
|
+
for rel_path, content in files_content:
|
|
47
|
+
|
|
48
|
+
if content is not None:
|
|
49
|
+
ext = rel_path.suffix.lower()
|
|
50
|
+
lang = {
|
|
51
|
+
'.py': 'python', '.js': 'javascript', '.ts': 'typescript',
|
|
52
|
+
'.html': 'html', '.css': 'css', '.json': 'json',
|
|
53
|
+
'.md': 'markdown', '.yaml': 'yaml', '.yml': 'yaml',
|
|
54
|
+
'.sh': 'bash', '.txt': 'text'
|
|
55
|
+
}.get(ext, 'text')
|
|
56
|
+
output.append(f"## `{rel_path}`\n```{lang}\n{content}\n```\n")
|
|
57
|
+
else:
|
|
58
|
+
output.append(f"## `{rel_path}`\n*[BINARY OR UNREADABLE]*\n")
|
|
59
|
+
return '\n'.join(output)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def render(root_name: str, tree: Dict, files_content: List[Tuple[Path, Optional[str]]], fmt: str = "txt") -> str:
|
|
63
|
+
if fmt == "txt":
|
|
64
|
+
return _render_txt(root_name, tree, files_content)
|
|
65
|
+
elif fmt == "md":
|
|
66
|
+
return _render_md(root_name, tree, files_content)
|
|
67
|
+
else:
|
|
68
|
+
raise ValueError(f"Unsupported format: {fmt}. Supported: 'txt', 'md'")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from pathspec import PathSpec
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def load_gitignore(root_path: Path) -> PathSpec:
|
|
6
|
+
gitignore_path = root_path / '.gitignore'
|
|
7
|
+
if not gitignore_path.exists():
|
|
8
|
+
return PathSpec.from_lines('gitignore', [])
|
|
9
|
+
with open(gitignore_path, 'r', encoding='utf-8') as file:
|
|
10
|
+
return PathSpec.from_lines('gitignore', file)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def is_ignored(path, root: Path, spec: PathSpec) -> bool:
|
|
14
|
+
original = str(path)
|
|
15
|
+
path_obj = Path(original)
|
|
16
|
+
|
|
17
|
+
if not path_obj.is_absolute():
|
|
18
|
+
path_obj = root / path_obj
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
rel_path = path_obj.relative_to(root)
|
|
22
|
+
except ValueError:
|
|
23
|
+
return False
|
|
24
|
+
rel_str = rel_path.as_posix()
|
|
25
|
+
|
|
26
|
+
if original.endswith(('/', '\\')) or (path_obj.exists() and path_obj.is_dir()):
|
|
27
|
+
rel_str += '/'
|
|
28
|
+
return spec.match_file(rel_str)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
SECRET_PATTERNS = [
|
|
6
|
+
(r'(password|passwd|pwd)\s*[=:]\s*["\']?([^"\'\s]+)["\']?', r'\1="***"'),
|
|
7
|
+
(r'(api_key|apikey)\s*[=:]\s*["\']?([^"\'\s]+)["\']?', r'\1="***"'),
|
|
8
|
+
(r'(token|access_token)\s*[=:]\s*["\']?([^"\'\s]+)["\']?', r'\1="***"'),
|
|
9
|
+
(r'(secret|private_key)\s*[=:]\s*["\']?([^"\'\s]+)["\']?', r'\1="***"'),
|
|
10
|
+
(r'(?:\\b[A-Za-z0-9+/]{40,}\\b)', '***'),
|
|
11
|
+
(r'(?:\\b[0-9a-f]{32,}\\b)', '***')
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
def _mask_secrets_in_text(text: str) -> str:
|
|
15
|
+
for pattern, replacement in SECRET_PATTERNS:
|
|
16
|
+
text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
|
|
17
|
+
return text
|
|
18
|
+
|
|
19
|
+
def read_file(file_path: Path, mask_secrets: bool = False) -> Optional[str]:
|
|
20
|
+
try:
|
|
21
|
+
raw_data = file_path.read_bytes()
|
|
22
|
+
except (OSError, IOError) as e:
|
|
23
|
+
print(f"Failed to read {file_path}: {e}")
|
|
24
|
+
return None
|
|
25
|
+
|
|
26
|
+
if b'\x00' in raw_data:
|
|
27
|
+
return None
|
|
28
|
+
try:
|
|
29
|
+
text = raw_data.decode('utf-8-sig')
|
|
30
|
+
except UnicodeDecodeError:
|
|
31
|
+
for enc in ('utf-8', 'cp1251', 'latin1'):
|
|
32
|
+
try:
|
|
33
|
+
text = raw_data.decode(enc)
|
|
34
|
+
break
|
|
35
|
+
except UnicodeDecodeError:
|
|
36
|
+
continue
|
|
37
|
+
else:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
if text.startswith('\ufeff'):
|
|
41
|
+
text = text[1:]
|
|
42
|
+
|
|
43
|
+
if mask_secrets:
|
|
44
|
+
text = _mask_secrets_in_text(text)
|
|
45
|
+
|
|
46
|
+
return text
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pilepack
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pack your codebase into a single file for AI analysis
|
|
5
|
+
Author-email: "Vasili S. Pribylov" <dartmew@yandex.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.8
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: pathspec>=0.10.0
|
|
11
|
+
Provides-Extra: test
|
|
12
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
13
|
+
Requires-Dist: pytest-cov; extra == "test"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# PilePack
|
|
17
|
+
|
|
18
|
+
[](https://www.python.org/downloads/)
|
|
19
|
+
[](LICENSE)
|
|
20
|
+
[](https://pypi.org/project/pilepack/)
|
|
21
|
+
[](tests/)
|
|
22
|
+
[](tests/)
|
|
23
|
+
[]()
|
|
24
|
+
|
|
25
|
+
**Pack your codebase into a single file for AI analysis**
|
|
26
|
+
Combine all your project files into one text file — perfect for sending to LLMs (ChatGPT, Claude, Copilot, Deepseek, etc.).
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## ✨ Features
|
|
31
|
+
|
|
32
|
+
- 📁 **Recursive scanning** – walks through all files in a directory.
|
|
33
|
+
- 🚫 **Respects .gitignore** – optionally disable with `--no-gitignore`.
|
|
34
|
+
- 🌳 **Tree structure** – displays project hierarchy.
|
|
35
|
+
- 📄 **Embedded content** – each file is shown with its path header.
|
|
36
|
+
- 🔐 **Secrets masking** – hides passwords, tokens, keys (`--mask-secrets`).
|
|
37
|
+
- 🖨️ **Two output formats** – plain text (`txt`) or Markdown (`md`).
|
|
38
|
+
- 💾 **Save to file** – use `-o output.txt`.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 📦 Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install pilepack
|
|
46
|
+
```
|
|
47
|
+
From source:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git clone https://github.com/dartmew/pilepack.git
|
|
51
|
+
cd pilepack
|
|
52
|
+
pip install -e .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 🚀 Usage
|
|
56
|
+
Basic command – pass a path to your project:
|
|
57
|
+
```bash
|
|
58
|
+
pilepack /path/to/your/project
|
|
59
|
+
```
|
|
60
|
+
Redirect output to a file:
|
|
61
|
+
```bash
|
|
62
|
+
pilepack . > report.txt
|
|
63
|
+
```
|
|
64
|
+
Example output (txt)
|
|
65
|
+
```text
|
|
66
|
+
myproject
|
|
67
|
+
├── main.py
|
|
68
|
+
├── utils/
|
|
69
|
+
│ ├── helpers.py
|
|
70
|
+
│ └── __init__.py
|
|
71
|
+
└── README.md
|
|
72
|
+
|
|
73
|
+
================================================================================
|
|
74
|
+
|
|
75
|
+
--- FILE: main.py ---
|
|
76
|
+
import utils.helpers
|
|
77
|
+
|
|
78
|
+
def main():
|
|
79
|
+
print("Hello")
|
|
80
|
+
|
|
81
|
+
--- FILE: utils/helpers.py ---
|
|
82
|
+
def greet(name):
|
|
83
|
+
return f"Hi {name}"
|
|
84
|
+
```
|
|
85
|
+
Markdown format
|
|
86
|
+
```bash
|
|
87
|
+
pilepack . -f md -o report.md
|
|
88
|
+
```
|
|
89
|
+
Produces a Markdown file with syntax highlighting.
|
|
90
|
+
|
|
91
|
+
Show only structure (no file contents)
|
|
92
|
+
```bash
|
|
93
|
+
pilepack . --no-content
|
|
94
|
+
```
|
|
95
|
+
Mask secrets
|
|
96
|
+
```bash
|
|
97
|
+
pilepack . --mask-secrets
|
|
98
|
+
```
|
|
99
|
+
Replaces values of password=, api_key=, token=, and long strings (base64/hex) with ***.
|
|
100
|
+
|
|
101
|
+
Disable .gitignore
|
|
102
|
+
```bash
|
|
103
|
+
pilepack . --no-gitignore
|
|
104
|
+
```
|
|
105
|
+
## 📋 CLI Options
|
|
106
|
+
| Option | Description |
|
|
107
|
+
|--------|-------------|
|
|
108
|
+
| `root` | Directory to scan (default: current directory) |
|
|
109
|
+
| `--no-content` | Show tree structure only, skip file contents |
|
|
110
|
+
| `--mask-secrets` | Mask passwords, tokens, API keys |
|
|
111
|
+
| `-o, --output` | Write report to a file instead of stdout |
|
|
112
|
+
| `--no-gitignore` | Do not respect `.gitignore` (include all files) |
|
|
113
|
+
| `-f, --format` | Output format: `txt` (default) or `md` |
|
|
114
|
+
|
|
115
|
+
## 🧪 Testing
|
|
116
|
+
Install test dependencies and run:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
pip install -e .[test]
|
|
120
|
+
pytest
|
|
121
|
+
```
|
|
122
|
+
With coverage:
|
|
123
|
+
```bash
|
|
124
|
+
pytest --cov=pilepack
|
|
125
|
+
```
|
|
126
|
+
Current coverage: 84% (20 tests, all passing).
|
|
127
|
+
```bash
|
|
128
|
+
Name Stmts Miss Cover
|
|
129
|
+
-------------------------------------------
|
|
130
|
+
pilepack\__init__.py 0 0 100%
|
|
131
|
+
pilepack\__main__.py 3 3 0%
|
|
132
|
+
pilepack\cli.py 51 7 86%
|
|
133
|
+
pilepack\collector.py 30 1 97%
|
|
134
|
+
pilepack\formatter.py 44 3 93%
|
|
135
|
+
pilepack\ignorer.py 21 3 86%
|
|
136
|
+
pilepack\reader.py 31 12 61%
|
|
137
|
+
-------------------------------------------
|
|
138
|
+
TOTAL 180 29 84%
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## 📄 License
|
|
142
|
+
[MIT](LICENSE) © 2026 Vasili S. Pribylov
|
|
143
|
+
|
|
144
|
+
## 🤝 Contributing
|
|
145
|
+
Issues and pull requests are welcome! For major changes, please open an issue first to discuss.
|
|
146
|
+
|
|
147
|
+
## 🙏 Acknowledgements
|
|
148
|
+
Inspired by the need to easily feed code into large language models.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
pilepack/__init__.py
|
|
5
|
+
pilepack/__main__.py
|
|
6
|
+
pilepack/cli.py
|
|
7
|
+
pilepack/collector.py
|
|
8
|
+
pilepack/formatter.py
|
|
9
|
+
pilepack/ignorer.py
|
|
10
|
+
pilepack/reader.py
|
|
11
|
+
pilepack.egg-info/PKG-INFO
|
|
12
|
+
pilepack.egg-info/SOURCES.txt
|
|
13
|
+
pilepack.egg-info/dependency_links.txt
|
|
14
|
+
pilepack.egg-info/entry_points.txt
|
|
15
|
+
pilepack.egg-info/requires.txt
|
|
16
|
+
pilepack.egg-info/top_level.txt
|
|
17
|
+
tests/test_cli.py
|
|
18
|
+
tests/test_collector.py
|
|
19
|
+
tests/test_formatter.py
|
|
20
|
+
tests/test_ignorer.py
|
|
21
|
+
tests/test_reader.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pilepack
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pilepack"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Pack your codebase into a single file for AI analysis"
|
|
9
|
+
authors = [{ name = "Vasili S. Pribylov", email = "dartmew@yandex.com" }]
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
readme = "README.md"
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"pathspec>=0.10.0",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
pilepack = "pilepack.cli:main"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools.packages.find]
|
|
21
|
+
where = ["."]
|
|
22
|
+
include = ["pilepack*"]
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
test = ["pytest>=7.0", "pytest-cov"]
|
|
26
|
+
|
|
27
|
+
[tool.pytest.ini_options]
|
|
28
|
+
testpaths = ["tests"]
|
|
29
|
+
python_files = "test_*.py"
|
pilepack-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from pilepack.cli import main, _generate_report
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
def test_generate_report_no_content(test_project):
|
|
7
|
+
report = _generate_report(test_project, include_content=False, fmt="txt")
|
|
8
|
+
assert "--- FILE:" not in report
|
|
9
|
+
assert "test_project" in report
|
|
10
|
+
assert "main.py" in report
|
|
11
|
+
|
|
12
|
+
def test_cli_basic(test_project, capsys, monkeypatch):
|
|
13
|
+
monkeypatch.setattr(sys, "argv", ["pilepack", str(test_project), "--no-content"])
|
|
14
|
+
main()
|
|
15
|
+
captured = capsys.readouterr()
|
|
16
|
+
assert "test_project" in captured.out
|
|
17
|
+
assert "main.py" in captured.out
|
|
18
|
+
assert "--- FILE:" not in captured.out
|
|
19
|
+
|
|
20
|
+
def test_cli_output_file(test_project, tmp_path, monkeypatch):
|
|
21
|
+
out_file = tmp_path / "report.txt"
|
|
22
|
+
monkeypatch.setattr(sys, "argv", ["pilepack", str(test_project), "-o", str(out_file)])
|
|
23
|
+
main()
|
|
24
|
+
assert out_file.exists()
|
|
25
|
+
content = out_file.read_text()
|
|
26
|
+
assert "main.py" in content
|
|
27
|
+
|
|
28
|
+
def test_cli_invalid_dir(capsys, monkeypatch):
|
|
29
|
+
monkeypatch.setattr(sys, "argv", ["pilepack", "/nonexistent"])
|
|
30
|
+
with pytest.raises(SystemExit):
|
|
31
|
+
main()
|
|
32
|
+
captured = capsys.readouterr()
|
|
33
|
+
assert "not a valid directory" in captured.err
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pilepack.collector import collect_files, build_tree
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
def test_collect_files_respect_gitignore(test_project):
|
|
6
|
+
(test_project / ".gitignore").write_text("utils/\n")
|
|
7
|
+
files = collect_files(test_project, follow_gitignore=True)
|
|
8
|
+
rel_paths = [str(p) for p in files]
|
|
9
|
+
assert "utils/helpers.py" not in rel_paths
|
|
10
|
+
assert "main.py" in rel_paths
|
|
11
|
+
|
|
12
|
+
def test_collect_files_ignore_gitignore(test_project):
|
|
13
|
+
(test_project / ".gitignore").write_text("main.py")
|
|
14
|
+
files = collect_files(test_project, follow_gitignore=False)
|
|
15
|
+
rel_paths = [str(p) for p in files]
|
|
16
|
+
assert "main.py" in rel_paths
|
|
17
|
+
|
|
18
|
+
def test_collect_files_excludes_git_and_gitignore(test_project):
|
|
19
|
+
(test_project / ".git").mkdir()
|
|
20
|
+
(test_project / ".git" / "config").touch()
|
|
21
|
+
(test_project / ".gitignore").touch()
|
|
22
|
+
files = collect_files(test_project, follow_gitignore=False)
|
|
23
|
+
rel_paths = [str(p) for p in files]
|
|
24
|
+
assert ".git" not in rel_paths
|
|
25
|
+
assert ".gitignore" not in rel_paths
|
|
26
|
+
assert "main.py" in rel_paths
|
|
27
|
+
|
|
28
|
+
def test_build_tree():
|
|
29
|
+
files = [Path("a/b/c.py"), Path("a/d.py"), Path("e.py")]
|
|
30
|
+
tree = build_tree(files)
|
|
31
|
+
expected = {"a": {"b": {"c.py": None}, "d.py": None}, "e.py": None}
|
|
32
|
+
assert tree == expected
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from pilepack.formatter import render, _format_tree
|
|
4
|
+
|
|
5
|
+
def test_format_tree_simple():
|
|
6
|
+
tree = {"a.py": None, "b": {"c.py": None}}
|
|
7
|
+
result = _format_tree(tree)
|
|
8
|
+
assert "b/" in result
|
|
9
|
+
assert "a.py" in result
|
|
10
|
+
|
|
11
|
+
def test_render_txt(test_project, tmp_path):
|
|
12
|
+
from pilepack.collector import collect_files, build_tree
|
|
13
|
+
files = collect_files(test_project, follow_gitignore=False)
|
|
14
|
+
tree = build_tree(files)
|
|
15
|
+
content_list = []
|
|
16
|
+
for rel in files:
|
|
17
|
+
if rel.name == "main.py":
|
|
18
|
+
content = (test_project / rel).read_text()
|
|
19
|
+
content_list.append((rel, content))
|
|
20
|
+
output = render("test_project", tree, content_list, fmt="txt")
|
|
21
|
+
assert "--- FILE: main.py ---" in output
|
|
22
|
+
assert "def main():" in output
|
|
23
|
+
|
|
24
|
+
def test_render_md(test_project):
|
|
25
|
+
from pilepack.collector import collect_files, build_tree
|
|
26
|
+
files = collect_files(test_project, follow_gitignore=False)
|
|
27
|
+
tree = build_tree(files[:2])
|
|
28
|
+
content_list = [(files[0], (test_project / files[0]).read_text())]
|
|
29
|
+
output = render("test_project", tree, content_list, fmt="md")
|
|
30
|
+
assert "## `main.py`" in output
|
|
31
|
+
assert "```python" in output
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from pilepack.ignorer import load_gitignore, is_ignored
|
|
4
|
+
|
|
5
|
+
def test_load_gitignore_missing(tmp_path):
|
|
6
|
+
spec = load_gitignore(tmp_path)
|
|
7
|
+
assert not spec.match_file("any.py")
|
|
8
|
+
|
|
9
|
+
def test_load_gitignore_existing(test_project):
|
|
10
|
+
gitignore = test_project / ".gitignore"
|
|
11
|
+
gitignore.write_text("*.log\ntemp/\n")
|
|
12
|
+
spec = load_gitignore(test_project)
|
|
13
|
+
assert spec.match_file("debug.log")
|
|
14
|
+
assert spec.match_file("temp/file.txt")
|
|
15
|
+
assert not spec.match_file("main.py")
|
|
16
|
+
|
|
17
|
+
def test_is_ignored_file(test_project):
|
|
18
|
+
gitignore = test_project / ".gitignore"
|
|
19
|
+
gitignore.write_text("*.log")
|
|
20
|
+
spec = load_gitignore(test_project)
|
|
21
|
+
log_file = test_project / "debug.log"
|
|
22
|
+
assert is_ignored(log_file, test_project, spec) is True
|
|
23
|
+
assert is_ignored(test_project / "main.py", test_project, spec) is False
|
|
24
|
+
|
|
25
|
+
def test_is_ignored_directory(test_project):
|
|
26
|
+
gitignore = test_project / ".gitignore"
|
|
27
|
+
gitignore.write_text("temp/")
|
|
28
|
+
spec = load_gitignore(test_project)
|
|
29
|
+
temp_dir = test_project / "temp"
|
|
30
|
+
assert is_ignored(temp_dir, test_project, spec) is True
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from pilepack.reader import read_file, _mask_secrets_in_text
|
|
4
|
+
|
|
5
|
+
def test_read_text_utf8(tmp_path):
|
|
6
|
+
f = tmp_path / "test.txt"
|
|
7
|
+
f.write_text("hello world", encoding="utf-8")
|
|
8
|
+
content = read_file(f)
|
|
9
|
+
assert content == "hello world"
|
|
10
|
+
|
|
11
|
+
def test_read_binary(tmp_path):
|
|
12
|
+
f = tmp_path / "binary.bin"
|
|
13
|
+
f.write_bytes(b'\x00\x01\x02\x03')
|
|
14
|
+
assert read_file(f) is None
|
|
15
|
+
|
|
16
|
+
def test_read_with_bom(tmp_path):
|
|
17
|
+
f = tmp_path / "bom.txt"
|
|
18
|
+
f.write_bytes(b'\xef\xbb\xbfhello')
|
|
19
|
+
content = read_file(f)
|
|
20
|
+
assert content == "hello"
|
|
21
|
+
assert not content.startswith('\ufeff')
|
|
22
|
+
|
|
23
|
+
def test_mask_secrets():
|
|
24
|
+
text = "password=12345, API_KEY=abc123"
|
|
25
|
+
masked = _mask_secrets_in_text(text)
|
|
26
|
+
assert "password=\"***\"" in masked
|
|
27
|
+
assert "API_KEY=\"***\"" in masked
|
|
28
|
+
assert "12345" not in masked
|
|
29
|
+
|
|
30
|
+
def test_read_file_with_masking(tmp_path):
|
|
31
|
+
f = tmp_path / "secret.env"
|
|
32
|
+
f.write_text("TOKEN=super-secret")
|
|
33
|
+
content = read_file(f, mask_secrets=True)
|
|
34
|
+
assert "super-secret" not in content
|
|
35
|
+
assert "TOKEN=\"***\"" in content
|