pseek 2.1.1__tar.gz → 2.1.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pseek
3
- Version: 2.1.1
3
+ Version: 2.1.3
4
4
  Summary: Pseek is a Python library to search files, folders, and text
5
5
  Home-page: https://github.com/ArianN8610/pysearch
6
6
  Author: Arian
@@ -100,3 +100,4 @@ pseek "error\d+" --regex
100
100
  | `--word` | Match the whole word only |
101
101
  | `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
102
102
  | `--full-path` | Display full path of files and directories |
103
+ | `--no-content` | Only display files path for content search |
@@ -76,3 +76,4 @@ pseek "error\d+" --regex
76
76
  | `--word` | Match the whole word only |
77
77
  | `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
78
78
  | `--full-path` | Display full path of files and directories |
79
+ | `--no-content` | Only display files path for content search |
@@ -13,7 +13,7 @@ from .searcher import Search
13
13
  # Additional options
14
14
  @click.option('-C', '--case-sensitive', is_flag=True, help='Make the search case-sensitive.')
15
15
  @click.option('--regex', is_flag=True, help='Use regular expression for searching.')
16
- @click.option('--word', is_flag=True, help='Match whole words only.')
16
+ @click.option('-w', '--word', is_flag=True, help='Match whole words only.')
17
17
  # Extension filters
18
18
  @click.option('--ext', multiple=True, type=click.STRING,
19
19
  help='Include files with these extensions. Example: --ext py --ext js')
@@ -29,8 +29,9 @@ from .searcher import Search
29
29
  @click.option('--min-size', type=click.FLOAT, help='Minimum file/directory size (in MB).')
30
30
  # Output option
31
31
  @click.option('--full-path', is_flag=True, help='Display full paths for results.')
32
+ @click.option('--no-content', is_flag=True, help='Only display files path for content search.')
32
33
  def search(query, path, file, directory, content, case_sensitive, ext, exclude_ext, regex,
33
- include, exclude, word, max_size, min_size, full_path):
34
+ include, exclude, word, max_size, min_size, full_path, no_content):
34
35
  """Search for files, directories, and file content based on the query."""
35
36
  # If no search type is specified, search in all types.
36
37
  if not any((file, directory, content)):
@@ -49,7 +50,8 @@ def search(query, path, file, directory, content, case_sensitive, ext, exclude_e
49
50
  whole_word=word,
50
51
  max_size=max_size,
51
52
  min_size=min_size,
52
- full_path=full_path
53
+ full_path=full_path,
54
+ no_content=no_content
53
55
  )
54
56
 
55
57
  total_results = 0
@@ -1,6 +1,7 @@
1
1
  import re
2
2
  import sys
3
3
  import click
4
+ import mmap
4
5
  from pathlib import Path
5
6
  from concurrent.futures import ThreadPoolExecutor
6
7
 
@@ -20,23 +21,24 @@ EXCLUDED_EXTENSIONS = {
20
21
 
21
22
  class Search:
22
23
  def __init__(self, base_path, query, case_sensitive, ext, exclude_ext, regex, include, exclude, whole_word,
23
- max_size, min_size, full_path):
24
+ max_size, min_size, full_path, no_content):
24
25
  """Initialize search parameters"""
25
- self.base_path = base_path
26
+ self.base_path = Path(base_path)
26
27
  self.query = query
27
28
  self.case_sensitive = case_sensitive
28
- self.ext = ext
29
- self.exclude_ext = exclude_ext
29
+ self.ext = set(ext)
30
+ self.exclude_ext = set(exclude_ext)
30
31
  self.regex = regex
31
- self.include = include
32
- self.exclude = exclude
32
+ self.include = {Path(p).resolve() for p in include}
33
+ self.exclude = {Path(p).resolve() for p in exclude}
33
34
  self.whole_word = whole_word
34
35
  self.max_size = max_size
35
36
  self.min_size = min_size
36
37
  self.full_path = full_path
38
+ self.no_content = no_content
37
39
  self.result = None
38
40
 
