Skip to content

Commit 64255e1

Browse files
authored
0.4.0 improvements (#25)
* Add license commands to allow viewers to read license **terms and conditions** and **warranty**. Also, added status message because who doesn't like to know what's really going on? * Showing CLI results in panels: ![Screenshot From 2025-05-09 05-49-24](https://github.com/user-attachments/assets/3d17a3bb-ce36-4158-bf5f-417ec3e1b98c)
2 parents c3828e8 + f856462 commit 64255e1

8 files changed

Lines changed: 803 additions & 65 deletions

File tree

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
652652
If the program does terminal interaction, make it output a short
653653
notice like this when it starts in an interactive mode:
654654

655-
searchcode-cli Copyright (C) 2023 Richard Mwewa
655+
<name> Copyright (C) <year> <name of author>
656656
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
657657
This is free software, and you are welcome to redistribute it
658658
under certain conditions; type `show c' for details.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<p align="center"><img src="https://github.com/user-attachments/assets/d28934e1-a1ab-4b6e-9d12-d64067a65a60"><br>Python SDK and CLI utility for <a href="https://searchcode.com">Searchcode</a>.<br><i>Search 75 billion lines of code from 40 million projects</i></p>
1+
<p align="center"><img src="https://github.com/user-attachments/assets/bce54aa5-7f20-435d-a419-72e8e779105a"><br>Python SDK and CLI utility for <a href="https://searchcode.com">Searchcode</a>.<br><i>Search 75 billion lines of code from 40 million projects</i></p>
22
<p align="center"></p>
33

44
```commandline

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "searchcode"
3-
version = "0.3.1"
3+
version = "0.4.0"
44
description = "Simple, comprehensive code search."
55
authors = ["Ritchie Mwewa <rly0nheart@duck.com>"]
66
license = "GPLv3+"
@@ -31,5 +31,5 @@ requires = ["poetry-core"]
3131
build-backend = "poetry.core.masonry.api"
3232

3333
[tool.poetry.scripts]
34-
sc = "searchcode.cli:cli" # shortened command
34+
sc = "searchcode.cli:cli"
3535
searchcode = "searchcode.cli:cli"

searchcode/__init__.py

Lines changed: 594 additions & 0 deletions
Large diffs are not rendered by default.

searchcode/api.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
"""
2+
Copyright (C) 2024 Ritchie Mwewa
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
"""
17+
118
import typing as t
219
from platform import python_version, platform
320

searchcode/cli.py

Lines changed: 154 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,77 @@
1+
"""
2+
Copyright (C) 2024 Ritchie Mwewa
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
"""
17+
18+
import os
19+
import subprocess
120
import typing as t
221

322
import rich_click as click
4-
from rich import print as rprint, box
23+
from rich import box
24+
from rich.console import Console
25+
from rich.panel import Panel
526
from rich.pretty import pprint
627
from rich.syntax import Syntax
7-
from rich.table import Table
828
from whats_that_code.election import guess_language_all_methods
929

30+
from . import License, __pkg__, __version__
1031
from .api import Searchcode
1132

12-
sc = Searchcode(user_agent="searchCode-sdk/cli")
33+
sc = Searchcode(user_agent=f"{__pkg__}-sdk/cli")
1334

1435
__all__ = ["cli"]
1536

37+
console = Console(highlight=True)
38+
1639

1740
@click.group()
41+
@click.version_option(version=__version__, package_name=__pkg__)
1842
def cli():
1943
"""
2044
Searchcode
2145
2246
Simple, comprehensive code search.
2347
"""
24-
...
48+
49+
__update_window_title("Source code search engine.")
50+
51+
52+
@cli.command("license")
53+
@click.option("--conditions", help="License terms and conditions.", is_flag=True)
54+
@click.option("--warranty", help="License warranty.", is_flag=True)
55+
def licence(conditions: t.Optional[bool], warranty: t.Optional[bool]):
56+
"""
57+
Show license
58+
"""
59+
__clear_screen()
60+
__update_window_title(
61+
text="Terms and Conditions" if conditions else "Warranty" if warranty else None
62+
)
63+
if conditions:
64+
console.print(
65+
License.terms_and_conditions,
66+
justify="center",
67+
style="on #272822", # monokai themed background :)
68+
)
69+
if warranty:
70+
console.print(
71+
License.warranty,
72+
justify="center",
73+
style="on #272822",
74+
)
2575

2676

2777
@cli.command()
@@ -31,7 +81,7 @@ def cli():
3181
"--page",
3282
type=int,
3383
default=0,
34-
help="Start page number (defaults to 0).",
84+
help="Start page (defaults to 0).",
3585
)
3686
@click.option(
3787
"--per-page",
@@ -76,33 +126,37 @@ def search(
76126
callback: t.Optional[str] = None,
77127
):
78128
"""
79-
Query the code index and (returns 100 results by default).
129+
Query the code index (returns 100 results by default).
80130
81-
e.g., searchcode search "import module"
131+
e.g., sc search "import module"
82132
"""
83-
languages = languages.split(",") if languages else None
84-
sources = sources.split(",") if sources else None
133+
__clear_screen()
134+
__update_window_title(text=query)
85135

86-
results = sc.search(
87-
query=query,
88-
page=page,
89-
per_page=per_page,
90-
languages=languages,
91-
sources=sources,
92-
lines_of_code_lt=lines_of_code_lt,
93-
lines_of_code_gt=lines_of_code_gt,
94-
callback=callback,
95-
)
136+
with console.status(f"Querying code index with [green]{query}[/]"):
137+
languages = languages.split(",") if languages else None
138+
sources = sources.split(",") if sources else None
96139

97-
(
98-
__print_jsonp(jsonp=results)
99-
if callback
100-
else (
101-
pprint(results)
102-
if pretty
103-
else __print_table(records=results["results"], ignore_keys=["lines"])
140+
results = sc.search(
141+
query=query,
142+
page=page,
143+
per_page=per_page,
144+
languages=languages,
145+
sources=sources,
146+
lines_of_code_lt=lines_of_code_lt,
147+
lines_of_code_gt=lines_of_code_gt,
148+
callback=callback,
149+
)
150+
151+
(
152+
__print_jsonp(jsonp=results)
153+
if callback
154+
else (
155+
pprint(results)
156+
if pretty
157+
else __print_panels(data=results.get("results"))
158+
)
104159
)
105-
)
106160

107161

108162
@cli.command()
@@ -111,55 +165,94 @@ def code(id: int):
111165
"""
112166
Get the raw data from a code file.
113167
114-
e.g., searchcode code 4061576
168+
e.g., sc code 4061576
169+
"""
170+
__clear_screen()
171+
__update_window_title(text=str(id))
172+
with console.status(f"Retrieving code data for [cyan]{id}[/]") as status:
173+
code_data = sc.code(id)
174+
if code_data:
175+
status.update("Determining code language")
176+
language = guess_language_all_methods(code=code_data)
177+
syntax = Syntax(code=code_data, lexer=language, line_numbers=True)
178+
console.print(syntax)
179+
180+
181+
def __print_jsonp(jsonp: str) -> None:
182+
"""
183+
Pretty-prints a raw JSONP string.
184+
185+
:param jsonp: A complete JSONP string.
115186
"""
116-
code_data = sc.code(id)
117-
if code_data:
118-
language = guess_language_all_methods(code=code_data)
119-
syntax = Syntax(code=code_data, lexer=language, line_numbers=True)
120-
rprint(syntax)
187+
syntax = Syntax(jsonp, "text", line_numbers=True)
188+
console.print(syntax)
121189

122190

123-
def __print_table(records: t.List[t.Dict], ignore_keys: t.List[str] = None) -> None:
191+
def __print_panels(data: t.List[t.Dict]):
124192
"""
125-
Creates a rich table from a list of dict objects,
126-
ignoring specified keys.
193+
Render a list of code records as rich panels with syntax highlighting.
194+
Line numbers are preserved and displayed alongside code content.
127195
128-
:param records: List of dict objects.
129-
:param ignore_keys: List of keys to exclude from the table.
130-
:return: None. Prints the table using rich.
196+
:param data: A list of dictionaries, where each dictionary represents a code record
131197
"""
132-
if not records:
133-
raise ValueError("Data must be a non-empty list of dict objects.")
134198

135-
ignore_keys = ignore_keys or []
199+
def extract_code_string_with_linenumbers(lines_dict: t.Dict[str, str]) -> str:
200+
"""
201+
Convert a dictionary of line_number: code_line into a single
202+
multiline string sorted by line number.
136203
137-
# Collect all unique keys across all records, excluding ignored ones
138-
all_keys = set()
139-
for record in records:
140-
all_keys.update(key for key in record.keys() if key not in ignore_keys)
204+
Each line is right-aligned to maintain visual alignment in output.
141205
142-
columns = sorted(all_keys)
206+
:param lines_dict: Dictionary where keys are line numbers (as strings) and values are lines of code.
207+
:return: Multiline string with original line numbers included.
208+
"""
209+
sorted_lines = sorted(lines_dict.items(), key=lambda x: int(x[0]))
210+
numbered_lines = [
211+
f"{line_no.rjust(4)} {line.rstrip()}" for line_no, line in sorted_lines
212+
]
213+
return "\n".join(numbered_lines)
143214

144-
table = Table(box=box.ROUNDED, highlight=True, header_style="bold")
215+
for item in data:
216+
filename = item.get("filename", "Unknown")
217+
repo = item.get("repo", "Unknown")
218+
language = item.get("language", "text")
219+
lines_count = item.get("linescount", "??")
145220

146-
for index, column in enumerate(columns):
147-
style = "dim" if index == 0 else None
148-
table.add_column(column.capitalize(), style=style)
221+
code_string = extract_code_string_with_linenumbers(
222+
lines_dict=item.get("lines", {})
223+
)
149224

150-
for record in records:
151-
data = record
152-
row = [str(data.get(column, "")) for column in columns]
153-
table.add_row(*row)
225+
syntax = Syntax(
226+
code=code_string, lexer=language, word_wrap=False, indent_guides=True
227+
)
154228

155-
rprint(table)
229+
panel = Panel(
230+
renderable=syntax,
231+
box=box.ROUNDED,
232+
title=f"[bold]{filename}[/] ([blue]{repo}[/]) {language} ⸱ [cyan]{lines_count}[/] lines",
233+
highlight=True,
234+
)
156235

236+
console.print(panel)
157237

158-
def __print_jsonp(jsonp: str) -> None:
238+
239+
def __update_window_title(text: str):
159240
"""
160-
Pretty-prints a raw JSONP string.
241+
Update the current window title with the specified text.
161242
162-
:param jsonp: A complete JSONP string.
243+
:param text: Text to update the window with.
163244
"""
164-
syntax = Syntax(jsonp, "text", line_numbers=True)
165-
rprint(syntax)
245+
console.set_window_title(f"{__pkg__.capitalize()} - {text}")
246+
247+
248+
def __clear_screen():
249+
"""
250+
Clear the screen.
251+
252+
Not using console.clear() because it doesn't really clear the screen.
253+
It instead creates a space between the items on top and below,
254+
then moves the cursor to the items on the bottom, thus creating the illusion of a "cleared screen".
255+
256+
Using subprocess might be a bad idea, but I'm yet to see how bad of an idea that is.
257+
"""
258+
subprocess.run(["cls" if os.name == "nt" else "clear"])

searchcode/filters.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
"""
2+
Copyright (C) 2024 Ritchie Mwewa
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
"""
17+
118
import typing as t
219

320

tests/test_searchcode.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,20 @@
1+
"""
2+
Copyright (C) 2024 Ritchie Mwewa
3+
4+
This program is free software: you can redistribute it and/or modify
5+
it under the terms of the GNU General Public License as published by
6+
the Free Software Foundation, either version 3 of the License, or
7+
(at your option) any later version.
8+
9+
This program is distributed in the hope that it will be useful,
10+
but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
GNU General Public License for more details.
13+
14+
You should have received a copy of the GNU General Public License
15+
along with this program. If not, see <https://www.gnu.org/licenses/>.
16+
"""
17+
118
from searchcode import Searchcode
219

320
sc = Searchcode(user_agent="Pytest")

0 commit comments

Comments
 (0)