reposnap 0.6.3__py3-none-any.whl → 0.6.5__py3-none-any.whl

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.
@@ -5,6 +5,7 @@ from reposnap.models.file_tree import FileTree
5
5
  import pathspec
6
6
  from typing import List, Optional
7
7
 
8
+
8
9
  class ProjectController:
9
10
  def __init__(self, args: Optional[object] = None):
10
11
  self.logger = logging.getLogger(__name__)
@@ -13,27 +14,43 @@ class ProjectController:
13
14
  if args:
14
15
  self.args = args
15
16
  # Treat positional arguments as literal file/directory names.
16
- input_paths = [Path(p) for p in (args.paths if hasattr(args, 'paths') else [args.path])]
17
+ input_paths = [
18
+ Path(p) for p in (args.paths if hasattr(args, "paths") else [args.path])
19
+ ]
17
20
  self.input_paths = []
18
21
  for p in input_paths:
19
22
  candidate = (self.root_dir / p).resolve()
20
23
  if candidate.exists():
21
24
  try:
22
25
  rel = candidate.relative_to(self.root_dir)
23
- if rel != Path('.'):
26
+ if rel != Path("."):
24
27
  self.input_paths.append(rel)
25
28
  except ValueError:
26
- self.logger.warning(f"Path {p} is not under repository root {self.root_dir}. Ignoring.")
29
+ self.logger.warning(
30
+ f"Path {p} is not under repository root {self.root_dir}. Ignoring."
31
+ )
27
32
  else:
28
- self.logger.warning(f"Path {p} does not exist relative to repository root {self.root_dir}.")
29
- self.output_file: Path = Path(args.output).resolve() if args.output else self.root_dir / 'output.md'
30
- self.structure_only: bool = args.structure_only if hasattr(args, 'structure_only') else False
31
- self.include_patterns: List[str] = args.include if hasattr(args, 'include') else []
32
- self.exclude_patterns: List[str] = args.exclude if hasattr(args, 'exclude') else []
33
+ self.logger.warning(
34
+ f"Path {p} does not exist relative to repository root {self.root_dir}."
35
+ )
36
+ self.output_file: Path = (
37
+ Path(args.output).resolve()
38
+ if args.output
39
+ else self.root_dir / "output.md"
40
+ )
41
+ self.structure_only: bool = (
42
+ args.structure_only if hasattr(args, "structure_only") else False
43
+ )
44
+ self.include_patterns: List[str] = (
45
+ args.include if hasattr(args, "include") else []
46
+ )
47
+ self.exclude_patterns: List[str] = (
48
+ args.exclude if hasattr(args, "exclude") else []
49
+ )
33
50
  else:
34
51
  self.args = None
35
52
  self.input_paths = []
36
- self.output_file = self.root_dir / 'output.md'
53
+ self.output_file = self.root_dir / "output.md"
37
54
  self.structure_only = False
38
55
  self.include_patterns = []
39
56
  self.exclude_patterns = []
@@ -48,11 +65,14 @@ class ProjectController:
48
65
  otherwise use the current directory.
