pseek 1.0.2__tar.gz → 2.0.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pseek
3
- Version: 1.0.2
3
+ Version: 2.0.0
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
@@ -1,8 +1,5 @@
1
- import re
2
1
  import click
3
-
4
- from .utils import display_results
5
- from .searcher import search_in_names, search_in_file_contents
2
+ from .searcher import Search
6
3
 
7
4
 
8
5
  @click.command()
@@ -28,36 +25,22 @@ def search(query, path, file, directory, content, case_sensitive, ext, regex, in
28
25
  max_size, min_size, full_path):
29
26
  """Search for files, directories, and content based on the query."""
30
27
 
31
- if regex:
32
- try:
33
- re.compile(query) # Ensure valid regex
34
- except re.error:
35
- click.echo(click.style('Invalid regex pattern: ', fg='red') + query)
36
- return
37
-
38
28
  # Enable all search types if none are explicitly selected
39
29
  if not any([file, directory, content]):
40
30
  file = directory = content = True
41
31
 
42
- results = []
32
+ count_results = 0
33
+ search_class = Search(path, query, case_sensitive, ext, regex, include, exclude, word, max_size, min_size,
34
+ full_path)
43
35
 
44
36
  if file:
45
- filename_results = search_in_names(path, query, case_sensitive, regex, include, exclude, word, max_size,
46
- min_size, full_path, ext)
47
- display_results(filename_results, 'Files', 'file')
48
- results.extend(filename_results)
37
+ count_results += search_class.search('file').echo('Files', 'file')
49
38
  if directory and not ext:
50
- dir_results = search_in_names(path, query, case_sensitive, regex, include, exclude, word, max_size, min_size,
51
- full_path, is_file=False)
52
- display_results(dir_results, 'Directories', 'directory')
53
- results.extend(dir_results)
39
+ count_results += search_class.search('directory').echo('Directories', 'directory')
54
40
  if content:
55
- content_results = search_in_file_contents(path, query, case_sensitive, ext, regex, include, exclude, word,
56
- max_size, min_size, full_path)
57
- display_results(content_results, 'Contents', 'content')
58
- results.extend(content_results)
41
+ count_results += search_class.search('content').echo('Contents', 'content')
59
42
 
60
- message = f'\nTotal results: {len(results)}' if results else 'No results found'
43
+ message = f'\nTotal results: {count_results}' if count_results else 'No results found'
61
44
  click.echo(click.style(message, fg='red')) # Display total of results
62
45
 
63
46
 