39
- def conditions(self, p_resolved: Path, search_type: str) -> bool:
41
+ def should_skip(self, p_resolved: Path, search_type: str) -> bool:
40
42
  """
41
43
  Check whether the file/directory should be skipped based on various filters.
42
44
  Returns True if the path should be skipped.
@@ -47,15 +49,10 @@ class Search:
47
49
  # If path is inaccessible, skip it.
48
50
  return True
49
51
 
50
- # Prepare include/exclude sets as resolved Paths
51
- include_paths = {Path(p).resolve() for p in self.include}
52
- exclude_paths = {Path(p).resolve() for p in self.exclude}
52
+ file_ext = p_resolved.suffix[1:].lower()
53
53
 
54
- file_ext = p_resolved.suffix[1:]
55
-
56
- # Conditions for skipping:
57
- if (include_paths and not any(p_resolved.is_relative_to(inc) for inc in include_paths)) \
58
- or (exclude_paths and any(p_resolved.is_relative_to(exc) for exc in exclude_paths)) \
54
+ if (self.include and not any(p_resolved.is_relative_to(inc) for inc in self.include)) \
55
+ or (self.exclude and any(p_resolved.is_relative_to(exc) for exc in self.exclude)) \
59
56
  or (self.ext and file_ext not in self.ext) \
60
57
  or (self.exclude_ext and file_ext in self.exclude_ext) \
61
58
  or (search_type == 'content' and file_ext in EXCLUDED_EXTENSIONS) \
@@ -69,18 +66,11 @@ class Search:
69
66
 
70
67
  def search(self, search_type: str):
71
68
  """Main search function. search_type can be 'file', 'directory' or 'content'"""
72
- base_path = Path(self.base_path)
73
69
  query = self.query
74
70
 
75
71
  # Prepare query: escape if not regex
76
72
  if not self.regex:
77
73
  query = re.escape(query)
78
- else:
79
- try:
80
- re.compile(query) # Validate regex pattern
81
- except re.error:
82
- click.echo(click.style('Invalid regex pattern: ', fg='red') + query)
83
- sys.exit(1)
84
74
 
85
75
  # Apply whole-word matching only if not using regex (or if desired behavior is defined)
86
76
  if self.whole_word and not self.regex:
@@ -97,13 +87,13 @@ class Search:
97
87
 
98
88
  if search_type in ('file', 'directory'):
99
89
  matches = []
100
- for p in base_path.rglob('*'):
90
+ for p in self.base_path.rglob('*'):
101
91
  try:
102
92
  p_resolved = p.resolve()
103
93
  except Exception:
104
94
  continue
105
95
  # Skip if conditions fail or if name doesn't match the query
106
- if self.conditions(p_resolved, search_type) or not pattern.search(p.name):
96
+ if self.should_skip(p_resolved, search_type) or not pattern.search(p.name):
107
97
  continue
108
98
 
109
99
  # Highlight matched query in the name
@@ -111,46 +101,74 @@ class Search:
111
101
  # Choose parent path based on full_path flag
112
102
  p_parent = p_resolved.parent if self.full_path else p.parent
113
103
  matches.append(f'{p_parent}\\{highlighted_name}')
114
- else: # For content search
104
+ else: # content search
115
105
  # Use dictionary: key: file path (colored), value: list of line matches
116
- matches = {}
106
+ matches = {} if not self.no_content else set()
107
+ # Compile binary version of the pattern
108
+ pattern_bytes = re.compile(pattern.pattern.encode('utf-8'))
117
109
 
118
110
  def process_file(file_path: Path):
119
111
  """Process a single file for content search"""
120
112
  try:
121
- p_resolved = file_path.resolve()
113
+ # Avoid empty files for mmap
114
+ if file_path.stat().st_size == 0:
115
+ return
116
+
117
+ # Open the file in binary read mode
118
+ with open(file_path, 'rb') as f:
119
+ # Memory-map the file for efficient access
120
+ with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
121
+ # Search for the pattern in the entire file as bytes
122
+ if pattern_bytes.search(mm):
123
+ # Choose the file path format based on the full_path setting
124
+ file_label = str(file_path.resolve()) if self.full_path else str(file_path)
125
+
126
+ # Avoid searching through the entire file content if the fast-content flag is True
127
+ if self.no_content:
128
+ matches.add(click.style(file_label, fg='cyan'))
129
+ return
130
+
131
+ lines = []
132
+ mm.seek(0) # Move the cursor to the beginning of the file
133
+
134
+ # Iterate over each line in the file
135
+ for num, line in enumerate(iter(mm.readline, b''), 1):
136
+ try:
137
+ # Decode the binary line as UTF-8 and strip whitespace
138
+ line_decoded = line.decode('utf-8').strip()
139
+ except UnicodeDecodeError:
140
+ # Skip lines that can't be decoded
141
+ continue
142
+
143
+ # If the pattern matches in the decoded line
144
+ if pattern_list := list(pattern.finditer(line_decoded)):
145
+ # Count how many times the pattern appears in the line
146
+ count = len(pattern_list)
147
+ # Highlight the matching parts in green
148
+ highlighted = pattern.sub(
149
+ lambda m: click.style(m.group(), fg='green'),
150
+ line_decoded
151
+ )
152
+ # Show a note if the pattern repeats 3 or more times
153
+ count_query = f' - Repeated {count} times' if count >= 3 else ''
154
+ # Format the output line with line number and highlighted matches
155
+ lines.append(
156
+ click.style(f'Line {num}{count_query}: ', fg='magenta') + highlighted
157
+ )
158
+
159
+ # If any matching lines were found
160
+ if lines:
161
+ # Add the file and its matching lines to the results
162
+ matches[click.style(file_label, fg='cyan')] = lines
122
163
  except Exception:
123
164
  return
124
165
 
125
- # Skip file if any filter condition is met
126
- if self.conditions(p_resolved, 'content'):
127
- return
128
-
129
- line_matches = []
130
- try:
131
- # Read file line by line
132
- with open(p_resolved, 'r', encoding='utf-8', errors='ignore') as f:
133
- for num, line in enumerate(f, 1):
134
- line = line.strip()
135
- if pattern.search(line):
136
- # Count occurrences of query in line
137
- count = len(list(pattern.finditer(line)))
138
- highlighted_line = pattern.sub(lambda m: click.style(m.group(), fg='green'), line)
139
- count_query = f'- Repeated {count} times' if count >= 3 else ''
140
- line_matches.append(
141
- click.style(f'Line {num}{count_query}: ', fg='magenta') + highlighted_line
142
- )
143
- except Exception:
144
- return # Skip unreadable files
145
-
146
- if line_matches:
147
- file_label = str(p_resolved) if self.full_path else str(file_path)
148
- # Use colored file path as key
149
- matches[click.style(file_label, fg='cyan')] = line_matches
166
+ # Filter files before processing
167
+ files_to_process = {
168
+ p for p in self.base_path.rglob('*') if not self.should_skip(p, 'content')
169
+ }
150
170
 
151
- # Create a list of files to process (filter out inaccessible ones early)
152
- files_to_process = [file for file in base_path.rglob('*')]
153
- with ThreadPoolExecutor() as executor:
171
+ with ThreadPoolExecutor(max_workers=8) as executor:
154
172
  executor.map(process_file, files_to_process)
155
173
 
156
174
  self.result = matches
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pseek
3
- Version: 2.1.1
3
+ Version: 2.1.3
4
4
  Summary: Pseek is a Python library to search files, folders, and text
5
5
  Home-page: https://github.com/ArianN8610/pysearch
6
6
  Author: Arian
@@ -100,3 +100,4 @@ pseek "error\d+" --regex
100
100
  | `--word` | Match the whole word only |
101
101
  | `--max-size`, `--min-size` | Specify maximum and minimum sizes for files and directories |
102
102
  | `--full-path` | Display full path of files and directories |
103
+ | `--no-content` | Only display files path for content search |
@@ -5,7 +5,7 @@ with open("README.md", encoding="utf-8") as f:
5
5
 
6
6
  setup(
7
7
  name="pseek",
8
- version="2.1.1",
8
+ version="2.1.3",
9
9
  author="Arian",
10
10
  author_email="ariannasiri86@gmail.com",
11
11
  description="Pseek is a Python library to search files, folders, and text",
File without changes
File without changes
File without changes
File without changes
File without changes