49
66
  """
50
67
  from git import Repo, InvalidGitRepositoryError
68
+
51
69
  try:
52
70
  repo = Repo(Path.cwd(), search_parent_directories=True)
53
71
  return Path(repo.working_tree_dir).resolve()
54
72
  except InvalidGitRepositoryError:
55
- self.logger.warning("Not a git repository. Using current directory as root.")
73
+ self.logger.warning(
74
+ "Not a git repository. Using current directory as root."
75
+ )
56
76
  return Path.cwd().resolve()
57
77
 
58
78
  def set_root_dir(self, root_dir: Path) -> None:
@@ -64,21 +84,27 @@ class ProjectController:
64
84
 
65
85
  def _apply_include_exclude(self, files: List[Path]) -> List[Path]:
66
86
  """Filter a list of file paths using include and exclude patterns."""
87
+
67
88
  def adjust_patterns(patterns):
68
89
  adjusted = []
69
90
  for p in patterns:
70
- if any(ch in p for ch in ['*', '?', '[']):
91
+ if any(ch in p for ch in ["*", "?", "["]):
71
92
  adjusted.append(p)
72
93
  else:
73
- adjusted.append(f'*{p}*')
94
+ adjusted.append(f"*{p}*")
74
95
  return adjusted
96
+
75
97
  if self.include_patterns:
76
98
  inc = adjust_patterns(self.include_patterns)
77
- spec_inc = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, inc)
99
+ spec_inc = pathspec.PathSpec.from_lines(
100
+ pathspec.patterns.GitWildMatchPattern, inc
101
+ )
78
102
  files = [f for f in files if spec_inc.match_file(f.as_posix())]
79
103
  if self.exclude_patterns:
80
104
  exc = adjust_patterns(self.exclude_patterns)
81
- spec_exc = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, exc)
105
+ spec_exc = pathspec.PathSpec.from_lines(
106
+ pathspec.patterns.GitWildMatchPattern, exc
107
+ )
82
108
  files = [f for f in files if not spec_exc.match_file(f.as_posix())]
83
109
  return files
84
110
 
@@ -86,6 +112,7 @@ class ProjectController:
86
112
  self.logger.info("Collecting files from Git tracked files if available.")
87
113
  try:
88
114
  from reposnap.core.git_repo import GitRepo
115
+
89
116
  git_repo = GitRepo(self.root_dir)
90
117
  all_files = git_repo.get_git_files()
91
118
  self.logger.debug(f"Git tracked files: {all_files}")
@@ -96,7 +123,9 @@ class ProjectController:
96
123
  if not all_files:
97
124
  file_list = [p for p in self.root_dir.rglob("*") if p.is_file()]
98
125
  if file_list:
99
- self.logger.info("Git tracked files empty, using filesystem scan fallback.")
126
+ self.logger.info(
127
+ "Git tracked files empty, using filesystem scan fallback."
128
+ )
100
129
  all_files = []
101
130
  for path in file_list:
102
131
  try:
@@ -109,7 +138,12 @@ class ProjectController:
109
138
  if self.input_paths:
110
139
  trees = []
111
140
  for input_path in self.input_paths:
112
- subset = [f for f in all_files if f == input_path or list(f.parts[:len(input_path.parts)]) == list(input_path.parts)]
141
+ subset = [
142
+ f
143
+ for f in all_files
144
+ if f == input_path
145
+ or list(f.parts[: len(input_path.parts)]) == list(input_path.parts)
146
+ ]
113
147
  self.logger.debug(f"Files for input path '{input_path}': {subset}")
114
148
  if subset:
115
149
  tree = FileSystem(self.root_dir).build_tree_structure(subset)
@@ -145,32 +179,40 @@ class ProjectController:
145
179
 
146
180
  def apply_filters(self) -> None:
147
181
  self.logger.info("Applying .gitignore filters to the merged tree.")
148
- spec = pathspec.PathSpec.from_lines(pathspec.patterns.GitWildMatchPattern, self.gitignore_patterns)
182
+ spec = pathspec.PathSpec.from_lines(
183
+ pathspec.patterns.GitWildMatchPattern, self.gitignore_patterns
184
+ )
149
185
  self.logger.debug(f".gitignore patterns: {self.gitignore_patterns}")
150
186
  self.file_tree.filter_files(spec)
151
187
 
152
188
  def generate_output(self) -> None:
153
189
  self.logger.info("Starting Markdown generation.")
154
190
  from reposnap.core.markdown_generator import MarkdownGenerator
191
+
155
192
  markdown_generator = MarkdownGenerator(
156
193
  root_dir=self.root_dir,
157
194
  output_file=self.output_file,
158
- structure_only=self.structure_only
195
+ structure_only=self.structure_only,
196
+ )
197
+ markdown_generator.generate_markdown(
198
+ self.file_tree.structure, self.file_tree.get_all_files()
159
199
  )
160
- markdown_generator.generate_markdown(self.file_tree.structure, self.file_tree.get_all_files())
161
200
  self.logger.info(f"Markdown generated at {self.output_file}.")
162
201
 
163
202
  def generate_output_from_selected(self, selected_files: set) -> None:
164
203
  self.logger.info("Generating Markdown from selected files.")
165
204
  pruned_tree = self.file_tree.prune_tree(selected_files)
166
205
  from reposnap.core.markdown_generator import MarkdownGenerator
206
+
167
207
  markdown_generator = MarkdownGenerator(
168
208
  root_dir=self.root_dir,
169
209
  output_file=self.output_file,
170
210
  structure_only=False,
171
- hide_untoggled=True
211
+ hide_untoggled=True,
212
+ )
213
+ markdown_generator.generate_markdown(
214
+ pruned_tree, [Path(f) for f in selected_files]
172
215
  )
173
- markdown_generator.generate_markdown(pruned_tree, [Path(f) for f in selected_files])
174
216
  self.logger.info(f"Markdown generated at {self.output_file}.")
175
217
 
176
218
  def run(self) -> None:
@@ -180,19 +222,25 @@ class ProjectController:
180
222
  self.generate_output()
181
223
 
182
224
  def _load_gitignore_patterns(self) -> List[str]:
183
- gitignore_path = self.root_dir / '.gitignore'
225
+ gitignore_path = self.root_dir / ".gitignore"
184
226
  if not gitignore_path.exists():
185
227
  for parent in self.root_dir.parents:
186
- candidate = parent / '.gitignore'
228
+ candidate = parent / ".gitignore"
187
229
  if candidate.exists():
188
230
  gitignore_path = candidate
189
231
  break
190
232
  else:
191
233
  gitignore_path = None
192
234
  if gitignore_path and gitignore_path.exists():
193
- with gitignore_path.open('r') as gitignore:
194
- patterns = [line.strip() for line in gitignore if line.strip() and not line.strip().startswith('#')]
195
- self.logger.debug(f"Loaded .gitignore patterns from {gitignore_path.parent}: {patterns}")
235
+ with gitignore_path.open("r") as gitignore:
236
+ patterns = [
237
+ line.strip()
238
+ for line in gitignore
239
+ if line.strip() and not line.strip().startswith("#")
240
+ ]
241
+ self.logger.debug(
242
+ f"Loaded .gitignore patterns from {gitignore_path.parent}: {patterns}"
243
+ )
196
244
  return patterns
197
245
  else:
198
246
  self.logger.debug(f"No .gitignore found starting from {self.root_dir}.")
reposnap/core/git_repo.py CHANGED
@@ -30,4 +30,3 @@ class GitRepo:
30
30
  except InvalidGitRepositoryError:
31
31
  self.logger.error(f"Invalid Git repository at: {self.repo_path}")
32
32
  return []
33
-
@@ -1,79 +1,91 @@
1
- # src/reposnap/core/markdown_generator.py
2
-
1
+ # src/reposnap/core/markdown_generator.py ★ fully-rewritten file
3
2
  import logging
4
3
  from pathlib import Path
4
+ from typing import Dict, List, Any
5
+
5
6
  from reposnap.utils.path_utils import format_tree
6
- from typing import List, Dict, Any
7
7
 
8
8
 
9
9
  class MarkdownGenerator:
10
- def __init__(self, root_dir: Path, output_file: Path, structure_only: bool = False, hide_untoggled: bool = False):
11
- self.root_dir: Path = root_dir.resolve()
12
- self.output_file: Path = output_file.resolve()
13
- self.structure_only: bool = structure_only
14
- self.hide_untoggled: bool = hide_untoggled
15
- self.logger = logging.getLogger(__name__)
10
+ """Render the collected file-tree into a single Markdown document."""
16
11
 
17
- def generate_markdown(self, tree_structure: Dict[str, Any], files: List[Path]) -> None:
18
- """
19
- Generates the Markdown file based on the provided tree structure and files.
12
+ def __init__(
13
+ self,
14
+ root_dir: Path,
15
+ output_file: Path,
16
+ structure_only: bool = False,
17
+ hide_untoggled: bool = False,
18
+ ):
19
+ self.root_dir = root_dir.resolve()
20
+ self.output_file = output_file.resolve()
21
+ self.structure_only = structure_only
22
+ self.hide_untoggled = hide_untoggled
23
+ self.logger = logging.getLogger(__name__)
20
24
 
