pseek 2.1.1__tar.gz → 2.1.2__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.2
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
@@ -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')
@@ -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
 
@@ -22,21 +23,21 @@ class Search:
22
23
  def __init__(self, base_path, query, case_sensitive, ext, exclude_ext, regex, include, exclude, whole_word,
23
24
  max_size, min_size, full_path):
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
37
38
  self.result = None
38
39
 
39
- def conditions(self, p_resolved: Path, search_type: str) -> bool:
40
+ def should_skip(self, p_resolved: Path, search_type: str) -> bool:
40
41
  """
41
42
  Check whether the file/directory should be skipped based on various filters.
42
43
  Returns True if the path should be skipped.
@@ -47,15 +48,10 @@ class Search:
47
48
  # If path is inaccessible, skip it.
48
49
  return True
49
50
 
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}
51
+ file_ext = p_resolved.suffix[1:].lower()
53
52
 
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)) \
53
+ if (self.include and not any(p_resolved.is_relative_to(inc) for inc in self.include)) \
54
+ or (self.exclude and any(p_resolved.is_relative_to(exc) for exc in self.exclude)) \
59
55
  or (self.ext and file_ext not in self.ext) \
60
56
  or (self.exclude_ext and file_ext in self.exclude_ext) \
61
57
  or (search_type == 'content' and file_ext in EXCLUDED_EXTENSIONS) \
@@ -69,18 +65,11 @@ class Search:
69
65
 
70
66
  def search(self, search_type: str):
71
67
  """Main search function. search_type can be 'file', 'directory' or 'content'"""
72
- base_path = Path(self.base_path)
73
68
  query = self.query
74
69
 
75
70
  # Prepare query: escape if not regex
76
71
  if not self.regex:
77
72
  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
73
 
85
74
  # Apply whole-word matching only if not using regex (or if desired behavior is defined)
86
75
  if self.whole_word and not self.regex:
@@ -97,13 +86,13 @@ class Search:
97
86
 
98
87
  if search_type in ('file', 'directory'):
99
88
  matches = []
100
- for p in base_path.rglob('*'):
89
+ for p in self.base_path.rglob('*'):
101
90
  try:
102
91
  p_resolved = p.resolve()
103
92
  except Exception:
104
93
  continue
105
94
  # 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):
95
+ if self.should_skip(p_resolved, search_type) or not pattern.search(p.name):
107
96
  continue
108
97
 
109
98
  # Highlight matched query in the name
@@ -111,46 +100,68 @@ class Search:
111
100
  # Choose parent path based on full_path flag
112
101
  p_parent = p_resolved.parent if self.full_path else p.parent
113
102
  matches.append(f'{p_parent}\\{highlighted_name}')
114
- else: # For content search
103
+ else: # content search
115
104
  # Use dictionary: key: file path (colored), value: list of line matches
116
105
  matches = {}
106
+ # Compile binary version of the pattern
107
+ pattern_bytes = re.compile(pattern.pattern.encode('utf-8'))
117
108
 
118
109
  def process_file(file_path: Path):
119
110
  """Process a single file for content search"""
120
111
  try:
121
- p_resolved = file_path.resolve()
112
+ # Avoid empty files for mmap
113
+ if file_path.stat().st_size == 0:
114
+ return
115
+
116
+ # Open the file in binary read mode
117
+ with open(file_path, 'rb') as f:
118
+ # Memory-map the file for efficient access
119
+ with mmap.mmap(f.fileno(), 0, access=mmap.ACCESS_READ) as mm:
120
+ # Search for the pattern in the entire file as bytes
121
+ if pattern_bytes.search(mm):
122
+ lines = []
123
+ mm.seek(0) # Move the cursor to the beginning of the file
124
+
125
+ # Iterate over each line in the file
126
+ for num, line in enumerate(iter(mm.readline, b''), 1):
127
+ try:
128
+ # Decode the binary line as UTF-8 and strip whitespace
129
+ line_decoded = line.decode('utf-8').strip()
130
+ except UnicodeDecodeError:
131
+ # Skip lines that can't be decoded
132
+ continue
133
+
134
+ # If the pattern matches in the decoded line
135
+ if pattern_list := list(pattern.finditer(line_decoded)):
136
+ # Count how many times the pattern appears in the line
137
+ count = len(pattern_list)
138
+ # Highlight the matching parts in green
139
+ highlighted = pattern.sub(
140
+ lambda m: click.style(m.group(), fg='green'),
141
+ line_decoded
142
+ )
143
+ # Show a note if the pattern repeats 3 or more times
144
+ count_query = f' - Repeated {count} times' if count >= 3 else ''
145
+ # Format the output line with line number and highlighted matches
146
+ lines.append(
147
+ click.style(f'Line {num}{count_query}: ', fg='magenta') + highlighted
148
+ )
149
+
150
+ # If any matching lines were found
151
+ if lines:
152
+ # Choose the file path format based on the full_path setting
153
+ file_label = str(file_path.resolve()) if self.full_path else str(file_path)
154
+ # Add the file and its matching lines to the results
155
+ matches[click.style(file_label, fg='cyan')] = lines
122
156
  except Exception:
123
157
  return
124
158
 
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
159
+ # Filter files before processing
160
+ files_to_process = {
161
+ p for p in self.base_path.rglob('*') if not self.should_skip(p, 'content')
162
+ }
150
163
 
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:
164
+ with ThreadPoolExecutor(max_workers=8) as executor:
154
165
  executor.map(process_file, files_to_process)
155
166
 
156
167
  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.2
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
@@ -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.2",
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
File without changes