dirshot 0.1.1__py3-none-any.whl → 0.1.2__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.
- dirshot/dirshot.py +129 -49
- {dirshot-0.1.1.dist-info → dirshot-0.1.2.dist-info}/METADATA +1 -1
- dirshot-0.1.2.dist-info/RECORD +7 -0
- dirshot-0.1.1.dist-info/RECORD +0 -7
- {dirshot-0.1.1.dist-info → dirshot-0.1.2.dist-info}/WHEEL +0 -0
- {dirshot-0.1.1.dist-info → dirshot-0.1.2.dist-info}/top_level.txt +0 -0
dirshot/dirshot.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
3
|
import re
|
|
4
|
+
import time # Imported for the fallback progress bar
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from dataclasses import dataclass, field
|
|
6
7
|
from typing import List, Optional, Set, Tuple, Callable, NamedTuple, Dict, Any
|
|
@@ -13,21 +14,70 @@ try:
|
|
|
13
14
|
from tqdm import tqdm
|
|
14
15
|
except ImportError:
|
|
15
16
|
|
|
17
|
+
# Define a functional fallback dummy tqdm class if the import fails.
|
|
16
18
|
class tqdm:
|
|
17
|
-
|
|
19
|
+
"""A simple, text-based progress bar fallback if tqdm is not installed."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, iterable=None, total=None, desc="", unit="it", **kwargs):
|
|
18
22
|
self.iterable = iterable
|
|
23
|
+
self.total = (
|
|
24
|
+
total
|
|
25
|
+
if total is not None
|
|
26
|
+
else (len(iterable) if hasattr(iterable, "__len__") else None)
|
|
27
|
+
)
|
|
28
|
+
self.desc = desc
|
|
29
|
+
self.unit = unit
|
|
30
|
+
self.current = 0
|
|
31
|
+
self.start_time = time.time()
|
|
32
|
+
self._last_update_time = 0
|
|
19
33
|
|
|
20
34
|
def __iter__(self):
|
|
21
|
-
|
|
35
|
+
for obj in self.iterable:
|
|
36
|
+
yield obj
|
|
37
|
+
self.update(1)
|
|
38
|
+
# The loop is finished, ensure the bar is 100% and close
|
|
39
|
+
if self.total is not None and self.current < self.total:
|
|
40
|
+
self.update(self.total - self.current)
|
|
41
|
+
self.close()
|
|
22
42
|
|
|
23
43
|
def update(self, n=1):
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
44
|
+
"""Update the progress bar by n steps."""
|
|
45
|
+
self.current += n
|
|
46
|
+
now = time.time()
|
|
47
|
+
# Throttle screen updates to prevent flickering and performance loss
|
|
48
|
+
if (
|
|
49
|
+
self.total is None
|
|
50
|
+
or now - self._last_update_time > 0.1
|
|
51
|
+
or self.current == self.total
|
|
52
|
+
):
|
|
53
|
+
self._last_update_time = now
|
|
54
|
+
self._draw()
|
|
55
|
+
|
|
56
|
+
def set_description(self, desc: str):
|
|
57
|
+
"""Set the description of the progress bar."""
|
|
58
|
+
self.desc = desc
|
|
59
|
+
self._draw()
|
|
60
|
+
|
|
61
|
+
def _draw(self):
|
|
62
|
+
"""Draw the progress bar to the console."""
|
|
63
|
+
if self.total:
|
|
64
|
+
percent = int((self.current / self.total) * 100)
|
|
65
|
+
bar_length = 25
|
|
66
|
+
filled_length = int(bar_length * self.current // self.total)
|
|
67
|
+
bar = "█" * filled_length + "-" * (bar_length - filled_length)
|
|
68
|
+
# Use carriage return to print on the same line
|
|
69
|
+
progress_line = f"\r{self.desc}: {percent}%|{bar}| {self.current}/{self.total} [{self.unit}]"
|
|
70
|
+
sys.stdout.write(progress_line)
|
|
71
|
+
else: # Case where total is not known
|
|
72
|
+
sys.stdout.write(f"\r{self.desc}: {self.current} {self.unit}")
|
|
73
|
+
|
|
74
|
+
sys.stdout.flush()
|
|
28
75
|
|
|
29
76
|
def close(self):
|
|
30
|
-
|
|
77
|
+
"""Clean up the progress bar line."""
|
|
78
|
+
# Print a newline to move off the progress bar line
|
|
79
|
+
sys.stdout.write("\n")
|
|
80
|
+
sys.stdout.flush()
|
|
31
81
|
|
|
32
82
|
|
|
33
83
|
# --- Configuration Constants ---
|
|
@@ -457,33 +507,36 @@ def _collate_content_to_file(
|
|
|
457
507
|
)
|
|
458
508
|
else: # ProjectMode.SEARCH
|
|
459
509
|
stats_key = (
|
|
460
|
-
"Key: [M: Matched files/dirs]\n"
|
|
461
|
-
" (f=files, d=directories)\n\n"
|
|
510
|
+
"Key: [M: Matched files/dirs]\n" " (f=files, d=directories)\n\n"
|
|
462
511
|
)
|
|
463
512
|
buffer.write(stats_key)
|
|
464
513
|
tree_content = "\n".join(tree_content_lines)
|
|
465
514
|
buffer.write(tree_content + "\n")
|
|
466
515
|
buffer.write(f"\n{separator_line}\n\n")
|
|
467
516
|
|
|
468
|
-
for
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
) as infile:
|
|
475
|
-
file_content = infile.read()
|
|
476
|
-
buffer.write(file_content)
|
|
477
|
-
buffer.write("\n\n")
|
|
478
|
-
except Exception:
|
|
479
|
-
buffer.write(
|
|
480
|
-
f"Error: Could not read file '{file_info.relative_path_posix}'.\n\n"
|
|
481
|
-
)
|
|
482
|
-
|
|
483
|
-
if not files_to_process and not tree_content_lines:
|
|
484
|
-
buffer.write(
|
|
485
|
-
"No files found matching the specified criteria for content aggregation.\n"
|
|
517
|
+
# This message is for the file content, not the console.
|
|
518
|
+
if not files_to_process:
|
|
519
|
+
message = (
|
|
520
|
+
"No files found matching the specified criteria.\n"
|
|
521
|
+
if mode == ProjectMode.SEARCH
|
|
522
|
+
else "No files found matching the specified criteria for content aggregation.\n"
|
|
486
523
|
)
|
|
524
|
+
buffer.write(message)
|
|
525
|
+
else:
|
|
526
|
+
for file_info in files_to_process:
|
|
527
|
+
header_content = f"{separator_line}\n{FILE_HEADER_PREFIX}{file_info.relative_path_posix}\n{separator_line}\n\n"
|
|
528
|
+
buffer.write(header_content)
|
|
529
|
+
try:
|
|
530
|
+
with open(
|
|
531
|
+
file_info.absolute_path, "r", encoding=encoding, errors="replace"
|
|
532
|
+
) as infile:
|
|
533
|
+
file_content = infile.read()
|
|
534
|
+
buffer.write(file_content)
|
|
535
|
+
buffer.write("\n\n")
|
|
536
|
+
except Exception:
|
|
537
|
+
buffer.write(
|
|
538
|
+
f"Error: Could not read file '{file_info.relative_path_posix}'.\n\n"
|
|
539
|
+
)
|
|
487
540
|
|
|
488
541
|
# Get the complete content from the buffer
|
|
489
542
|
final_content = buffer.getvalue()
|
|
@@ -506,17 +559,22 @@ def _collate_content_to_file(
|
|
|
506
559
|
# Write the main content
|
|
507
560
|
outfile.write(final_content)
|
|
508
561
|
except IOError as e:
|
|
509
|
-
print(f"
|
|
562
|
+
print(f"\nError: Could not write to output file '{output_file_path}': {e}")
|
|
510
563
|
return
|
|
511
564
|
|
|
512
|
-
# Final console output
|
|
513
|
-
|
|
565
|
+
# Final console output for user feedback
|
|
566
|
+
if mode == ProjectMode.SEARCH:
|
|
567
|
+
if files_to_process:
|
|
568
|
+
print("\nSuccess! Collation complete.")
|
|
569
|
+
else: # Filter mode has its own messaging pattern
|
|
570
|
+
print(f"\nProcess complete. Output written to: {output_file_path}")
|
|
571
|
+
if len(files_to_process) > 0:
|
|
572
|
+
print(
|
|
573
|
+
f"Summary: {len(files_to_process)} files selected for content processing."
|
|
574
|
+
)
|
|
575
|
+
|
|
514
576
|
if show_token_count:
|
|
515
577
|
print(f"Total Approximated Tokens ({mode_display}): {total_token_count}")
|
|
516
|
-
if len(files_to_process) > 0:
|
|
517
|
-
print(
|
|
518
|
-
f"Summary: {len(files_to_process)} files selected for content processing."
|
|
519
|
-
)
|
|
520
578
|
|
|
521
579
|
|
|
522
580
|
def filter_and_append_content(
|
|
@@ -611,19 +669,43 @@ def search_and_collate_content(
|
|
|
611
669
|
if not normalized_keywords:
|
|
612
670
|
print("Error: Search mode requires 'search_keywords' to be provided.")
|
|
613
671
|
return
|
|
672
|
+
|
|
673
|
+
print("Phase 1: Finding all matching files...")
|
|
674
|
+
if criteria.ignore_path_components:
|
|
675
|
+
print(
|
|
676
|
+
f"Ignoring directories and files containing: {', '.join(criteria.ignore_path_components)}"
|
|
677
|
+
)
|
|
678
|
+
|
|
614
679
|
candidate_files: List[Path] = []
|
|
615
680
|
for dirpath_str, dirnames, filenames in os.walk(str(root_dir), topdown=True):
|
|
616
681
|
current_dir_path = Path(dirpath_str)
|
|
682
|
+
# Prune directories based on ignore criteria
|
|
617
683
|
dirnames[:] = [
|
|
618
|
-
d
|
|
684
|
+
d
|
|
685
|
+
for d in dirnames
|
|
686
|
+
if (current_dir_path / d).name.lower()
|
|
687
|
+
not in criteria.ignore_path_components
|
|
619
688
|
]
|
|
689
|
+
|
|
620
690
|
for filename in filenames:
|
|
621
691
|
file_abs_path = current_dir_path / filename
|
|
692
|
+
# Also ignore individual files based on path components
|
|
693
|
+
try:
|
|
694
|
+
relative_parts = file_abs_path.relative_to(root_dir).parts
|
|
695
|
+
if any(
|
|
696
|
+
part.lower() in criteria.ignore_path_components
|
|
697
|
+
for part in relative_parts
|
|
698
|
+
):
|
|
699
|
+
continue
|
|
700
|
+
except ValueError:
|
|
701
|
+
continue
|
|
702
|
+
|
|
622
703
|
if (
|
|
623
|
-
|
|
624
|
-
or
|
|
704
|
+
not criteria.file_extensions
|
|
705
|
+
or file_abs_path.suffix.lower() in criteria.file_extensions
|
|
625
706
|
):
|
|
626
707
|
candidate_files.append(file_abs_path)
|
|
708
|
+
|
|
627
709
|
matched_files: Set[Path] = set()
|
|
628
710
|
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
629
711
|
future_to_file = {
|
|
@@ -646,23 +728,21 @@ def search_and_collate_content(
|
|
|
646
728
|
result = future.result()
|
|
647
729
|
if result:
|
|
648
730
|
matched_files.add(result)
|
|
731
|
+
|
|
649
732
|
if not matched_files:
|
|
650
733
|
print("\nScan complete. No matching files were found.")
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
[],
|
|
655
|
-
DEFAULT_ENCODING,
|
|
656
|
-
DEFAULT_SEPARATOR_CHAR,
|
|
657
|
-
DEFAULT_SEPARATOR_LINE_LENGTH,
|
|
658
|
-
show_token_count,
|
|
659
|
-
show_tree_stats,
|
|
660
|
-
ProjectMode.SEARCH,
|
|
661
|
-
)
|
|
734
|
+
# Still create the output file with a "not found" message
|
|
735
|
+
with open(output_file, "w", encoding=DEFAULT_ENCODING) as f_out:
|
|
736
|
+
f_out.write("No files found matching the specified criteria.\n")
|
|
662
737
|
return
|
|
738
|
+
|
|
663
739
|
sorted_matched_files = sorted(
|
|
664
740
|
list(matched_files), key=lambda p: p.relative_to(root_dir).as_posix().lower()
|
|
665
741
|
)
|
|
742
|
+
|
|
743
|
+
print(f"\nPhase 1 Complete: Found {len(sorted_matched_files)} matching files.")
|
|
744
|
+
print(f"\nPhase 2: Generating output file at '{Path(output_file).resolve()}'...")
|
|
745
|
+
|
|
666
746
|
tree_content_lines = _generate_tree_from_paths(
|
|
667
747
|
root_dir, sorted_matched_files, tree_style, show_tree_stats
|
|
668
748
|
)
|
|
@@ -928,4 +1008,4 @@ if __name__ == "__main__":
|
|
|
928
1008
|
ignore_dirs_in_path=["venv", "build", "node_modules", "static", "templates"],
|
|
929
1009
|
show_tree_stats=True,
|
|
930
1010
|
show_token_count=True,
|
|
931
|
-
)
|
|
1011
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dirshot
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: A flexible utility for creating project snapshots and searching for files.
|
|
5
5
|
Author-email: init-helpful <init.helpful@gmail.com>
|
|
6
6
|
Project-URL: Homepage, https://github.com/init-helpful/dirshot
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
dirshot/__init__.py,sha256=ss4HC5VTyD9j6GFGCLMU6VxPlXy0qaGFzXlZB3_d2WM,403
|
|
2
|
+
dirshot/dirshot.py,sha256=jpOFyHGlgkVQrG_uHFWfvM-XWcv6tJuaVFvCI7_5fn0,38304
|
|
3
|
+
dirshot/examples.py,sha256=q--iNqxmA4xX8nyXYdOP-HPsqzpLHBFo1PTseQ9ki7M,2344
|
|
4
|
+
dirshot-0.1.2.dist-info/METADATA,sha256=KsaUKmA1g9LuxIYmCH-mtFVaCkYS97_fLmSN60tyq4M,4172
|
|
5
|
+
dirshot-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
+
dirshot-0.1.2.dist-info/top_level.txt,sha256=ROGW8gTcmwJ2jJ1Fp7TV1REZLRUGbL3L-Lfoy8tPxOA,8
|
|
7
|
+
dirshot-0.1.2.dist-info/RECORD,,
|
dirshot-0.1.1.dist-info/RECORD
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
dirshot/__init__.py,sha256=ss4HC5VTyD9j6GFGCLMU6VxPlXy0qaGFzXlZB3_d2WM,403
|
|
2
|
-
dirshot/dirshot.py,sha256=ItCwC4BsSbPzBLlHddiFlYsqdB3Hh3PEpwN89EuplIc,34693
|
|
3
|
-
dirshot/examples.py,sha256=q--iNqxmA4xX8nyXYdOP-HPsqzpLHBFo1PTseQ9ki7M,2344
|
|
4
|
-
dirshot-0.1.1.dist-info/METADATA,sha256=z72qXvnkUFizL4qkdXEXF6QWu3yZs28szf9wuaru4kI,4172
|
|
5
|
-
dirshot-0.1.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
6
|
-
dirshot-0.1.1.dist-info/top_level.txt,sha256=ROGW8gTcmwJ2jJ1Fp7TV1REZLRUGbL3L-Lfoy8tPxOA,8
|
|
7
|
-
dirshot-0.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|