21
- Args:
22
- tree_structure (Dict[str, Any]): The hierarchical structure of the project files.
23
- files (List[Path]): List of file paths to include in the markdown.
24
- """
25
+ # --------------------------------------------------------------
26
+ # public API
27
+ # --------------------------------------------------------------
28
+ def generate_markdown(
29
+ self, tree_structure: Dict[str, Any], files: List[Path]
30
+ ) -> None:
31
+ """Write header (tree) and, unless *structure_only*, every file body."""
25
32
  self._write_header(tree_structure)
26
33
  if not self.structure_only:
27
34
  self._write_file_contents(files)
28
35
 
36
+ # --------------------------------------------------------------
37
+ # helpers
38
+ # --------------------------------------------------------------
29
39
  def _write_header(self, tree_structure: Dict[str, Any]) -> None:
30
- """
31
- Writes the header and project structure to the Markdown file.
32
- """
40
+ """Emit the *Project Structure* section."""
33
41
  self.logger.debug("Writing Markdown header and project structure.")
34
42
  try:
35
- with self.output_file.open('w', encoding='utf-8') as f:
36
- f.write("# Project Structure\n\n")
37
- f.write("```\n")
38
- for line in format_tree(tree_structure, hide_untoggled=self.hide_untoggled):
39
- f.write(line)
40
- f.write("```\n\n")
41
- self.logger.debug("Header and project structure written successfully.")
42
- except IOError as e:
43
- self.logger.error(f"Failed to write header to {self.output_file}: {e}")
43
+ with self.output_file.open(mode="w", encoding="utf-8") as fh:
44
+ fh.write("# Project Structure\n\n```\n")
45
+ for line in format_tree(
46
+ tree_structure, hide_untoggled=self.hide_untoggled
47
+ ):
48
+ fh.write(line)
49
+ fh.write("```\n\n")
50
+ except OSError as exc:
51
+ self.logger.error("Failed to write header: %s", exc)
44
52
  raise
45
53
 
46
54
  def _write_file_contents(self, files: List[Path]) -> None:
47
- """
48
- Writes the contents of each file to the Markdown file.
49
-
50
- Args:
51
- files (List[Path]): List of file paths relative to root_dir.
52
- """
55
+ """Append every file in *files* under its own fenced section."""
53
56
  self.logger.debug("Writing file contents to Markdown.")
54
- for relative_path in files:
55
- file_path: Path = self.root_dir / relative_path
56
-
57
- if not file_path.exists():
58
- self.logger.debug(f"File not found: {file_path}. Skipping.")
57
+ for rel_path in files:
58
+ abs_path = self.root_dir / rel_path
59
+ if not abs_path.exists(): # git had stale entry
60
+ self.logger.debug("File not found: %s -- skipping.", abs_path)
59
61
  continue
60
-
61
62
  try:
62
- self._write_file_content(file_path, relative_path.as_posix())
63
- except UnicodeDecodeError as e:
64
- self.logger.error(f"UnicodeDecodeError for file {file_path}: {e}")
65
-
63
+ self._write_single_file(abs_path, rel_path.as_posix())
64
+ except UnicodeDecodeError as exc:
65
+ self.logger.error("Unicode error for %s: %s", abs_path, exc)
66
66
 
67
- def _write_file_content(self, file_path: Path, relative_path: str) -> None:
67
+ # --------------------------------------------------------------
68
+ # single-file writer
69
+ # --------------------------------------------------------------
70
+ def _write_single_file(self, file_path: Path, rel_str: str) -> None:
68
71
  """
