Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:

searchcode-cli Copyright (C) 2023 Richard Mwewa
<name> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<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>
<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>
<p align="center"></p>

```commandline
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "searchcode"
version = "0.3.1"
version = "0.4.0"
description = "Simple, comprehensive code search."
authors = ["Ritchie Mwewa <rly0nheart@duck.com>"]
license = "GPLv3+"
Expand Down Expand Up @@ -31,5 +31,5 @@ requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

[tool.poetry.scripts]
sc = "searchcode.cli:cli" # shortened command
sc = "searchcode.cli:cli"
searchcode = "searchcode.cli:cli"
594 changes: 594 additions & 0 deletions searchcode/__init__.py

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions searchcode/api.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
"""
Copyright (C) 2024 Ritchie Mwewa

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""

import typing as t
from platform import python_version, platform

Expand Down
215 changes: 154 additions & 61 deletions searchcode/cli.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,77 @@
"""
Copyright (C) 2024 Ritchie Mwewa

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""

import os
import subprocess
import typing as t

import rich_click as click
from rich import print as rprint, box
from rich import box
from rich.console import Console
from rich.panel import Panel
from rich.pretty import pprint
from rich.syntax import Syntax
from rich.table import Table
from whats_that_code.election import guess_language_all_methods

from . import License, __pkg__, __version__
from .api import Searchcode

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

__all__ = ["cli"]

console = Console(highlight=True)


@click.group()
@click.version_option(version=__version__, package_name=__pkg__)
def cli():
"""
Searchcode

Simple, comprehensive code search.
"""
...

__update_window_title("Source code search engine.")


@cli.command("license")
@click.option("--conditions", help="License terms and conditions.", is_flag=True)
@click.option("--warranty", help="License warranty.", is_flag=True)
def licence(conditions: t.Optional[bool], warranty: t.Optional[bool]):
"""
Show license
"""
__clear_screen()
__update_window_title(
text="Terms and Conditions" if conditions else "Warranty" if warranty else None
)
if conditions:
console.print(
License.terms_and_conditions,
justify="center",
style="on #272822", # monokai themed background :)
)
if warranty:
console.print(
License.warranty,
justify="center",
style="on #272822",
)


@cli.command()
Expand All @@ -31,7 +81,7 @@ def cli():
"--page",
type=int,
default=0,
help="Start page number (defaults to 0).",
help="Start page (defaults to 0).",
)
@click.option(
"--per-page",
Expand Down Expand Up @@ -76,33 +126,37 @@ def search(
callback: t.Optional[str] = None,
):
"""
Query the code index and (returns 100 results by default).
Query the code index (returns 100 results by default).

e.g., searchcode search "import module"
e.g., sc search "import module"
"""
languages = languages.split(",") if languages else None
sources = sources.split(",") if sources else None
__clear_screen()
__update_window_title(text=query)

results = sc.search(
query=query,
page=page,
per_page=per_page,
languages=languages,
sources=sources,
lines_of_code_lt=lines_of_code_lt,
lines_of_code_gt=lines_of_code_gt,
callback=callback,
)
with console.status(f"Querying code index with [green]{query}[/]"):
languages = languages.split(",") if languages else None
sources = sources.split(",") if sources else None

(
__print_jsonp(jsonp=results)
if callback
else (
pprint(results)
if pretty
else __print_table(records=results["results"], ignore_keys=["lines"])
results = sc.search(
query=query,
page=page,
per_page=per_page,
languages=languages,
sources=sources,
lines_of_code_lt=lines_of_code_lt,
lines_of_code_gt=lines_of_code_gt,
callback=callback,
)

(
__print_jsonp(jsonp=results)
if callback
else (
pprint(results)
if pretty
else __print_panels(data=results.get("results"))
)
)
)


@cli.command()
Expand All @@ -111,55 +165,94 @@ def code(id: int):
"""
Get the raw data from a code file.

e.g., searchcode code 4061576
e.g., sc code 4061576
"""
__clear_screen()
__update_window_title(text=str(id))
with console.status(f"Retrieving code data for [cyan]{id}[/]") as status:
code_data = sc.code(id)
if code_data:
status.update("Determining code language")
language = guess_language_all_methods(code=code_data)
syntax = Syntax(code=code_data, lexer=language, line_numbers=True)
console.print(syntax)


def __print_jsonp(jsonp: str) -> None:
"""
Pretty-prints a raw JSONP string.

:param jsonp: A complete JSONP string.
"""
code_data = sc.code(id)
if code_data:
language = guess_language_all_methods(code=code_data)
syntax = Syntax(code=code_data, lexer=language, line_numbers=True)
rprint(syntax)
syntax = Syntax(jsonp, "text", line_numbers=True)
console.print(syntax)


def __print_table(records: t.List[t.Dict], ignore_keys: t.List[str] = None) -> None:
def __print_panels(data: t.List[t.Dict]):
"""
Creates a rich table from a list of dict objects,
ignoring specified keys.
Render a list of code records as rich panels with syntax highlighting.
Line numbers are preserved and displayed alongside code content.

:param records: List of dict objects.
:param ignore_keys: List of keys to exclude from the table.
:return: None. Prints the table using rich.
:param data: A list of dictionaries, where each dictionary represents a code record
"""
if not records:
raise ValueError("Data must be a non-empty list of dict objects.")

ignore_keys = ignore_keys or []
def extract_code_string_with_linenumbers(lines_dict: t.Dict[str, str]) -> str:
"""
Convert a dictionary of line_number: code_line into a single
multiline string sorted by line number.

# Collect all unique keys across all records, excluding ignored ones
all_keys = set()
for record in records:
all_keys.update(key for key in record.keys() if key not in ignore_keys)
Each line is right-aligned to maintain visual alignment in output.

columns = sorted(all_keys)
:param lines_dict: Dictionary where keys are line numbers (as strings) and values are lines of code.
:return: Multiline string with original line numbers included.
"""
sorted_lines = sorted(lines_dict.items(), key=lambda x: int(x[0]))
numbered_lines = [
f"{line_no.rjust(4)} {line.rstrip()}" for line_no, line in sorted_lines
]
return "\n".join(numbered_lines)

table = Table(box=box.ROUNDED, highlight=True, header_style="bold")
for item in data:
filename = item.get("filename", "Unknown")
repo = item.get("repo", "Unknown")
language = item.get("language", "text")
lines_count = item.get("linescount", "??")

for index, column in enumerate(columns):
style = "dim" if index == 0 else None
table.add_column(column.capitalize(), style=style)
code_string = extract_code_string_with_linenumbers(
lines_dict=item.get("lines", {})
)

for record in records:
data = record
row = [str(data.get(column, "")) for column in columns]
table.add_row(*row)
syntax = Syntax(
code=code_string, lexer=language, word_wrap=False, indent_guides=True
)

rprint(table)
panel = Panel(
renderable=syntax,
box=box.ROUNDED,
title=f"[bold]{filename}[/] ([blue]{repo}[/]) {language} ⸱ [cyan]{lines_count}[/] lines",
highlight=True,
)

console.print(panel)

def __print_jsonp(jsonp: str) -> None:

def __update_window_title(text: str):
"""
Pretty-prints a raw JSONP string.
Update the current window title with the specified text.

:param jsonp: A complete JSONP string.
:param text: Text to update the window with.
"""
syntax = Syntax(jsonp, "text", line_numbers=True)
rprint(syntax)
console.set_window_title(f"{__pkg__.capitalize()} - {text}")


def __clear_screen():
"""
Clear the screen.

Not using console.clear() because it doesn't really clear the screen.
It instead creates a space between the items on top and below,
then moves the cursor to the items on the bottom, thus creating the illusion of a "cleared screen".

Using subprocess might be a bad idea, but I'm yet to see how bad of an idea that is.
"""
subprocess.run(["cls" if os.name == "nt" else "clear"])
17 changes: 17 additions & 0 deletions searchcode/filters.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
"""
Copyright (C) 2024 Ritchie Mwewa

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""

import typing as t


Expand Down
17 changes: 17 additions & 0 deletions tests/test_searchcode.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
"""
Copyright (C) 2024 Ritchie Mwewa

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""

from searchcode import Searchcode

sc = Searchcode(user_agent="Pytest")
Expand Down