@@ -0,0 +1,146 @@
1
+ import re
2
+ import sys
3
+ import click
4
+ from pathlib import Path
5
+ from concurrent.futures import ThreadPoolExecutor
6
+
7
+
8
+ class Search:
9
+ def __init__(self, base_path, query, case_sensitive, ext, regex, include, exclude, whole_word,
10
+ max_size, min_size, full_path):
11
+ self.base_path = base_path
12
+ self.query = query
13
+ self.case_sensitive = case_sensitive
14
+ self.ext = ext
15
+ self.regex = regex
16
+ self.include = include
17
+ self.exclude = exclude
18
+ self.whole_word = whole_word
19
+ self.max_size = max_size
20
+ self.min_size = min_size
21
+ self.full_path = full_path
22
+
23
+ self.result = []
24
+
25
+ def conditions(self, p_resolved: Path, search_type: str) -> bool:
26
+ """Check the correctness of path based on data"""
27
+
28
+ try:
29
+ p_size_mb = p_resolved.stat().st_size / 1_048_576 # Convert to MB
30
+ except OSError:
31
+ return True # if path cannot be accessible
32
+
33
+ # Convert include and exclude paths to `Path`
34
+ include_paths = {Path(p).resolve() for p in self.include}
35
+ exclude_paths = {Path(p).resolve() for p in self.exclude}
36
+
37
+ if (
38
+ # Filter by include/exclude directories and files
39
+ (self.include and not any(p_resolved.is_relative_to(inc) for inc in include_paths))
40
+ or (self.exclude and any(p_resolved.is_relative_to(exc) for exc in exclude_paths))
41
+ # Filter by extension
42
+ or (self.ext and p_resolved.suffix[1:] not in self.ext)
43
+ # Filter by file size
44
+ or (self.max_size and p_size_mb > self.max_size)
45
+ or (self.min_size and p_size_mb < self.min_size)
46
+ # Filter by path type
47
+ or ((search_type == 'file' or search_type == 'content') and not p_resolved.is_file())
48
+ or (search_type == 'directory' and not p_resolved.is_dir())
49
+ ):
50
+ return True
51
+
52
+ return False
53
+
54
+ def search(self, search_type: str):
55
+ """Search for paths based on the query"""
56
+
57
+ base_path = Path(self.base_path)
58
+ query = self.query
59
+
60
+ if not self.regex:
61
+ query = re.escape(query) # If regex is disabled, convert query to plain text
62
+ else:
63
+ try:
64
+ re.compile(query) # Ensure valid regex
65
+ except re.error:
66
+ click.echo(click.style('Invalid regex pattern: ', fg='red') + query)
67
+ sys.exit(1) # Exit the program with error code
68
+
69
+ if self.whole_word:
70
+ query = rf'\b{query}\b' # Ensure search for whole words
71
+
72
+ flags = 0 if self.case_sensitive else re.IGNORECASE # Adjust case sensitivity
73
+
74
+ if search_type == 'file' or search_type == 'directory':
75
+ matches = []
76
+ for p in base_path.rglob('*'):
77
+ p_resolved = p.resolve() # Actual file or folder path
78
+ if self.conditions(p_resolved, search_type) or (not re.search(query, p.name, flags)):
79
+ continue
80
+
81
+ # Specify the found part
82
+ highlighted_name = re.sub(query, lambda m: click.style(m.group(), fg='green'), p.name, flags=flags)
83
+ # Get full path if full_path is True
84
+ p_parent = p_resolved.parent if self.full_path else p.parent
85
+
86
+ matches.append(f'{p_parent}\\{highlighted_name}')
87
+ else:
88
+ matches = dict()
89
+ def process_file(file_path):
90
+ """Processes a single file and searches for the query inside its content."""
91
+ p_resolved = file_path.resolve()
92
+ line_matches = []
93
+
94
+ try:
95
+ for num, line in enumerate(p_resolved.read_text(encoding='utf-8', errors='ignore').splitlines(), 1):
96
+ line = line.strip()
97
+ if re.search(query, line, flags):
98
+ # Count query in each line
99
+ count_query = len(list(re.finditer(query, line, flags)))
100
+ highlighted_name = re.sub(query, lambda m: click.style(m.group(), fg='green'), line,
101
+ flags=flags)
102
+ count_query = f'- Repeated {count_query} times' if count_query >= 3 else ''
103
+
104
+ line_matches.append(
105
+ click.style(f'Line {num}{count_query}: ', fg='magenta')
106
+ + highlighted_name
107
+ )
108
+ except Exception:
109
+ return # Ignore unreadable files
110
+
111
+ if line_matches:
112
+ file_path = p_resolved if self.full_path else file_path
113
+ matches.update({click.style(file_path, fg='cyan'): line_matches})
114
+
115
+ # Use multi-threading for faster processing
116
+ with ThreadPoolExecutor() as executor:
117
+ executor.map(
118
+ process_file,
119
+ (file for file in base_path.rglob('*') if not self.conditions(file, 'content'))
120
+ )
121
+
122
+ self.result = matches
123
+ return self
124
+
125
+ def echo(self, title: str, result_name: str) -> int:
126
+ """Display search results"""
127
+ count_result = 0
128
+
129
+ if self.result:
130
+ click.echo(click.style(f'\n{title}:\n', fg='yellow'))
131
+
132
+ if isinstance(self.result, dict):
133
+ # Display for content results
134
+ for key, value in self.result.items():
135
+ click.echo(key)
136
+ click.echo('\n'.join(value) + '\n')
137
+ count_result += len(value)
138
+ else:
139
+ # Display for directory and file results
140
+ count_result = len(self.result)
141
+ click.echo('\n'.join(self.result))
142
+
143
+ if count_result >= 3:
144
+ click.echo(click.style(f'\n{count_result} results found for {result_name}', fg='blue'))
145
+
146
+ return count_result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: pseek
3
- Version: 1.0.2
3
+ Version: 2.0.0
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
@@ -4,7 +4,6 @@ setup.py
4
4
  pseek/__init__.py