69
- Writes the content of a single file to the Markdown file with syntax highlighting.
72
+ Append one file.
73
+
74
+ We guarantee **one and only one** newline between the last character
75
+ of *content* and the closing code-fence so the output is stable and
76
+ deterministic (important for tests and downstream diff-tools).
70
77
  """
71
78
  try:
72
- with file_path.open('r', encoding='utf-8') as f:
73
- content: str = f.read()
74
- with self.output_file.open('a', encoding='utf-8') as f:
75
- f.write(f"## {relative_path}\n\n")
76
- f.write("```python\n" if file_path.suffix == '.py' else "```\n")
77
- f.write(f"{content}\n```\n\n")
78
- except IOError as e:
79
- self.logger.error(f"Error reading or writing file {file_path}: {e}")
79
+ with file_path.open(encoding="utf-8") as src:
80
+ content = src.read()
81
+
82
+ with self.output_file.open(mode="a", encoding="utf-8") as dst:
83
+ dst.write(f"## {rel_str}\n\n")
84
+ dst.write("```python\n" if file_path.suffix == ".py" else "```\n")
85
+
86
+ # normalise trailing EOL exactly one '\n'
87
+ dst.write(content if content.endswith("\n") else f"{content}\n")
88
+
89
+ dst.write("```\n\n")
90
+ except OSError as exc:
91
+ self.logger.error("Error processing %s: %s", file_path, exc)
@@ -7,27 +7,46 @@ from reposnap.controllers.project_controller import ProjectController
7
7
 
8
8
  def main():
9
9
  parser = argparse.ArgumentParser(
10
- description='Generate a Markdown representation of a Git repository.'
10
+ description="Generate a Markdown representation of a Git repository."
11
11
  )
12
12
  # Changed positional argument to allow one or more paths.
13
13
  parser.add_argument(
14
- 'paths',
15
- nargs='+',
16
- help='One or more paths (files or directories) to include in the Markdown output.'
17
- )
18
- parser.add_argument('-o', '--output', help='Output Markdown file', default='output.md')
19
- parser.add_argument('--structure-only', action='store_true',
20
- help='Only include the file structure without content.')
21
- parser.add_argument('--debug', action='store_true', help='Enable debug-level logging.')
22
- parser.add_argument('-i', '--include', nargs='*', default=[],
23
- help='File/folder patterns to include.')
24
- parser.add_argument('-e', '--exclude', nargs='*', default=[],
25
- help='File/folder patterns to exclude.')
14
+ "paths",
15
+ nargs="+",
16
+ help="One or more paths (files or directories) to include in the Markdown output.",
17
+ )
18
+ parser.add_argument(
19
+ "-o", "--output", help="Output Markdown file", default="output.md"
20
+ )
21
+ parser.add_argument(
22
+ "--structure-only",
23
+ action="store_true",
24
+ help="Only include the file structure without content.",
25
+ )
26
+ parser.add_argument(
27
+ "--debug", action="store_true", help="Enable debug-level logging."
28
+ )
29
+ parser.add_argument(
30
+ "-i",
31
+ "--include",
32
+ nargs="*",
33
+ default=[],
34
+ help="File/folder patterns to include.",
35
+ )
36
+ parser.add_argument(
37
+ "-e",
38
+ "--exclude",
39
+ nargs="*",
40
+ default=[],
41
+ help="File/folder patterns to exclude.",
42
+ )
26
43
 
27
44
  args = parser.parse_args()
28
45
 
29
46
  log_level = logging.DEBUG if args.debug else logging.INFO
30
- logging.basicConfig(level=log_level, format='%(asctime)s - %(levelname)s - %(message)s')
47
+ logging.basicConfig(
48
+ level=log_level, format="%(asctime)s - %(levelname)s - %(message)s"
49
+ )
31
50
 
32
51
  controller = ProjectController(args)
33
52
  controller.run()
@@ -14,7 +14,7 @@ class MyCheckBox(urwid.CheckBox):
14
14
  class RepoSnapGUI:
15
15
  def __init__(self):
16
16
  self.controller = ProjectController()
17
- self.root_dir = Path('.').resolve()
17
+ self.root_dir = Path(".").resolve()
18
18
  self.file_tree = None
19
19
  self.selected_files = set()
20
20
 
@@ -22,23 +22,22 @@ class RepoSnapGUI:
22
22
  self.build_main_menu()
23
23
 
24
24
  def build_main_menu(self):
25
- self.root_dir_edit = urwid.Edit(('bold', "Root Directory: "), str(self.root_dir))
25
+ self.root_dir_edit = urwid.Edit(
26
+ ("bold", "Root Directory: "), str(self.root_dir)
27
+ )
26
28
  scan_button = urwid.Button("Scan", on_press=self.on_scan)
27
29
 
28
30
  main_menu = urwid.Frame(
29
- header=urwid.Text(('bold', "RepoSnap - Main Menu")),
31
+ header=urwid.Text(("bold", "RepoSnap - Main Menu")),
30
32
  body=urwid.Padding(
31
33
  urwid.LineBox(
32
- urwid.ListBox(
33
- urwid.SimpleFocusListWalker([
34
- self.root_dir_edit
35
- ])
36
- ),
37
- title="Enter Root Directory"
34
+ urwid.ListBox(urwid.SimpleFocusListWalker([self.root_dir_edit])),
35
+ title="Enter Root Directory",
38
36
  ),
39
- left=2, right=2
37
+ left=2,
38
+ right=2,
40
39
  ),
41
- footer=urwid.Padding(scan_button, align='center')
40
+ footer=urwid.Padding(scan_button, align="center"),
42
41
  )
43
42
 
44
43
  self.main_widget = main_menu
@@ -56,9 +55,9 @@ class RepoSnapGUI:
56
55
  render_button = urwid.Button("Render", on_press=self.on_render)
57
56
 
58
57
  tree_menu = urwid.Frame(
59
- header=urwid.Text(('bold', f"File Tree of {self.root_dir}")),
58
+ header=urwid.Text(("bold", f"File Tree of {self.root_dir}")),
60
59
  body=urwid.LineBox(tree_listbox),
61
- footer=urwid.Padding(render_button, align='center')
60
+ footer=urwid.Padding(render_button, align="center"),
62
61
  )
63
62
 
64
63
  self.main_widget = tree_menu
@@ -67,23 +66,25 @@ class RepoSnapGUI:
67
66
  def build_tree_widget(self, tree_structure, parent_path="", level=0):
68
67
  widgets = []
69
68
  for key, value in sorted(tree_structure.items()):
70
- node_path = f"{parent_path}/{key}".lstrip('/')
69
+ node_path = f"{parent_path}/{key}".lstrip("/")
71
70
  checkbox = MyCheckBox(
72
71
  key,
73
- user_data={'path': node_path, 'level': level},
72
+ user_data={"path": node_path, "level": level},
74
73
  state=False,
75
- on_state_change=self.on_checkbox_change
74
+ on_state_change=self.on_checkbox_change,
76
75
  )
77
- indented_checkbox = urwid.Padding(checkbox, left=4*level)
76
+ indented_checkbox = urwid.Padding(checkbox, left=4 * level)
78
77
  widgets.append(indented_checkbox)
79
78
  if isinstance(value, dict):
80
- widgets.extend(self.build_tree_widget(value, node_path, level=level+1))
79
+ widgets.extend(
80
+ self.build_tree_widget(value, node_path, level=level + 1)
81
+ )
81
82
  return widgets
82
83
 
83
84
  def on_checkbox_change(self, checkbox, state):
84
85
  user_data = checkbox.user_data
85
- node_path = user_data['path']
86
- level = user_data['level']
86
+ node_path = user_data["path"]
87
+ level = user_data["level"]
87
88
  if state:
88
89
  self.selected_files.add(node_path)
89
90
  else:
@@ -108,10 +109,10 @@ class RepoSnapGUI:
108
109
  if isinstance(widget, urwid.Padding):
109
110
  checkbox_widget = widget.original_widget
110
111
  widget_user_data = checkbox_widget.user_data
111
- widget_level = widget_user_data['level']
112
+ widget_level = widget_user_data["level"]
112
113
  if widget_level > level:
113
114
  checkbox_widget.set_state(state, do_callback=False)
114
- node_path = widget_user_data['path']
115
+ node_path = widget_user_data["path"]
115
116
  if state:
116
117
  self.selected_files.add(node_path)
117
118
  else:
@@ -124,12 +125,14 @@ class RepoSnapGUI:
124
125
 
125
126
  def on_render(self, button):
126
127
  self.controller.generate_output_from_selected(self.selected_files)
127
- message = urwid.Text(('bold', f"Markdown generated at {self.controller.output_file}"))
128
+ message = urwid.Text(
129
+ ("bold", f"Markdown generated at {self.controller.output_file}")
130
+ )
128
131
  exit_button = urwid.Button("Exit", on_press=self.exit_program)
129
132
  result_menu = urwid.Frame(
130
- header=urwid.Text(('bold', "Success")),
131
- body=urwid.Filler(message, valign='middle'),
132
- footer=urwid.Padding(exit_button, align='center')
133
+ header=urwid.Text(("bold", "Success")),
134
+ body=urwid.Filler(message, valign="middle"),
135
+ footer=urwid.Padding(exit_button, align="center"),
133
136
  )
134
137
  self.main_widget = result_menu
135
138
  self.refresh()
@@ -19,10 +19,12 @@ class FileTree:
19
19
  """
