reposnap 0.4.0__tar.gz → 0.5.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.
- {reposnap-0.4.0 → reposnap-0.5.0}/PKG-INFO +30 -4
- {reposnap-0.4.0 → reposnap-0.5.0}/README.md +29 -3
- {reposnap-0.4.0 → reposnap-0.5.0}/pyproject.toml +1 -1
- {reposnap-0.4.0 → reposnap-0.5.0}/src/reposnap/controllers/project_controller.py +26 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/src/reposnap/interfaces/cli.py +7 -0
- reposnap-0.5.0/tests/reposnap/test_project_controller.py +306 -0
- reposnap-0.4.0/tests/reposnap/test_project_controller.py +0 -97
- {reposnap-0.4.0 → reposnap-0.5.0}/.gitignore +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/.python-version +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/LICENSE +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/requirements-dev.lock +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/requirements.lock +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/src/reposnap/__init__.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/src/reposnap/controllers/__init__.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/src/reposnap/core/__init__.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/src/reposnap/core/file_system.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/src/reposnap/core/git_repo.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/src/reposnap/core/markdown_generator.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/src/reposnap/interfaces/__init__.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/src/reposnap/interfaces/gui.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/src/reposnap/models/__init__.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/src/reposnap/models/file_tree.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/src/reposnap/utils/__init__.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/src/reposnap/utils/path_utils.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/tests/__init__.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/tests/reposnap/__init__.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/tests/reposnap/test_cli.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/tests/reposnap/test_file_system.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/tests/reposnap/test_file_tree.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/tests/reposnap/test_git_repo.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/tests/reposnap/test_gui.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/tests/reposnap/test_markdown_generator.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/tests/reposnap/test_path_utils.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/tests/resources/another_existing_file.py +0 -0
- {reposnap-0.4.0 → reposnap-0.5.0}/tests/resources/existing_file.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: reposnap
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.5.0
|
4
4
|
Summary: Generate a Markdown file with all contents of your project
|
5
5
|
Author: agoloborodko
|
6
6
|
License-File: LICENSE
|
@@ -24,6 +24,7 @@ Description-Content-Type: text/markdown
|
|
24
24
|
- **Syntax Highlighting**: Includes syntax highlighting for known file types in the generated Markdown file.
|
25
25
|
- **Structure Only Option**: The `--structure-only` flag can be used to generate the Markdown file with just the directory structure, omitting the contents of the files.
|
26
26
|
- **Gitignore Support**: Automatically respects `.gitignore` patterns to exclude files and directories.
|
27
|
+
- **Include and Exclude Patterns**: Use `--include` and `--exclude` to specify patterns for files and directories to include or exclude.
|
27
28
|
|
28
29
|
## Installation
|
29
30
|
|
@@ -48,7 +49,7 @@ pip install -r requirements.txt
|
|
48
49
|
To use `reposnap` from the command line, run it with the following options:
|
49
50
|
|
50
51
|
```bash
|
51
|
-
reposnap [-h] [-o OUTPUT] [--structure-only] [--debug] path
|
52
|
+
reposnap [-h] [-o OUTPUT] [--structure-only] [--debug] [-i INCLUDE [INCLUDE ...]] [-e EXCLUDE [EXCLUDE ...]] path
|
52
53
|
```
|
53
54
|
|
54
55
|
- `path`: Path to the Git repository or subdirectory.
|
@@ -56,6 +57,19 @@ reposnap [-h] [-o OUTPUT] [--structure-only] [--debug] path
|
|
56
57
|
- `-o, --output`: The name of the output Markdown file. Defaults to `output.md`.
|
57
58
|
- `--structure-only`: Generate a Markdown file that includes only the project structure, without file contents.
|
58
59
|
- `--debug`: Enable debug-level logging.
|
60
|
+
- `-i, --include`: File/folder patterns to include. For example, `-i "*.py"` includes only Python files.
|
61
|
+
- `-e, --exclude`: File/folder patterns to exclude. For example, `-e "*.md"` excludes all Markdown files.
|
62
|
+
|
63
|
+
#### Pattern Matching
|
64
|
+
|
65
|
+
- **Pattern Interpretation**: Patterns follow gitignore-style syntax but with a twist.
|
66
|
+
- **Patterns without Wildcards**: If a pattern does not contain any wildcard characters (`*`, `?`, or `[`), it is treated as `*pattern*`. This means it will match any file or directory containing `pattern` in its name.
|
67
|
+
- **Patterns with Wildcards**: If a pattern contains wildcard characters, it retains its original behavior.
|
68
|
+
|
69
|
+
- **Examples**:
|
70
|
+
- `-e "gui"`: Excludes any files or directories containing `"gui"` in their names.
|
71
|
+
- `-i "*.py"`: Includes only files ending with `.py`.
|
72
|
+
- `-e "*.test.*"`: Excludes files with `.test.` in their names.
|
59
73
|
|
60
74
|
#### Examples
|
61
75
|
|
@@ -71,10 +85,22 @@ reposnap [-h] [-o OUTPUT] [--structure-only] [--debug] path
|
|
71
85
|
reposnap my_project/ --structure-only
|
72
86
|
```
|
73
87
|
|
74
|
-
3. **Generate a Markdown file
|
88
|
+
3. **Generate a Markdown file including only Python files**:
|
89
|
+
|
90
|
+
```bash
|
91
|
+
reposnap my_project/ -i "*.py"
|
92
|
+
```
|
93
|
+
|
94
|
+
4. **Generate a Markdown file excluding certain files and directories**:
|
95
|
+
|
96
|
+
```bash
|
97
|
+
reposnap my_project/ -e "tests" -e "*.md"
|
98
|
+
```
|
99
|
+
|
100
|
+
5. **Exclude files and directories containing a substring**:
|
75
101
|
|
76
102
|
```bash
|
77
|
-
reposnap my_project/ -
|
103
|
+
reposnap my_project/ -e "gui"
|
78
104
|
```
|
79
105
|
|
80
106
|
### Graphical User Interface
|
@@ -11,6 +11,7 @@
|
|
11
11
|
- **Syntax Highlighting**: Includes syntax highlighting for known file types in the generated Markdown file.
|
12
12
|
- **Structure Only Option**: The `--structure-only` flag can be used to generate the Markdown file with just the directory structure, omitting the contents of the files.
|
13
13
|
- **Gitignore Support**: Automatically respects `.gitignore` patterns to exclude files and directories.
|
14
|
+
- **Include and Exclude Patterns**: Use `--include` and `--exclude` to specify patterns for files and directories to include or exclude.
|
14
15
|
|
15
16
|
## Installation
|
16
17
|
|
@@ -35,7 +36,7 @@ pip install -r requirements.txt
|
|
35
36
|
To use `reposnap` from the command line, run it with the following options:
|
36
37
|
|
37
38
|
```bash
|
38
|
-
reposnap [-h] [-o OUTPUT] [--structure-only] [--debug] path
|
39
|
+
reposnap [-h] [-o OUTPUT] [--structure-only] [--debug] [-i INCLUDE [INCLUDE ...]] [-e EXCLUDE [EXCLUDE ...]] path
|
39
40
|
```
|
40
41
|
|
41
42
|
- `path`: Path to the Git repository or subdirectory.
|
@@ -43,6 +44,19 @@ reposnap [-h] [-o OUTPUT] [--structure-only] [--debug] path
|
|
43
44
|
- `-o, --output`: The name of the output Markdown file. Defaults to `output.md`.
|
44
45
|
- `--structure-only`: Generate a Markdown file that includes only the project structure, without file contents.
|
45
46
|
- `--debug`: Enable debug-level logging.
|
47
|
+
- `-i, --include`: File/folder patterns to include. For example, `-i "*.py"` includes only Python files.
|
48
|
+
- `-e, --exclude`: File/folder patterns to exclude. For example, `-e "*.md"` excludes all Markdown files.
|
49
|
+
|
50
|
+
#### Pattern Matching
|
51
|
+
|
52
|
+
- **Pattern Interpretation**: Patterns follow gitignore-style syntax but with a twist.
|
53
|
+
- **Patterns without Wildcards**: If a pattern does not contain any wildcard characters (`*`, `?`, or `[`), it is treated as `*pattern*`. This means it will match any file or directory containing `pattern` in its name.
|
54
|
+
- **Patterns with Wildcards**: If a pattern contains wildcard characters, it retains its original behavior.
|
55
|
+
|
56
|
+
- **Examples**:
|
57
|
+
- `-e "gui"`: Excludes any files or directories containing `"gui"` in their names.
|
58
|
+
- `-i "*.py"`: Includes only files ending with `.py`.
|
59
|
+
- `-e "*.test.*"`: Excludes files with `.test.` in their names.
|
46
60
|
|
47
61
|
#### Examples
|
48
62
|
|
@@ -58,10 +72,22 @@ reposnap [-h] [-o OUTPUT] [--structure-only] [--debug] path
|
|
58
72
|
reposnap my_project/ --structure-only
|
59
73
|
```
|
60
74
|
|
61
|
-
3. **Generate a Markdown file
|
75
|
+
3. **Generate a Markdown file including only Python files**:
|
76
|
+
|
77
|
+
```bash
|
78
|
+
reposnap my_project/ -i "*.py"
|
79
|
+
```
|
80
|
+
|
81
|
+
4. **Generate a Markdown file excluding certain files and directories**:
|
82
|
+
|
83
|
+
```bash
|
84
|
+
reposnap my_project/ -e "tests" -e "*.md"
|
85
|
+
```
|
86
|
+
|
87
|
+
5. **Exclude files and directories containing a substring**:
|
62
88
|
|
63
89
|
```bash
|
64
|
-
reposnap my_project/ -
|
90
|
+
reposnap my_project/ -e "gui"
|
65
91
|
```
|
66
92
|
|
67
93
|
### Graphical User Interface
|
@@ -19,6 +19,8 @@ class ProjectController:
|
|
19
19
|
self.args: object = args
|
20
20
|
self.file_tree: Optional[FileTree] = None
|
21
21
|
self.gitignore_patterns: List[str] = []
|
22
|
+
self.include_patterns: List[str] = args.include if args and hasattr(args, 'include') else []
|
23
|
+
self.exclude_patterns: List[str] = args.exclude if args and hasattr(args, 'exclude') else []
|
22
24
|
if self.root_dir:
|
23
25
|
self.gitignore_patterns = self._load_gitignore_patterns()
|
24
26
|
|
@@ -40,6 +42,30 @@ class ProjectController:
|
|
40
42
|
git_files: List[Path] = git_repo.get_git_files()
|
41
43
|
self.logger.debug(f"Git files before filtering: {git_files}")
|
42
44
|
|
45
|
+
# Adjust patterns
|
46
|
+
def adjust_patterns(patterns):
|
47
|
+
adjusted = []
|
48
|
+
for pattern in patterns:
|
49
|
+
if '*' in pattern or '?' in pattern or '[' in pattern:
|
50
|
+
adjusted.append(pattern)
|
51
|
+
else:
|
52
|
+
adjusted.append(f'*{pattern}*')
|
53
|
+
return adjusted
|
54
|
+
|
55
|
+
# Apply include patterns
|
56
|
+
if self.include_patterns:
|
57
|
+
adjusted_include_patterns = adjust_patterns(self.include_patterns)
|
58
|
+
include_spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, adjusted_include_patterns)
|
59
|
+
git_files = [f for f in git_files if include_spec.match_file(f.as_posix())]
|
60
|
+
self.logger.debug(f"Git files after include patterns: {git_files}")
|
61
|
+
|
62
|
+
# Apply exclude patterns
|
63
|
+
if self.exclude_patterns:
|
64
|
+
adjusted_exclude_patterns = adjust_patterns(self.exclude_patterns)
|
65
|
+
exclude_spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, adjusted_exclude_patterns)
|
66
|
+
git_files = [f for f in git_files if not exclude_spec.match_file(f.as_posix())]
|
67
|
+
self.logger.debug(f"Git files after exclude patterns: {git_files}")
|
68
|
+
|
43
69
|
self.logger.info("Building tree structure.")
|
44
70
|
file_system: FileSystem = FileSystem(self.root_dir)
|
45
71
|
tree_structure: dict = file_system.build_tree_structure(git_files)
|
@@ -13,6 +13,12 @@ def main():
|
|
13
13
|
help='Only include the file structure without content.')
|
14
14
|
parser.add_argument('--debug', action='store_true', help='Enable debug-level logging.')
|
15
15
|
|
16
|
+
# New arguments for include and exclude patterns
|
17
|
+
parser.add_argument('-i', '--include', nargs='*', default=[],
|
18
|
+
help='File/folder patterns to include.')
|
19
|
+
parser.add_argument('-e', '--exclude', nargs='*', default=[],
|
20
|
+
help='File/folder patterns to exclude.')
|
21
|
+
|
16
22
|
args = parser.parse_args()
|
17
23
|
|
18
24
|
log_level = logging.DEBUG if args.debug else logging.INFO
|
@@ -21,5 +27,6 @@ def main():
|
|
21
27
|
controller = ProjectController(args)
|
22
28
|
controller.run()
|
23
29
|
|
30
|
+
|
24
31
|
if __name__ == "__main__":
|
25
32
|
main()
|
@@ -0,0 +1,306 @@
|
|
1
|
+
# tests/reposnap/test_project_controller.py
|
2
|
+
|
3
|
+
import pytest
|
4
|
+
import tempfile
|
5
|
+
import os
|
6
|
+
from pathlib import Path
|
7
|
+
from unittest.mock import patch, MagicMock
|
8
|
+
from reposnap.controllers.project_controller import ProjectController
|
9
|
+
|
10
|
+
|
11
|
+
def create_file(file_path: str, content: str = ''):
|
12
|
+
with open(file_path, 'w') as f:
|
13
|
+
f.write(content)
|
14
|
+
|
15
|
+
|
16
|
+
def create_directory_structure(base_dir: str, structure: dict):
|
17
|
+
for name, content in structure.items():
|
18
|
+
path = os.path.join(base_dir, name)
|
19
|
+
if isinstance(content, dict):
|
20
|
+
os.makedirs(path, exist_ok=True)
|
21
|
+
create_directory_structure(path, content)
|
22
|
+
else:
|
23
|
+
create_file(path, content)
|
24
|
+
|
25
|
+
|
26
|
+
def test_project_controller_includes_py_files():
|
27
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
28
|
+
gitignore_content = """
|
29
|
+
*.py[oc]
|
30
|
+
"""
|
31
|
+
structure = {
|
32
|
+
'src': {
|
33
|
+
'module': {
|
34
|
+
'file1.py': 'print("File 1")',
|
35
|
+
'file2.py': 'print("File 2")',
|
36
|
+
'file3.pyc': 'Compiled code',
|
37
|
+
}
|
38
|
+
},
|
39
|
+
'.gitignore': gitignore_content,
|
40
|
+
}
|
41
|
+
|
42
|
+
create_directory_structure(temp_dir, structure)
|
43
|
+
|
44
|
+
args = type('Args', (object,), {
|
45
|
+
'path': temp_dir,
|
46
|
+
'output': os.path.join(temp_dir, 'output.md'),
|
47
|
+
'structure_only': False,
|
48
|
+
'debug': False
|
49
|
+
})
|
50
|
+
|
51
|
+
with patch('reposnap.controllers.project_controller.GitRepo') as MockGitRepo:
|
52
|
+
mock_git_repo_instance = MockGitRepo.return_value
|
53
|
+
mock_git_repo_instance.get_git_files.return_value = [
|
54
|
+
Path('src/module/file1.py'),
|
55
|
+
Path('src/module/file2.py'),
|
56
|
+
Path('.gitignore')
|
57
|
+
]
|
58
|
+
|
59
|
+
controller = ProjectController(args)
|
60
|
+
controller.run()
|
61
|
+
|
62
|
+
# Read the output file
|
63
|
+
with open(args.output, 'r') as f:
|
64
|
+
output_content = f.read()
|
65
|
+
|
66
|
+
# Check that contents of .py files are included
|
67
|
+
assert 'print("File 1")' in output_content
|
68
|
+
assert 'print("File 2")' in output_content
|
69
|
+
# .pyc files should be ignored
|
70
|
+
assert 'Compiled code' not in output_content
|
71
|
+
|
72
|
+
|
73
|
+
@patch('reposnap.controllers.project_controller.MarkdownGenerator')
|
74
|
+
@patch('reposnap.controllers.project_controller.FileSystem')
|
75
|
+
@patch('reposnap.controllers.project_controller.GitRepo')
|
76
|
+
def test_project_controller_run(mock_git_repo, mock_file_system, mock_markdown_generator):
|
77
|
+
# Setup mocks
|
78
|
+
mock_git_repo_instance = MagicMock()
|
79
|
+
mock_file_system_instance = MagicMock()
|
80
|
+
mock_markdown_generator_instance = MagicMock()
|
81
|
+
|
82
|
+
mock_git_repo.return_value = mock_git_repo_instance
|
83
|
+
mock_file_system.return_value = mock_file_system_instance
|
84
|
+
mock_markdown_generator.return_value = mock_markdown_generator_instance
|
85
|
+
|
86
|
+
# Use Path objects instead of strings
|
87
|
+
mock_git_repo_instance.get_git_files.return_value = [Path('file1.py'), Path('file2.py')]
|
88
|
+
mock_file_system_instance.build_tree_structure.return_value = {'dir': {'file1.py': 'file1.py'}}
|
89
|
+
|
90
|
+
args = MagicMock()
|
91
|
+
args.path = 'root_dir'
|
92
|
+
args.output = 'output.md'
|
93
|
+
args.structure_only = False
|
94
|
+
args.include = []
|
95
|
+
args.exclude = []
|
96
|
+
args.debug = False # Add if necessary
|
97
|
+
|
98
|
+
controller = ProjectController(args)
|
99
|
+
controller.run()
|
100
|
+
|
101
|
+
mock_markdown_generator_instance.generate_markdown.assert_called_once()
|
102
|
+
|
103
|
+
|
104
|
+
def test_include_pattern():
|
105
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
106
|
+
structure = {
|
107
|
+
'src': {
|
108
|
+
'module': {
|
109
|
+
'file1.py': 'print("File 1")',
|
110
|
+
'file2.txt': 'File 2 content',
|
111
|
+
'submodule': {
|
112
|
+
'file3.py': 'print("File 3")',
|
113
|
+
'file4.md': '# File 4',
|
114
|
+
}
|
115
|
+
}
|
116
|
+
},
|
117
|
+
'README.md': '# Project README',
|
118
|
+
'setup.py': 'setup code',
|
119
|
+
'notes.txt': 'Some notes',
|
120
|
+
}
|
121
|
+
|
122
|
+
create_directory_structure(temp_dir, structure)
|
123
|
+
|
124
|
+
args = type('Args', (object,), {
|
125
|
+
'path': temp_dir,
|
126
|
+
'output': os.path.join(temp_dir, 'output.md'),
|
127
|
+
'structure_only': False,
|
128
|
+
'debug': False,
|
129
|
+
'include': ['*.py'],
|
130
|
+
'exclude': []
|
131
|
+
})
|
132
|
+
|
133
|
+
# Mock the GitRepo class
|
134
|
+
with patch('reposnap.controllers.project_controller.GitRepo') as MockGitRepo:
|
135
|
+
mock_git_repo_instance = MockGitRepo.return_value
|
136
|
+
|
137
|
+
# Collect all files under temp_dir
|
138
|
+
all_files = []
|
139
|
+
for root, dirs, files in os.walk(temp_dir):
|
140
|
+
for name in files:
|
141
|
+
file_path = Path(root) / name
|
142
|
+
rel_path = file_path.relative_to(temp_dir)
|
143
|
+
all_files.append(rel_path)
|
144
|
+
|
145
|
+
mock_git_repo_instance.get_git_files.return_value = all_files
|
146
|
+
|
147
|
+
controller = ProjectController(args)
|
148
|
+
controller.collect_file_tree()
|
149
|
+
|
150
|
+
# Get the list of files included in the tree
|
151
|
+
included_files = []
|
152
|
+
|
153
|
+
def traverse(tree, path=''):
|
154
|
+
for name, node in tree.items():
|
155
|
+
current_path = os.path.join(path, name)
|
156
|
+
if isinstance(node, dict):
|
157
|
+
traverse(node, current_path)
|
158
|
+
else:
|
159
|
+
included_files.append(current_path)
|
160
|
+
|
161
|
+
traverse(controller.file_tree.structure)
|
162
|
+
|
163
|
+
expected_files = [
|
164
|
+
os.path.join('src', 'module', 'file1.py'),
|
165
|
+
os.path.join('src', 'module', 'submodule', 'file3.py'),
|
166
|
+
'setup.py',
|
167
|
+
]
|
168
|
+
|
169
|
+
assert sorted(included_files) == sorted(expected_files)
|
170
|
+
|
171
|
+
|
172
|
+
def test_exclude_pattern():
|
173
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
174
|
+
structure = {
|
175
|
+
'src': {
|
176
|
+
'module': {
|
177
|
+
'file1.py': 'print("File 1")',
|
178
|
+
'file2.txt': 'File 2 content',
|
179
|
+
'submodule': {
|
180
|
+
'file3.py': 'print("File 3")',
|
181
|
+
'file4.md': '# File 4',
|
182
|
+
}
|
183
|
+
}
|
184
|
+
},
|
185
|
+
'README.md': '# Project README',
|
186
|
+
'setup.py': 'setup code',
|
187
|
+
'notes.txt': 'Some notes',
|
188
|
+
}
|
189
|
+
|
190
|
+
create_directory_structure(temp_dir, structure)
|
191
|
+
|
192
|
+
args = type('Args', (object,), {
|
193
|
+
'path': temp_dir,
|
194
|
+
'output': os.path.join(temp_dir, 'output.md'),
|
195
|
+
'structure_only': False,
|
196
|
+
'debug': False,
|
197
|
+
'include': [],
|
198
|
+
'exclude': ['*.md', '*.txt']
|
199
|
+
})
|
200
|
+
|
201
|
+
with patch('reposnap.controllers.project_controller.GitRepo') as MockGitRepo:
|
202
|
+
mock_git_repo_instance = MockGitRepo.return_value
|
203
|
+
|
204
|
+
# Collect all files under temp_dir
|
205
|
+
all_files = []
|
206
|
+
for root, dirs, files in os.walk(temp_dir):
|
207
|
+
for name in files:
|
208
|
+
file_path = Path(root) / name
|
209
|
+
rel_path = file_path.relative_to(temp_dir)
|
210
|
+
all_files.append(rel_path)
|
211
|
+
|
212
|
+
mock_git_repo_instance.get_git_files.return_value = all_files
|
213
|
+
|
214
|
+
controller = ProjectController(args)
|
215
|
+
controller.collect_file_tree()
|
216
|
+
|
217
|
+
included_files = []
|
218
|
+
|
219
|
+
def traverse(tree, path=''):
|
220
|
+
for name, node in tree.items():
|
221
|
+
current_path = os.path.join(path, name)
|
222
|
+
if isinstance(node, dict):
|
223
|
+
traverse(node, current_path)
|
224
|
+
else:
|
225
|
+
included_files.append(current_path)
|
226
|
+
|
227
|
+
traverse(controller.file_tree.structure)
|
228
|
+
|
229
|
+
expected_files = [
|
230
|
+
os.path.join('src', 'module', 'file1.py'),
|
231
|
+
os.path.join('src', 'module', 'submodule', 'file3.py'),
|
232
|
+
'setup.py',
|
233
|
+
]
|
234
|
+
|
235
|
+
assert sorted(included_files) == sorted(expected_files)
|
236
|
+
|
237
|
+
|
238
|
+
def test_include_and_exclude_patterns():
|
239
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
240
|
+
structure = {
|
241
|
+
'src': {
|
242
|
+
'foo_module': {
|
243
|
+
'foo_file1.py': 'print("Foo File 1")',
|
244
|
+
'file2.py': 'print("File 2")',
|
245
|
+
'submodule': {
|
246
|
+
'foo_file3.py': 'print("Foo File 3")',
|
247
|
+
'file4.py': 'print("File 4")',
|
248
|
+
}
|
249
|
+
},
|
250
|
+
'bar_module': {
|
251
|
+
'bar_file1.py': 'print("Bar File 1")',
|
252
|
+
}
|
253
|
+
},
|
254
|
+
'README.md': '# Project README',
|
255
|
+
'setup.py': 'setup code',
|
256
|
+
'notes.txt': 'Some notes',
|
257
|
+
}
|
258
|
+
|
259
|
+
create_directory_structure(temp_dir, structure)
|
260
|
+
|
261
|
+
args = type('Args', (object,), {
|
262
|
+
'path': temp_dir,
|
263
|
+
'output': os.path.join(temp_dir, 'output.md'),
|
264
|
+
'structure_only': False,
|
265
|
+
'debug': False,
|
266
|
+
'include': ['*foo*'],
|
267
|
+
'exclude': ['*submodule*']
|
268
|
+
})
|
269
|
+
|
270
|
+
with patch('reposnap.controllers.project_controller.GitRepo') as MockGitRepo:
|
271
|
+
mock_git_repo_instance = MockGitRepo.return_value
|
272
|
+
|
273
|
+
# Collect all files under temp_dir
|
274
|
+
all_files = []
|
275
|
+
for root, dirs, files in os.walk(temp_dir):
|
276
|
+
for name in files:
|
277
|
+
file_path = Path(root) / name
|
278
|
+
rel_path = file_path.relative_to(temp_dir)
|
279
|
+
all_files.append(rel_path)
|
280
|
+
|
281
|
+
mock_git_repo_instance.get_git_files.return_value = all_files
|
282
|
+
|
283
|
+
controller = ProjectController(args)
|
284
|
+
controller.collect_file_tree()
|
285
|
+
|
286
|
+
included_files = []
|
287
|
+
|
288
|
+
def traverse(tree, path=''):
|
289
|
+
for name, node in tree.items():
|
290
|
+
current_path = os.path.join(path, name)
|
291
|
+
if isinstance(node, dict):
|
292
|
+
included_files.append(current_path)
|
293
|
+
traverse(node, current_path)
|
294
|
+
else:
|
295
|
+
included_files.append(current_path)
|
296
|
+
|
297
|
+
traverse(controller.file_tree.structure)
|
298
|
+
|
299
|
+
expected_files = [
|
300
|
+
os.path.join('src'),
|
301
|
+
os.path.join('src', 'foo_module'),
|
302
|
+
os.path.join('src', 'foo_module', 'foo_file1.py'),
|
303
|
+
os.path.join('src', 'foo_module', 'file2.py'), # Include this file
|
304
|
+
]
|
305
|
+
|
306
|
+
assert sorted(included_files) == sorted(expected_files)
|
@@ -1,97 +0,0 @@
|
|
1
|
-
# tests/reposnap/test_project_controller.py
|
2
|
-
|
3
|
-
import pytest
|
4
|
-
import tempfile
|
5
|
-
import os
|
6
|
-
from pathlib import Path
|
7
|
-
from unittest.mock import patch, MagicMock
|
8
|
-
from reposnap.controllers.project_controller import ProjectController
|
9
|
-
|
10
|
-
|
11
|
-
def create_file(file_path: str, content: str = ''):
|
12
|
-
with open(file_path, 'w') as f:
|
13
|
-
f.write(content)
|
14
|
-
|
15
|
-
|
16
|
-
def create_directory_structure(base_dir: str, structure: dict):
|
17
|
-
for name, content in structure.items():
|
18
|
-
path = os.path.join(base_dir, name)
|
19
|
-
if isinstance(content, dict):
|
20
|
-
os.makedirs(path, exist_ok=True)
|
21
|
-
create_directory_structure(path, content)
|
22
|
-
else:
|
23
|
-
create_file(path, content)
|
24
|
-
|
25
|
-
|
26
|
-
def test_project_controller_includes_py_files():
|
27
|
-
with tempfile.TemporaryDirectory() as temp_dir:
|
28
|
-
gitignore_content = """
|
29
|
-
*.py[oc]
|
30
|
-
"""
|
31
|
-
structure = {
|
32
|
-
'src': {
|
33
|
-
'module': {
|
34
|
-
'file1.py': 'print("File 1")',
|
35
|
-
'file2.py': 'print("File 2")',
|
36
|
-
'file3.pyc': 'Compiled code',
|
37
|
-
}
|
38
|
-
},
|
39
|
-
'.gitignore': gitignore_content,
|
40
|
-
}
|
41
|
-
|
42
|
-
create_directory_structure(temp_dir, structure)
|
43
|
-
|
44
|
-
args = type('Args', (object,), {
|
45
|
-
'path': temp_dir,
|
46
|
-
'output': os.path.join(temp_dir, 'output.md'),
|
47
|
-
'structure_only': False,
|
48
|
-
'debug': False
|
49
|
-
})
|
50
|
-
|
51
|
-
with patch('reposnap.controllers.project_controller.GitRepo') as MockGitRepo:
|
52
|
-
mock_git_repo_instance = MockGitRepo.return_value
|
53
|
-
mock_git_repo_instance.get_git_files.return_value = [
|
54
|
-
Path('src/module/file1.py'),
|
55
|
-
Path('src/module/file2.py'),
|
56
|
-
Path('.gitignore')
|
57
|
-
]
|
58
|
-
|
59
|
-
controller = ProjectController(args)
|
60
|
-
controller.run()
|
61
|
-
|
62
|
-
# Read the output file
|
63
|
-
with open(args.output, 'r') as f:
|
64
|
-
output_content = f.read()
|
65
|
-
|
66
|
-
# Check that contents of .py files are included
|
67
|
-
assert 'print("File 1")' in output_content
|
68
|
-
assert 'print("File 2")' in output_content
|
69
|
-
# .pyc files should be ignored
|
70
|
-
assert 'Compiled code' not in output_content
|
71
|
-
|
72
|
-
|
73
|
-
@patch('reposnap.controllers.project_controller.MarkdownGenerator')
|
74
|
-
@patch('reposnap.controllers.project_controller.FileSystem')
|
75
|
-
@patch('reposnap.controllers.project_controller.GitRepo')
|
76
|
-
def test_project_controller_run(mock_git_repo, mock_file_system, mock_markdown_generator):
|
77
|
-
# Setup mocks
|
78
|
-
mock_git_repo_instance = MagicMock()
|
79
|
-
mock_file_system_instance = MagicMock()
|
80
|
-
mock_markdown_generator_instance = MagicMock()
|
81
|
-
|
82
|
-
mock_git_repo.return_value = mock_git_repo_instance
|
83
|
-
mock_file_system.return_value = mock_file_system_instance
|
84
|
-
mock_markdown_generator.return_value = mock_markdown_generator_instance
|
85
|
-
|
86
|
-
mock_git_repo_instance.get_git_files.return_value = ['file1.py', 'file2.py']
|
87
|
-
mock_file_system_instance.build_tree_structure.return_value = {'dir': {'file1.py': 'file1.py'}}
|
88
|
-
|
89
|
-
args = MagicMock()
|
90
|
-
args.path = 'root_dir'
|
91
|
-
args.output = 'output.md'
|
92
|
-
args.structure_only = False
|
93
|
-
|
94
|
-
controller = ProjectController(args)
|
95
|
-
controller.run()
|
96
|
-
|
97
|
-
mock_markdown_generator_instance.generate_markdown.assert_called_once()
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|