5
5
  pseek/cli.py
6
6
  pseek/searcher.py
7
- pseek/utils.py
8
7
  pseek.egg-info/PKG-INFO
9
8
  pseek.egg-info/SOURCES.txt
10
9
  pseek.egg-info/dependency_links.txt
@@ -5,7 +5,7 @@ with open("README.md", encoding="utf-8") as f:
5
5
 
6
6
  setup(
7
7
  name="pseek",
8
- version="1.0.2",
8
+ version="2.0.0",
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",
@@ -1,109 +0,0 @@
1
- import re
2
- import click
3
- from pathlib import Path
4
- from concurrent.futures import ThreadPoolExecutor
5
-
6
- from .utils import highlight_matches, safe_is_file
7
-
8
-
9
- def search_in_names(base_path, query, case_sensitive, regex, include, exclude, whole_word, max_size, min_size,
10
- full_path, ext=tuple(), is_file=True):
11
- """Search for file and folder names"""
12
- base_path = Path(base_path)
13
- matches = []
14
-
15
- if not regex:
16
- query = re.escape(query) # If regex is disabled, convert query to plain text
17
-
18
- if whole_word:
19
- query = rf'\b{query}\b' # Ensure search for whole words
20
-
21
- flags = 0 if case_sensitive else re.IGNORECASE # Adjust case sensitivity
22
-
23
- # Convert include and exclude paths to `Path`
24
- include_paths = {Path(p).resolve() for p in include}
25
- exclude_paths = {Path(p).resolve() for p in exclude}
26
-
27
- for p in base_path.rglob('*'):
28
- p_resolved = p.resolve() # Actual file or folder path
29
- try:
30
- p_size_mb = p_resolved.stat().st_size / 1_048_576 # Convert to MB
31
- except OSError:
32
- continue # if path cannot be accessible
33
-
34
- if (
35
- # If include is given, at least one of them must be present in the name
36
- (include and not any(p_resolved.is_relative_to(inc) for inc in include_paths))
37
- # If exclude is given, none should be in the name
38
- or (exclude and any(p_resolved.is_relative_to(exc) for exc in exclude_paths))
39
- # Prevent unnecessary continuation in case of type mismatch
40
- or (is_file and ext and p.suffix[1:] not in ext)
41
- # Check file or directory size
42
- or (max_size and p_size_mb > max_size)
43
- or (min_size and p_size_mb < min_size)
44
- ):
45
- continue
46
-
47
- if re.search(query, p.name, flags) and ((is_file and p.is_file()) or (not is_file and p.is_dir())):
48
- # Specify the found part
49
- highlighted_name = re.sub(query, lambda m: click.style(m.group(), fg='green'), p.name, flags=flags)
50
- p_parent = p_resolved.parent if full_path else p.parent # Get full path if full_path is True
51
- matches.append(f'{p_parent}\\{highlighted_name}')
52
-
53
- return matches
54
-
55
-
56
- def search_in_file_contents(base_path, query, case_sensitive, ext, regex, include, exclude, whole_word,
57
- max_size, min_size, full_path):
58
- """Search inside file contents"""
59
- base_path = Path(base_path)
60
- matches = []
61
-
62
- if not regex:
63
- query = re.escape(query)
64
-
65
- if whole_word:
66
- query = rf'\b{query}\b'
67
-
68
- flags = 0 if case_sensitive else re.IGNORECASE
69
-
70
- include_paths = {Path(p).resolve() for p in include}
71
- exclude_paths = {Path(p).resolve() for p in exclude}
72
-
73
- def process_file(file_path):
74
- """Processes a single file and searches for the query inside its content."""
75
- p_resolved = file_path.resolve()
76
- p_size_mb = p_resolved.stat().st_size / 1_048_576
77
-
78
- if (
79
- # Filter by include/exclude directories
80
- (include and not any(p_resolved.is_relative_to(inc) for inc in include_paths))
81
- or (exclude and any(p_resolved.is_relative_to(exc) for exc in exclude_paths))
82
- # Filter by extension
83
- or (ext and p_resolved.suffix[1:] not in ext)
84
- # Filter by file size
85
- or (max_size and p_size_mb > max_size)
86
- or (min_size and p_size_mb < min_size)
87
- ):
88
- return
89
-
90
- try:
91
- for num, line in enumerate(p_resolved.read_text(encoding='utf-8', errors='ignore').splitlines(), 1):
92
- line = line.strip()
93
-
94
- if re.search(query, line, flags):
95
- highlighted_snippet, count_query = highlight_matches(line, query, case_sensitive, regex, whole_word)
96
- file_path = p_resolved if full_path else file_path
97
- matches.append(
98
- click.style(file_path, fg='cyan')
99
- + click.style(f' (Line {num}) (Repeated {count_query} time(s)): ', fg='magenta')
100
- + highlighted_snippet
101
- )
102
- except Exception:
103
- return # Ignore unreadable files
104
-
105
- # Use multi-threading for faster processing
106
- with ThreadPoolExecutor() as executor:
107
- executor.map(process_file, (file for file in base_path.rglob('*') if safe_is_file(file)))
108
-
109
- return matches
@@ -1,66 +0,0 @@
1
- import re
2
- import click
3
-
4
-
5
- def highlight_matches(line, query, case_sensitive, regex, whole_word, context=20):
6
- """Highlight matches and truncate long lines with proper handling"""
7
-
8
- flags = 0 if case_sensitive else re.IGNORECASE
9
-
10
- if not regex and not whole_word:
11
- query = re.escape(query) # If regex is disabled, use escape to search for regular text
12
-
13
- # Find all matches
14
- matches = list(re.finditer(query, line, flags))
15
-
16
- if not matches:
17
- return None # If there is no match, return None
18
-
19
- start_positions = [max(0, m.start() - context) for m in matches] # 20 characters before
20
- end_positions = [min(len(line), m.end() + context) for m in matches] # 20 characters after
21
-
22
- # Combine sections close together to avoid repetition
23
- merged_snippets = []
24
- current_start, current_end = start_positions[0], end_positions[0]
25
-
26
- for i in range(1, len(matches)):
27
- if start_positions[i] <= current_end: # If the next section is close, combine them
28
- current_end = max(current_end, end_positions[i])
29
- else:
30
- merged_snippets.append((current_start, current_end))
31
- current_start, current_end = start_positions[i], end_positions[i]
32
-
33
- merged_snippets.append((current_start, current_end)) # Add the last section
34
-
35
- result = []
36
- for start, end in merged_snippets:
37
- snippet = line[start:end]
38
- snippet = re.sub(query, lambda m: click.style(m.group(), fg='green'), snippet, flags=flags)
39
- result.append(snippet)
40
-
41
- final_output = ' ... '.join(result) # If there are multiple results, put `...` between them
42
-
43
- # Add `...` at the beginning and end if needed
44
- if merged_snippets[0][0] > 0:
45
- final_output = '...' + final_output
46
- if merged_snippets[-1][1] < len(line):
47
- final_output += '...'
48
-
49
- return final_output, len(matches) # Count the number of repetitions
50
-
51
-
52
- def safe_is_file(file):
53
- """Check if a file is accessible and return True/False."""
54
- try:
55
- return file.is_file()
56
- except OSError:
57
- return False # Ignore files that cannot be accessed
58
-
59
-
60
- def display_results(results, title, result_name):
61
- """Display search results"""
62
- if results:
63
- click.echo(click.style(f'\n{title}:\n', fg='yellow'))
64
- for result in results:
65
- click.echo(result)
66
- click.echo(click.style(f'\n{len(results)} result(s) found for {result_name}', fg='blue'))
File without changes
File without changes
File without changes
File without changes
File without changes