20
20
  return self._extract_files(self.structure)
21
21
 
22
- def _extract_files(self, subtree: Dict[str, Any], path_prefix: str = '') -> List[Path]:
22
+ def _extract_files(
23
+ self, subtree: Dict[str, Any], path_prefix: str = ""
24
+ ) -> List[Path]:
23
25
  files: List[Path] = []
24
26
  for key, value in subtree.items():
25
- current_path: str = f"{path_prefix}/{key}".lstrip('/')
27
+ current_path: str = f"{path_prefix}/{key}".lstrip("/")
26
28
  if isinstance(value, dict):
27
29
  files.extend(self._extract_files(value, current_path))
28
30
  else:
@@ -39,17 +41,23 @@ class FileTree:
39
41
  self.logger.debug("Filtering files in the file tree.")
40
42
  self.structure = self._filter_tree(self.structure, spec)
41
43
 
42
- def _filter_tree(self, subtree: Dict[str, Any], spec: pathspec.PathSpec, path_prefix: str = '') -> Dict[str, Any]:
44
+ def _filter_tree(
45
+ self, subtree: Dict[str, Any], spec: pathspec.PathSpec, path_prefix: str = ""
46
+ ) -> Dict[str, Any]:
43
47
  filtered_subtree: Dict[str, Any] = {}
44
48
  for key, value in subtree.items():
45
- current_path: str = f"{path_prefix}/{key}".lstrip('/')
49
+ current_path: str = f"{path_prefix}/{key}".lstrip("/")
46
50
  if isinstance(value, dict):
47
- filtered_value: Dict[str, Any] = self._filter_tree(value, spec, current_path)
51
+ filtered_value: Dict[str, Any] = self._filter_tree(
52
+ value, spec, current_path
53
+ )
48
54
  if filtered_value:
49
55
  filtered_subtree[key] = filtered_value
50
56
  else:
51
57
  # Exclude the file if either the full path OR its basename matches a .gitignore pattern.
52
- if not spec.match_file(current_path) and not spec.match_file(Path(current_path).name):
58
+ if not spec.match_file(current_path) and not spec.match_file(
59
+ Path(current_path).name
60
+ ):
53
61
  filtered_subtree[key] = value
54
62
  return filtered_subtree
55
63
 
@@ -65,12 +73,16 @@ class FileTree:
65
73
  """
66
74
  return self._prune_tree(self.structure, selected_files)
67
75
 
68
- def _prune_tree(self, subtree: Dict[str, Any], selected_files: set, path_prefix: str = '') -> Dict[str, Any]:
76
+ def _prune_tree(
77
+ self, subtree: Dict[str, Any], selected_files: set, path_prefix: str = ""
78
+ ) -> Dict[str, Any]:
69
79
  pruned_subtree: Dict[str, Any] = {}
70
80
  for key, value in subtree.items():
71
- current_path: str = f"{path_prefix}/{key}".lstrip('/')
81
+ current_path: str = f"{path_prefix}/{key}".lstrip("/")
72
82
  if isinstance(value, dict):
73
- pruned_value: Dict[str, Any] = self._prune_tree(value, selected_files, current_path)
83
+ pruned_value: Dict[str, Any] = self._prune_tree(
84
+ value, selected_files, current_path
85
+ )
74
86
  if pruned_value:
75
87
  pruned_subtree[key] = pruned_value
76
88
  else:
@@ -2,12 +2,14 @@
2
2
  from typing import Dict, Generator, Any
3
3
 
4
4
 
5
- def format_tree(tree: Dict[str, Any], indent: str = '', hide_untoggled: bool = False) -> Generator[str, None, None]:
5
+ def format_tree(
6
+ tree: Dict[str, Any], indent: str = "", hide_untoggled: bool = False
7
+ ) -> Generator[str, None, None]:
6
8
  for key, value in tree.items():
7
- if value == '<hidden>':
9
+ if value == "<hidden>":
8
10
  yield f"{indent}<...>\n"
9
11
  elif isinstance(value, dict):
10
12
  yield f"{indent}{key}/\n"
11
- yield from format_tree(value, indent + ' ', hide_untoggled)
13
+ yield from format_tree(value, indent + " ", hide_untoggled)
12
14
  else:
13
15
  yield f"{indent}{key}\n"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reposnap
3
- Version: 0.6.3
3
+ Version: 0.6.5
4
4
  Summary: Generate a Markdown file with all contents of your project
5
5
  Author: agoloborodko
6
6
  License-File: LICENSE
@@ -0,0 +1,19 @@
1
+ reposnap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ reposnap/controllers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ reposnap/controllers/project_controller.py,sha256=XranKRoWd1cRZ4cEXmN_YddO-GHREz9t2J0TUJ4mtgs,10053
4
+ reposnap/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ reposnap/core/file_system.py,sha256=82gwvmgrsWf63paMrIz-Z0eqIjbqt9_-vujdXlJJoFE,1074
6
+ reposnap/core/git_repo.py,sha256=I0AhB6XbABJ-oVGOSkVhSjFjSFfcm6f1VFHGTeuM4gE,1255
7
+ reposnap/core/markdown_generator.py,sha256=V6uEbxVSbCbxKN9ysTDKsIDvEGBxFutpOpyaZRXZUGw,3747
8
+ reposnap/interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ reposnap/interfaces/cli.py,sha256=qkbYAlgZzlxgW8ePIUwd8JSbvnBVSvde-VlVqzMDh7g,1470
10
+ reposnap/interfaces/gui.py,sha256=sTuQxjD1nPa9FpgfzOwi6VDO5QMMtDX-5CiEhbJJcs4,5429
11
+ reposnap/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ reposnap/models/file_tree.py,sha256=jGo_SizdFcOiDC1OOMz-tiijRN3iSD7ENh6Xw8S6OL0,3362
13
+ reposnap/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ reposnap/utils/path_utils.py,sha256=UrMe5cjspTf-4gjg2lzv6BgLwZ7S_1lLECQvDMDZO9Y,507
15
+ reposnap-0.6.5.dist-info/METADATA,sha256=V85e8nN5tNH_U-W_tludr_nG8CNvhGEDAX34Tz1r8JM,5348
16
+ reposnap-0.6.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
+ reposnap-0.6.5.dist-info/entry_points.txt,sha256=o3GyO7bpR0dujPCjsvvZMPv4pXNJlFwD49_pA1r5FOA,102
18
+ reposnap-0.6.5.dist-info/licenses/LICENSE,sha256=Aj7WCYBXi98pvi723HPn4GDRyjxToNWb3PC6j1_lnPk,1069
19
+ reposnap-0.6.5.dist-info/RECORD,,
@@ -1,19 +0,0 @@
1
- reposnap/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- reposnap/controllers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- reposnap/controllers/project_controller.py,sha256=u3agILanSms5Gx4D5e6EWhHrb6B08saz5udct8yVS-s,9353
4
- reposnap/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- reposnap/core/file_system.py,sha256=82gwvmgrsWf63paMrIz-Z0eqIjbqt9_-vujdXlJJoFE,1074
6
- reposnap/core/git_repo.py,sha256=2u_ILkV-Ur7qr1WHmHM2yg44Ggft61RsdbZLsZaQ5NU,1256
7
- reposnap/core/markdown_generator.py,sha256=Ld6ix4gzkLJJyeUoWHwhpbAf3DvEC5E0S1DykYnLGnQ,3297
8
- reposnap/interfaces/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- reposnap/interfaces/cli.py,sha256=JzTNDibzuRRmnWg-gBfKJ2tSlh-NYSL_3q6J-Erjrr8,1374
10
- reposnap/interfaces/gui.py,sha256=pzWQbW55gBNZu4tXRdBFic39upGtYxew91FSiEvalj0,5421
11
- reposnap/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- reposnap/models/file_tree.py,sha256=0WcSDbFH5pSZHyWxWtmz-FF4_ELnZ3Byz2iXN4Tpijw,3206
13
- reposnap/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- reposnap/utils/path_utils.py,sha256=7072816LCP8Q8XBydn0iknmfrObPO_-2rFqpbAvPrjY,501
15
- reposnap-0.6.3.dist-info/METADATA,sha256=wPRE4NKJuzwMKTyYD8e2nPUYUNjwM81kCEsVz18yWTY,5348
16
- reposnap-0.6.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
- reposnap-0.6.3.dist-info/entry_points.txt,sha256=o3GyO7bpR0dujPCjsvvZMPv4pXNJlFwD49_pA1r5FOA,102
18
- reposnap-0.6.3.dist-info/licenses/LICENSE,sha256=Aj7WCYBXi98pvi723HPn4GDRyjxToNWb3PC6j1_lnPk,1069
19
- reposnap-0.6.3.dist-info/RECORD,,