From d4c5d4af4880236bab77970b337529537813b4c5 Mon Sep 17 00:00:00 2001
From: Ritchie Mwewa <74001397+rly0nheart@users.noreply.github.com>
Date: Tue, 13 May 2025 05:19:59 +0200
Subject: [PATCH 1/6] Fix: broken layout in search results' panels
---
README.md | 19 +++++++++----------
pyproject.toml | 2 +-
src/searchcode/__app.py | 16 ++++++++++------
src/searchcode/__init__.py | 2 +-
4 files changed, 21 insertions(+), 18 deletions(-)
diff --git a/README.md b/README.md
index d9522cb..b772754 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,7 @@
-

Python SDK and CLI utility for Searchcode.
Simple, comprehensive code search.
+
+
Searchcode SDK: Python library and CLI utility for Searchcode.
Simple, comprehensive code search.
----
-
```commandline
searchcode search "import module"
```
@@ -15,7 +14,7 @@ sc search "import module"
```python
from pprint import pprint
-from src.searchcode import Searchcode
+from searchcode import Searchcode
sc = Searchcode(user_agent="My-Searchcode-script")
search = sc.search(query="import module")
@@ -44,7 +43,7 @@ searchcode "import module"
```python
from pprint import pprint
-from src.searchcode import Searchcode
+from searchcode import Searchcode
sc = Searchcode(user_agent="My-Searchcode-script")
search = sc.search(query="import module")
@@ -66,7 +65,7 @@ searchcode "import module" --languages java,javascript
```python
from pprint import pprint
-from src.searchcode import Searchcode
+from searchcode import Searchcode
sc = Searchcode(user_agent="My-Searchcode-script")
search = sc.search(query="import module", languages=["Java", "JavaScript"])
@@ -89,7 +88,7 @@ searchcode "import module" --sources bitbucket,codeplex
```python
from pprint import pprint
-from src.searchcode import Searchcode
+from searchcode import Searchcode
sc = Searchcode(user_agent="My-Searchcode-script")
search = sc.search(query="import module", sources=["BitBucket", "CodePlex"])
@@ -112,7 +111,7 @@ searchcode "import module" --lines-of-code-gt 500 --lines-of-code-lt 1000
```python
from pprint import pprint
-from src.searchcode import Searchcode
+from searchcode import Searchcode
sc = Searchcode(user_agent="My-Searchcode-script")
search = sc.search(query="import module", lines_of_code_gt=500, lines_of_code_lt=1000)
@@ -135,7 +134,7 @@ searchcode "import module" --callback myCallback
```python
from pprint import pprint
-from src.searchcode import Searchcode
+from searchcode import Searchcode
sc = Searchcode(user_agent="My-Searchcode-script")
search = sc.search(query="import module", callback="myCallback")
@@ -216,7 +215,7 @@ searchode code 4061576
```python
-from src.searchcode import Searchcode
+from searchcode import Searchcode
sc = Searchcode(user_agent="My-Searchcode-script")
data = sc.code(4061576)
diff --git a/pyproject.toml b/pyproject.toml
index 142df7e..1fc18d4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "searchcode"
-version = "0.5.1"
+version = "0.5.2"
description = "Simple, comprehensive code search."
authors = ["Ritchie Mwewa "]
license = "GPLv3+"
diff --git a/src/searchcode/__app.py b/src/searchcode/__app.py
index 2c96917..213bcd7 100644
--- a/src/searchcode/__app.py
+++ b/src/searchcode/__app.py
@@ -122,8 +122,8 @@ def licence(
)
def search(
query: str,
- page: int = 0,
- per_page: int = 100,
+ page: int,
+ per_page: int,
pretty: bool = False,
lines_of_code_lt: t.Optional[int] = None,
lines_of_code_gt: t.Optional[int] = None,
@@ -156,12 +156,16 @@ def search(
callback=callback,
)
- (
- print_jsonp(jsonp=response)
- if callback
- else (pprint(response) if pretty else print_panels(data=response.results))
+ console.print(
+ f"πΈ Showing {len(response.results)} of {per_page} results for '{query}'"
)
+ (
+ print_jsonp(jsonp=response)
+ if callback
+ else (pprint(response) if pretty else print_panels(data=response.results))
+ )
+
@cli.command()
@click.argument("id", type=int)
diff --git a/src/searchcode/__init__.py b/src/searchcode/__init__.py
index 6827e3e..54fd605 100644
--- a/src/searchcode/__init__.py
+++ b/src/searchcode/__init__.py
@@ -20,7 +20,7 @@
from .api import Searchcode
__pkg__ = "searchcode"
-__version__ = "0.5.1"
+__version__ = "0.5.2"
__author__ = "Ritchie Mwewa"
From e99f1d545c8fe2b769a6d4822915eea15388d1f4 Mon Sep 17 00:00:00 2001
From: Ritchie Mwewa <74001397+rly0nheart@users.noreply.github.com>
Date: Tue, 13 May 2025 05:29:59 +0200
Subject: [PATCH 2/6] Fix: broken layout in search results' panels
---
src/searchcode/__app.py | 17 ++++-------------
src/searchcode/__lib.py | 2 +-
2 files changed, 5 insertions(+), 14 deletions(-)
diff --git a/src/searchcode/__app.py b/src/searchcode/__app.py
index 213bcd7..8faf089 100644
--- a/src/searchcode/__app.py
+++ b/src/searchcode/__app.py
@@ -18,7 +18,6 @@
import typing as t
import rich_click as click
-from rich.console import Console
from rich.pretty import pprint
from rich.syntax import Syntax
@@ -26,6 +25,7 @@
from .__lib import (
__pkg__,
__version__,
+ console,
update_window_title,
clear_screen,
print_jsonp,
@@ -33,11 +33,8 @@
)
from .api import Searchcode
-sc = Searchcode(user_agent=f"{__pkg__}-sdk/cli")
-
__all__ = ["cli"]
-
-console = Console(highlight=True)
+sc = Searchcode(user_agent=f"{__pkg__}-sdk/cli")
@click.group()
@@ -139,9 +136,7 @@ def search(
clear_screen()
update_window_title(text=query)
- with console.status(
- f"Querying code index with search string: [green]{query}[/]..."
- ):
+ with console.status(f"Querying code index: [green]{query}[/]"):
languages = languages.split(",") if languages else None
sources = sources.split(",") if sources else None
@@ -156,10 +151,6 @@ def search(
callback=callback,
)
- console.print(
- f"πΈ Showing {len(response.results)} of {per_page} results for '{query}'"
- )
-
(
print_jsonp(jsonp=response)
if callback
@@ -177,7 +168,7 @@ def code(id: int):
"""
clear_screen()
update_window_title(text=str(id))
- with console.status(f"Fetching data for code file with ID: [cyan]{id}[/]..."):
+ with console.status(f"Fetching code file: [cyan]{id}[/]"):
data = sc.code(id)
lines = data.code
language = data.language
diff --git a/src/searchcode/__lib.py b/src/searchcode/__lib.py
index 7bde606..1ccc239 100644
--- a/src/searchcode/__lib.py
+++ b/src/searchcode/__lib.py
@@ -10,7 +10,7 @@
from . import __pkg__, __version__
-console = Console()
+console = Console(highlight=True)
def print_jsonp(jsonp: str) -> None:
From fbf94c477a35440663ad27b2b34683131087a46b Mon Sep 17 00:00:00 2001
From: Ritchie Mwewa <74001397+rly0nheart@users.noreply.github.com>
Date: Wed, 14 May 2025 01:12:11 +0200
Subject: [PATCH 3/6] bump to v0.6.0 with refactored CLI architecture,
pagination, and safer internal API
---
pyproject.toml | 10 +-
src/searchcode/__app.py | 179 --------------
src/searchcode/__cli/__init__.py | 0
src/searchcode/__cli/app.py | 283 ++++++++++++++++++++++
src/searchcode/{__lib.py => __cli/lib.py} | 47 +++-
src/searchcode/__init__.py | 2 +-
src/searchcode/api.py | 30 +--
7 files changed, 339 insertions(+), 212 deletions(-)
delete mode 100644 src/searchcode/__app.py
create mode 100644 src/searchcode/__cli/__init__.py
create mode 100644 src/searchcode/__cli/app.py
rename src/searchcode/{__lib.py => __cli/lib.py} (60%)
diff --git a/pyproject.toml b/pyproject.toml
index 1fc18d4..77895fe 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "searchcode"
-version = "0.5.2"
+version = "0.6.0"
description = "Simple, comprehensive code search."
authors = ["Ritchie Mwewa "]
license = "GPLv3+"
@@ -16,6 +16,10 @@ classifiers = [
"Natural Language :: English"
]
+
+packages = [
+ { include = "searchcode", from = "src" }
+]
[tool.poetry.dependencies]
python = "^3.10"
requests = "^2.32.2"
@@ -30,5 +34,5 @@ requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
-sc = "searchcode.__app:cli"
-searchcode = "searchcode.__app:cli"
\ No newline at end of file
+sc = "searchcode.__cli.app:cli"
+searchcode = "searchcode.__cli.app:cli"
\ No newline at end of file
diff --git a/src/searchcode/__app.py b/src/searchcode/__app.py
deleted file mode 100644
index 8faf089..0000000
--- a/src/searchcode/__app.py
+++ /dev/null
@@ -1,179 +0,0 @@
-"""
-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 .
-"""
-
-import typing as t
-
-import rich_click as click
-from rich.pretty import pprint
-from rich.syntax import Syntax
-
-from . import License
-from .__lib import (
- __pkg__,
- __version__,
- console,
- update_window_title,
- clear_screen,
- print_jsonp,
- print_panels,
-)
-from .api import Searchcode
-
-__all__ = ["cli"]
-sc = Searchcode(user_agent=f"{__pkg__}-sdk/cli")
-
-
-@click.group()
-@click.version_option(version=__version__, package_name=__pkg__)
-def cli():
- """
- Searchcode
-
- Simple, comprehensive code search.
- """
-
- update_window_title(text="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)
-@click.pass_context
-def licence(
- ctx: click.Context, conditions: t.Optional[bool], warranty: t.Optional[bool]
-):
- """
- Show license information
- """
- 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",
- )
- elif warranty:
- console.print(
- License.warranty,
- justify="center",
- )
- else:
- click.echo(ctx.get_help())
-
-
-@cli.command()
-@click.argument("query", type=str)
-@click.option("--pretty", help="Return results in raw JSON format.", is_flag=True)
-@click.option(
- "--page",
- type=int,
- default=0,
- help="Start page (defaults to 0).",
-)
-@click.option(
- "--per-page",
- type=int,
- default=100,
- help="Results per page (defaults to 100).",
-)
-@click.option(
- "--lines-of-code-lt",
- type=int,
- help="Filter to sources with less lines of code than the supplied value (Valid values: 0 to 10000).",
-)
-@click.option(
- "--lines-of-code-gt",
- type=int,
- help="Filter to sources with greater lines of code than the supplied value (Valid values: 0 to 10000).",
-)
-@click.option(
- "--sources",
- type=str,
- help="A comma-separated list of code sources to filter results.",
-)
-@click.option(
- "--languages",
- type=str,
- help="A comma-separated list of code languages to filter results.",
-)
-@click.option(
- "--callback",
- type=str,
- help="callback function (returns JSONP)",
-)
-def search(
- query: str,
- page: int,
- per_page: int,
- pretty: bool = False,
- lines_of_code_lt: t.Optional[int] = None,
- lines_of_code_gt: t.Optional[int] = None,
- languages: t.Optional[str] = None,
- sources: t.Optional[str] = None,
- callback: t.Optional[str] = None,
-):
- """
- Query the code index (returns 100 results by default).
-
- e.g., sc search "import module"
- """
- clear_screen()
- update_window_title(text=query)
-
- with console.status(f"Querying code index: [green]{query}[/]"):
- languages = languages.split(",") if languages else None
- sources = sources.split(",") if sources else None
-
- response = 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=response)
- if callback
- else (pprint(response) if pretty else print_panels(data=response.results))
- )
-
-
-@cli.command()
-@click.argument("id", type=int)
-def code(id: int):
- """
- Get the raw data from a code file.
-
- e.g., sc code 4061576
- """
- clear_screen()
- update_window_title(text=str(id))
- with console.status(f"Fetching code file: [cyan]{id}[/]"):
- data = sc.code(id)
- lines = data.code
- language = data.language
- if lines:
- syntax = Syntax(
- code=lines, lexer=language, line_numbers=True, theme="dracula"
- )
- console.print(syntax)
diff --git a/src/searchcode/__cli/__init__.py b/src/searchcode/__cli/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/searchcode/__cli/app.py b/src/searchcode/__cli/app.py
new file mode 100644
index 0000000..b495a4e
--- /dev/null
+++ b/src/searchcode/__cli/app.py
@@ -0,0 +1,283 @@
+"""
+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 .
+"""
+
+import typing as t
+
+import rich_click as click
+from rich.syntax import Syntax
+
+from .lib import (
+ console,
+ update_window_title,
+ clear_screen,
+ print_jsonp,
+ print_panels,
+ namespace_to_dict,
+)
+from .. import __pkg__, __version__, License
+from ..api import Searchcode
+
+__all__ = ["cli"]
+sc = Searchcode(user_agent=f"{__pkg__}-sdk/__cli")
+
+
+@click.group()
+@click.version_option(version=__version__, package_name=__pkg__)
+def cli():
+ """
+ Searchcode
+
+ Simple, comprehensive code search.
+ """
+
+ update_window_title(text="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)
+@click.pass_context
+def licence(
+ ctx: click.Context, conditions: t.Optional[bool], warranty: t.Optional[bool]
+):
+ """
+ Show license information
+ """
+ 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",
+ )
+ elif warranty:
+ console.print(
+ License.warranty,
+ justify="center",
+ )
+ else:
+ click.echo(ctx.get_help())
+
+
+import typing as t
+from types import SimpleNamespace
+
+
+@click.option(
+ "--page", type=int, default=0, show_default=True, help="Start page number."
+)
+@click.option(
+ "--pages",
+ type=int,
+ default=1,
+ show_default=True,
+ help="Number of pages to fetch (maximum 5). Ignored if --callback is set.",
+)
+@click.option(
+ "--per-page",
+ type=int,
+ default=50,
+ show_default=True,
+ help="Results per page (maximum 100).",
+)
+@click.option(
+ "--lines-of-code-lt",
+ type=int,
+ help="Filter to sources with fewer lines of code (0 to 10000).",
+)
+@click.option(
+ "--lines-of-code-gt",
+ type=int,
+ help="Filter to sources with more lines of code (0 to 10000).",
+)
+@click.option("--sources", type=str, help="Comma-separated list of source filters.")
+@click.option("--languages", type=str, help="Comma-separated list of language filters.")
+@click.option(
+ "--callback",
+ type=str,
+ help="Callback function for JSONP output (disables pagination).",
+)
+@click.option("--pretty", is_flag=True, help="Print raw JSON output.")
+@click.argument("query", type=str)
+@cli.command()
+def search(
+ query: str,
+ page: int,
+ pages: int,
+ per_page: int,
+ pretty: bool,
+ lines_of_code_lt: t.Optional[int],
+ lines_of_code_gt: t.Optional[int],
+ languages: t.Optional[str],
+ sources: t.Optional[str],
+ callback: t.Optional[str],
+):
+ """
+ Query the code index (paginated or JSONP).
+
+ e.g., sc search "import module"
+ """
+ clear_screen()
+ update_window_title(text=query)
+
+ languages = languages.split(",") if languages else None
+ sources = sources.split(",") if sources else None
+ pages = max(1, min(pages, 5)) # limit 1 <= pages <= 5
+
+ if callback:
+ # JSONP mode = single page only
+ response = 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=response)
+ return
+
+ # normal paginated search
+ with console.status(f"Querying: [green]{query}[/]") as status:
+ results, total = fetch_paginated_results(
+ query=query,
+ start_page=page,
+ per_page=per_page,
+ pages=pages,
+ languages=languages,
+ sources=sources,
+ lines_of_code_lt=lines_of_code_lt,
+ lines_of_code_gt=lines_of_code_gt,
+ callback=None,
+ status=status,
+ )
+
+ print_results(
+ results=results, total=total, query=query, callback=callback, pretty=pretty
+ )
+
+
+def fetch_paginated_results(
+ query: str,
+ start_page: int,
+ per_page: int,
+ pages: int,
+ languages: t.Optional[t.List[str]],
+ sources: t.Optional[t.List[str]],
+ lines_of_code_lt: t.Optional[int],
+ lines_of_code_gt: t.Optional[int],
+ callback: t.Optional[str],
+ status,
+) -> t.Tuple[t.List[SimpleNamespace], int]:
+ """
+ Fetch paginated results from the code index.
+
+ :return: Tuple of (results list, total number of results)
+ """
+ all_results = []
+ current_page = start_page
+ total_results = 0
+
+ for current_iteration in range(1, pages + 1):
+ status.update(
+ f"Fetching page [cyan]{current_iteration}[/]/[cyan]{pages}[/] "
+ f"([cyan]{len(all_results)}[/] results collected)"
+ )
+
+ response = sc.search(
+ query=query,
+ page=current_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=None,
+ )
+
+ if isinstance(response, str):
+ break
+ elif response.results:
+ all_results.extend(response.results)
+ total_results = response.total
+
+ if len(all_results) >= response.total:
+ break
+
+ current_page += 1
+ else:
+ break
+
+ return all_results, total_results
+
+
+def print_results(
+ results: t.List, total: int, query: str, callback: t.Optional[str], pretty: bool
+):
+ """
+ Print search results.
+
+ :param results: List of code records.
+ :param total: Total result count.
+ :param query: Search query.
+ :param callback: JSONP callback (should be None).
+ :param pretty: Whether to print raw JSON.
+ """
+ if results:
+ if pretty:
+ console.print(namespace_to_dict(obj=results))
+ else:
+ print_panels(data=results)
+
+ if not callback and not pretty:
+ console.log(
+ f"[bold green]β[/bold green] Returned {len(results)} of {total} "
+ f"results for [bold green]{query}[/bold green]."
+ )
+ else:
+ console.log(
+ f"[bold yellow]β[/bold yellow] No results for [bold yellow]{query}[/bold yellow]."
+ )
+
+
+@cli.command()
+@click.argument("id", type=int)
+def code(id: int):
+ """
+ Get the raw data from a code file.
+
+ e.g., sc code 4061576
+ """
+ clear_screen()
+ update_window_title(text=str(id))
+ with console.status(f"Fetching code file: [cyan]{id}[/]"):
+ data = sc.code(id)
+ lines = data.code
+ language = data.language
+ if lines:
+ syntax = Syntax(
+ code=lines, lexer=language, line_numbers=True, theme="dracula"
+ )
+ console.print(syntax)
+ else:
+ console.log(
+ f"[bold yellow]β[/bold yellow] No matching file found: [bold yellow]{id}[/bold yellow]."
+ )
diff --git a/src/searchcode/__lib.py b/src/searchcode/__cli/lib.py
similarity index 60%
rename from src/searchcode/__lib.py
rename to src/searchcode/__cli/lib.py
index 1ccc239..0b136ee 100644
--- a/src/searchcode/__lib.py
+++ b/src/searchcode/__cli/lib.py
@@ -8,9 +8,50 @@
from rich.panel import Panel
from rich.syntax import Syntax
-from . import __pkg__, __version__
+console = Console(highlight=True, log_time=False)
-console = Console(highlight=True)
+
+def namespace_to_dict(
+ obj: t.Union[SimpleNamespace, t.List[SimpleNamespace]],
+) -> t.Union[t.Dict, t.List[t.Dict], SimpleNamespace, t.List[SimpleNamespace]]:
+ """
+ Recursively convert a SimpleNamespace object and any nested namespaces into a dictionary.
+
+ :param obj: The object to convert. It can be a SimpleNamespace, list, dictionary, or any other type.
+ :type obj: Union[SimpleNamespace, List[SimpleNamespace]]
+ :return: A dictionary (or list, or primitive type) suitable for JSON serialization.
+ :rtype: Union[Dict, List[Dict], SimpleNamespace, List[SimpleNamespace]]
+ """
+ if isinstance(obj, SimpleNamespace):
+ return {key: namespace_to_dict(value) for key, value in vars(obj).items()}
+ elif isinstance(obj, list):
+ return [namespace_to_dict(item) for item in obj]
+ elif isinstance(obj, dict):
+ return {key: namespace_to_dict(value) for key, value in obj.items()}
+ else:
+ return obj
+
+
+def dict_to_namespace(
+ obj: t.Union[t.List[t.Dict], t.Dict],
+) -> t.Union[t.List[SimpleNamespace], SimpleNamespace, t.List[t.Dict], t.Dict]:
+ """
+ Recursively converts the API response into a SimpleNamespace object(s).
+
+ :param obj: The object to convert, either a dictionary or a list of dictionaries.
+ :type obj: Union[List[Dict], Dict]
+ :return: A SimpleNamespace object or list of SimpleNamespace objects.
+ :rtype: Union[List[SimpleNamespace], SimpleNamespace, List[Dict], Dict]
+ """
+
+ if isinstance(obj, t.Dict):
+ return SimpleNamespace(
+ **{key: dict_to_namespace(obj=value) for key, value in obj.items()}
+ )
+ elif isinstance(obj, t.List):
+ return [dict_to_namespace(obj=item) for item in obj]
+ else:
+ return obj
def print_jsonp(jsonp: str) -> None:
@@ -80,6 +121,8 @@ def update_window_title(text: str):
:param text: Text to update the window with.
"""
+ from .. import __pkg__, __version__
+
console.set_window_title(f"{__pkg__.capitalize()} v{__version__} - {text}")
diff --git a/src/searchcode/__init__.py b/src/searchcode/__init__.py
index 54fd605..e5d98b6 100644
--- a/src/searchcode/__init__.py
+++ b/src/searchcode/__init__.py
@@ -20,7 +20,7 @@
from .api import Searchcode
__pkg__ = "searchcode"
-__version__ = "0.5.2"
+__version__ = "0.6.0"
__author__ = "Ritchie Mwewa"
diff --git a/src/searchcode/api.py b/src/searchcode/api.py
index b476dca..45f7864 100644
--- a/src/searchcode/api.py
+++ b/src/searchcode/api.py
@@ -21,6 +21,7 @@
import requests
+from .__cli.lib import dict_to_namespace
from .filters import CODE_LANGUAGES, CODE_SOURCES, get_language_ids, get_source_ids
__all__ = ["Searchcode"]
@@ -95,7 +96,7 @@ def search(
)
if not callback:
- response = self.__to_namespace_obj(response=response)
+ response = dict_to_namespace(obj=response)
response.results = response.results[:per_page]
return response
@@ -113,7 +114,7 @@ def code(self, __id: int) -> SimpleNamespace:
response = self.__send_request(
endpoint=f"{self.__base_api_endpoint}/result/{__id}"
)
- return self.__to_namespace_obj(response=response)
+ return dict_to_namespace(obj=response)
# This is deprecated (for now).
# def related(_id: int) -> Dict:
@@ -159,28 +160,3 @@ def __send_request(
)
response.raise_for_status()
return response.text if callback else response.json()
-
- def __to_namespace_obj(
- self,
- response: t.Union[t.List[t.Dict], t.Dict],
- ) -> t.Union[t.List[SimpleNamespace], SimpleNamespace, t.List[t.Dict], t.Dict]:
- """
- Recursively converts the API response into a SimpleNamespace object(s).
-
- :param response: The object to convert, either a dictionary or a list of dictionaries.
- :type response: Union[List[Dict], Dict]
- :return: A SimpleNamespace object or list of SimpleNamespace objects.
- :rtype: Union[List[SimpleNamespace], SimpleNamespace, None]
- """
-
- if isinstance(response, t.Dict):
- return SimpleNamespace(
- **{
- key: self.__to_namespace_obj(response=value)
- for key, value in response.items()
- }
- )
- elif isinstance(response, t.List):
- return [self.__to_namespace_obj(response=item) for item in response]
- else:
- return response
From ef0e109dc2bf5d7199b637c32b045fea4e1bfef5 Mon Sep 17 00:00:00 2001
From: Ritchie Mwewa <74001397+rly0nheart@users.noreply.github.com>
Date: Wed, 14 May 2025 01:14:32 +0200
Subject: [PATCH 4/6] bump to v0.6.0 with refactored CLI architecture,
pagination, and safer internal API
---
src/searchcode/__cli/app.py | 9 ++-------
1 file changed, 2 insertions(+), 7 deletions(-)
diff --git a/src/searchcode/__cli/app.py b/src/searchcode/__cli/app.py
index b495a4e..fcb8ff7 100644
--- a/src/searchcode/__cli/app.py
+++ b/src/searchcode/__cli/app.py
@@ -16,6 +16,7 @@
"""
import typing as t
+from types import SimpleNamespace
import rich_click as click
from rich.syntax import Syntax
@@ -75,10 +76,6 @@ def licence(
click.echo(ctx.get_help())
-import typing as t
-from types import SimpleNamespace
-
-
@click.option(
"--page", type=int, default=0, show_default=True, help="Start page number."
)
@@ -166,7 +163,6 @@ def search(
sources=sources,
lines_of_code_lt=lines_of_code_lt,
lines_of_code_gt=lines_of_code_gt,
- callback=None,
status=status,
)
@@ -184,8 +180,7 @@ def fetch_paginated_results(
sources: t.Optional[t.List[str]],
lines_of_code_lt: t.Optional[int],
lines_of_code_gt: t.Optional[int],
- callback: t.Optional[str],
- status,
+ status: console.status,
) -> t.Tuple[t.List[SimpleNamespace], int]:
"""
Fetch paginated results from the code index.
From 77aece4ae7c7a17f58cc012d1eef694be54222e2 Mon Sep 17 00:00:00 2001
From: Ritchie Mwewa <74001397+rly0nheart@users.noreply.github.com>
Date: Thu, 15 May 2025 01:01:26 +0200
Subject: [PATCH 5/6] Bump version 0.6: Show results in panels... like on
searchcode.com :)
---
pyproject.toml | 4 +-
src/searchcode/__init__.py | 2 +
src/searchcode/{__cli => _cli}/__init__.py | 0
src/searchcode/{__cli => _cli}/app.py | 85 ++++---------
src/searchcode/_cli/panels.py | 135 +++++++++++++++++++++
src/searchcode/{__cli/lib.py => _lib.py} | 71 +----------
src/searchcode/api.py | 8 +-
src/searchcode/filters.py | 14 +--
8 files changed, 177 insertions(+), 142 deletions(-)
rename src/searchcode/{__cli => _cli}/__init__.py (100%)
rename src/searchcode/{__cli => _cli}/app.py (78%)
create mode 100644 src/searchcode/_cli/panels.py
rename src/searchcode/{__cli/lib.py => _lib.py} (54%)
diff --git a/pyproject.toml b/pyproject.toml
index 77895fe..fd52a59 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -34,5 +34,5 @@ requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
-sc = "searchcode.__cli.app:cli"
-searchcode = "searchcode.__cli.app:cli"
\ No newline at end of file
+sc = "searchcode._cli.app:cli"
+searchcode = "searchcode._cli.app:cli"
\ No newline at end of file
diff --git a/src/searchcode/__init__.py b/src/searchcode/__init__.py
index e5d98b6..a541d0a 100644
--- a/src/searchcode/__init__.py
+++ b/src/searchcode/__init__.py
@@ -23,6 +23,8 @@
__version__ = "0.6.0"
__author__ = "Ritchie Mwewa"
+__all__ = ["Searchcode"]
+
class License:
terms_and_conditions: str = """
diff --git a/src/searchcode/__cli/__init__.py b/src/searchcode/_cli/__init__.py
similarity index 100%
rename from src/searchcode/__cli/__init__.py
rename to src/searchcode/_cli/__init__.py
diff --git a/src/searchcode/__cli/app.py b/src/searchcode/_cli/app.py
similarity index 78%
rename from src/searchcode/__cli/app.py
rename to src/searchcode/_cli/app.py
index fcb8ff7..94e833e 100644
--- a/src/searchcode/__cli/app.py
+++ b/src/searchcode/_cli/app.py
@@ -19,17 +19,14 @@
from types import SimpleNamespace
import rich_click as click
-from rich.syntax import Syntax
-from .lib import (
- console,
- update_window_title,
+from .panels import console, print_panels
+from .. import __pkg__, __version__, License
+from .._lib import (
clear_screen,
- print_jsonp,
- print_panels,
namespace_to_dict,
+ update_window_title,
)
-from .. import __pkg__, __version__, License
from ..api import Searchcode
__all__ = ["cli"]
@@ -149,12 +146,12 @@ def search(
lines_of_code_gt=lines_of_code_gt,
callback=callback,
)
- print_jsonp(jsonp=response)
+ print_panels(data=response)
return
# normal paginated search
- with console.status(f"Querying: [green]{query}[/]") as status:
- results, total = fetch_paginated_results(
+ with console.status(f"Querying code index with [green]{query}[/]...") as status:
+ results, total = _fetch_paginated_results(
query=query,
start_page=page,
per_page=per_page,
@@ -166,12 +163,20 @@ def search(
status=status,
)
- print_results(
- results=results, total=total, query=query, callback=callback, pretty=pretty
- )
+ if results:
+ if not callback and not pretty:
+ console.log(f"Showing {len(results)} of {total} results for '{query}'")
+ if pretty:
+ console.print(namespace_to_dict(obj=results))
+ else:
+ print_panels(data=results)
+ else:
+ console.log(
+ f"[bold yellow]β[/bold yellow] No results found for [bold yellow]{query}[/bold yellow]."
+ )
-def fetch_paginated_results(
+def _fetch_paginated_results(
query: str,
start_page: int,
per_page: int,
@@ -192,11 +197,6 @@ def fetch_paginated_results(
total_results = 0
for current_iteration in range(1, pages + 1):
- status.update(
- f"Fetching page [cyan]{current_iteration}[/]/[cyan]{pages}[/] "
- f"([cyan]{len(all_results)}[/] results collected)"
- )
-
response = sc.search(
query=query,
page=current_page,
@@ -207,6 +207,10 @@ def fetch_paginated_results(
lines_of_code_gt=lines_of_code_gt,
callback=None,
)
+ status.update(
+ f"Getting page results on page [cyan]{current_iteration}[/] of [cyan]{pages}[/] "
+ f"([cyan]{len(all_results)}[/] results collected)..."
+ )
if isinstance(response, str):
break
@@ -224,35 +228,6 @@ def fetch_paginated_results(
return all_results, total_results
-def print_results(
- results: t.List, total: int, query: str, callback: t.Optional[str], pretty: bool
-):
- """
- Print search results.
-
- :param results: List of code records.
- :param total: Total result count.
- :param query: Search query.
- :param callback: JSONP callback (should be None).
- :param pretty: Whether to print raw JSON.
- """
- if results:
- if pretty:
- console.print(namespace_to_dict(obj=results))
- else:
- print_panels(data=results)
-
- if not callback and not pretty:
- console.log(
- f"[bold green]β[/bold green] Returned {len(results)} of {total} "
- f"results for [bold green]{query}[/bold green]."
- )
- else:
- console.log(
- f"[bold yellow]β[/bold yellow] No results for [bold yellow]{query}[/bold yellow]."
- )
-
-
@cli.command()
@click.argument("id", type=int)
def code(id: int):
@@ -263,16 +238,6 @@ def code(id: int):
"""
clear_screen()
update_window_title(text=str(id))
- with console.status(f"Fetching code file: [cyan]{id}[/]"):
+ with console.status(f"Getting code file [cyan]{id}[/]..."):
data = sc.code(id)
- lines = data.code
- language = data.language
- if lines:
- syntax = Syntax(
- code=lines, lexer=language, line_numbers=True, theme="dracula"
- )
- console.print(syntax)
- else:
- console.log(
- f"[bold yellow]β[/bold yellow] No matching file found: [bold yellow]{id}[/bold yellow]."
- )
+ print_panels(data=data, id=id)
diff --git a/src/searchcode/_cli/panels.py b/src/searchcode/_cli/panels.py
new file mode 100644
index 0000000..b958068
--- /dev/null
+++ b/src/searchcode/_cli/panels.py
@@ -0,0 +1,135 @@
+import typing as t
+from types import SimpleNamespace
+
+from rich.console import Group, Console
+from rich.panel import Panel
+from rich.rule import Rule
+from rich.syntax import Syntax
+from rich.text import Text
+
+console = Console(highlight=True, log_time=False)
+
+
+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.
+
+ Each line is right-aligned to maintain visual alignment in output.
+
+ :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)
+
+
+def _make_syntax(code: str, language: str, **syntax_kwargs) -> Syntax:
+ """
+ Create a Syntax object with consistent settings.
+
+ :param code: The source code to render.
+ :type code: str
+ :param language: The programming language lexer to use.
+ :type language: str
+ :param syntax_kwargs: Additional keyword arguments for Syntax.
+ :type syntax_kwargs: Any
+ :return: A rich Syntax object for displaying code.
+ :rtype: Syntax
+ """
+ return Syntax(code=code, lexer=language, theme="dracula", **syntax_kwargs)
+
+
+def _make_syntax_panel(
+ syntax: Syntax, header_text: t.Optional[str] = None, add_divider: bool = False
+) -> Panel:
+ """
+ Wrap a Syntax (or any renderable) in a styled Panel. Optionally include a header and divider.
+
+ :param syntax: The Syntax object to display inside the Panel.
+ :type syntax: Syntax
+ :param header_text: Optional markup string for the header above the syntax.
+ :type header_text: Optional[str]
+ :param add_divider: Whether to include a horizontal rule between header and syntax.
+ :type add_divider: bool
+ :return: A rich Panel containing the syntax (and optional header/divider).
+ :rtype: Panel
+ """
+ if header_text:
+ header = Text.from_markup(header_text, justify="left", overflow="ellipsis")
+ divider = Rule(style="#444444") if add_divider else None
+ content_items = [header, divider, syntax] if divider else [header, syntax]
+ content = Group(*content_items)
+ else:
+ content = syntax
+
+ return Panel(renderable=content, border_style="#444444", title_align="left")
+
+
+def print_panels(
+ data: t.Union[t.List[SimpleNamespace], SimpleNamespace, str], **kwargs
+) -> None:
+ """
+ Print panels for displaying code or structured file information.
+
+ Accepts either:
+ - a single SimpleNamespace with fields `code`, `language`
+ - a string of raw code
+ - a list of SimpleNamespace objects with fields `filename`, `repo`, `language`, `linescount`, `lines`
+
+ :param data: The input data to display as panels.
+ :type data: Union[List[SimpleNamespace], SimpleNamespace, str]
+ :param kwargs: Additional optional keyword arguments (e.g., id for logging).
+ :type kwargs: Any
+ :return: None
+ :rtype: None
+ """
+ panels: t.List[Panel] = []
+
+ if isinstance(data, SimpleNamespace):
+ code = data.code
+ language = data.language
+ if code:
+ syntax = _make_syntax(code, language, line_numbers=True)
+ panel = _make_syntax_panel(syntax)
+ panels.append(panel)
+ else:
+ console.log(
+ f"[bold yellow]β[/bold yellow] No matching file found: [bold yellow]{kwargs.get('id')}[/bold yellow]."
+ )
+ return
+ elif isinstance(data, str):
+ syntax = _make_syntax(data, "text", line_numbers=True)
+ panel = _make_syntax_panel(syntax)
+ panels.append(panel)
+ else:
+ for item in data:
+ filename = item.filename
+ repo = item.repo
+ language = item.language
+ lines_count = item.linescount
+ lines = item.lines
+
+ code_string = _extract_code_string_with_linenumbers(
+ lines_dict=lines.__dict__
+ )
+
+ syntax = _make_syntax(
+ code=code_string, language=language, word_wrap=False, indent_guides=True
+ )
+
+ header_text = (
+ f"[bold]{filename}[/] ([blue]{repo}[/]) "
+ f"{language} Β· [cyan]{lines_count}[/] lines"
+ )
+
+ panel = _make_syntax_panel(
+ syntax=syntax, header_text=header_text, add_divider=True
+ )
+
+ panels.append(panel)
+
+ console.print(*panels)
diff --git a/src/searchcode/__cli/lib.py b/src/searchcode/_lib.py
similarity index 54%
rename from src/searchcode/__cli/lib.py
rename to src/searchcode/_lib.py
index 0b136ee..538e861 100644
--- a/src/searchcode/__cli/lib.py
+++ b/src/searchcode/_lib.py
@@ -3,13 +3,6 @@
import typing as t
from types import SimpleNamespace
-from rich import box
-from rich.console import Console
-from rich.panel import Panel
-from rich.syntax import Syntax
-
-console = Console(highlight=True, log_time=False)
-
def namespace_to_dict(
obj: t.Union[SimpleNamespace, t.List[SimpleNamespace]],
@@ -54,74 +47,14 @@ def dict_to_namespace(
return obj
-def print_jsonp(jsonp: str) -> None:
- """
- Pretty-prints a raw JSONP string.
-
- :param jsonp: A complete JSONP string.
- """
- syntax = Syntax(jsonp, "text", line_numbers=True)
- console.print(syntax)
-
-
-def print_panels(data: t.List[SimpleNamespace]):
- """
- Render a list of code records as rich panels with syntax highlighting.
- Line numbers are preserved and displayed alongside code content.
-
- :param data: A list of dictionaries, where each dictionary represents a code record
- """
-
- 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.
-
- Each line is right-aligned to maintain visual alignment in output.
-
- :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)
-
- for item in data:
- filename = item.filename
- repo = item.repo
- language = item.language
- lines_count = item.linescount
- lines = item.lines
-
- code_string = extract_code_string_with_linenumbers(lines_dict=lines.__dict__)
-
- syntax = Syntax(
- code=code_string,
- lexer=language,
- word_wrap=False,
- indent_guides=True,
- theme="dracula",
- )
-
- panel = Panel(
- renderable=syntax,
- box=box.ROUNDED,
- title=f"[bold]{filename}[/] ([blue]{repo}[/]) {language} βΈ± [cyan]{lines_count}[/] lines",
- highlight=True,
- )
-
- console.print(panel)
-
-
def update_window_title(text: str):
"""
Update the current window title with the specified text.
:param text: Text to update the window with.
"""
- from .. import __pkg__, __version__
+ from . import __pkg__, __version__
+ from ._cli.panels import console
console.set_window_title(f"{__pkg__.capitalize()} v{__version__} - {text}")
diff --git a/src/searchcode/api.py b/src/searchcode/api.py
index 45f7864..d631607 100644
--- a/src/searchcode/api.py
+++ b/src/searchcode/api.py
@@ -21,8 +21,8 @@
import requests
-from .__cli.lib import dict_to_namespace
-from .filters import CODE_LANGUAGES, CODE_SOURCES, get_language_ids, get_source_ids
+from ._lib import dict_to_namespace
+from .filters import LANGUAGES, SOURCES, get_language_ids, get_source_ids
__all__ = ["Searchcode"]
@@ -37,8 +37,8 @@ def search(
query: str,
page: int = 0,
per_page: int = 100,
- languages: t.Optional[t.List[CODE_LANGUAGES]] = None,
- sources: t.Optional[t.List[CODE_SOURCES]] = None,
+ languages: t.Optional[t.List[LANGUAGES]] = None,
+ sources: t.Optional[t.List[SOURCES]] = None,
lines_of_code_gt: t.Optional[int] = None,
lines_of_code_lt: t.Optional[int] = None,
callback: t.Optional[str] = None,
diff --git a/src/searchcode/filters.py b/src/searchcode/filters.py
index ffb6bb4..054e6fb 100644
--- a/src/searchcode/filters.py
+++ b/src/searchcode/filters.py
@@ -18,9 +18,9 @@
import typing as t
-__all__ = ["CODE_LANGUAGES", "CODE_SOURCES", "get_language_ids", "get_source_ids"]
+__all__ = ["LANGUAGES", "SOURCES", "get_language_ids", "get_source_ids"]
-CODE_SOURCES = t.Literal[
+SOURCES = t.Literal[
"Google Code",
"GitHub",
"BitBucket",
@@ -38,7 +38,7 @@
"Sr.ht",
]
-CODE_LANGUAGES = t.Literal[
+LANGUAGES = t.Literal[
"XAML",
"ASP.NET",
"HTML",
@@ -387,12 +387,12 @@
]
-def get_source_ids(source_names: t.List[CODE_SOURCES]) -> t.List[int]:
+def get_source_ids(source_names: t.List[SOURCES]) -> t.List[int]:
"""
Gets a list of source IDs corresponding to the given source names.
:param source_names: A list of source names to look up (e.g., "GitHub", "GitLab").
- :type source_names: List[CODE_SOURCES]
+ :type source_names: List[SOURCES]
:return: A list of IDs corresponding to the given source names.
:rtype: List[int]
"""
@@ -418,12 +418,12 @@ def get_source_ids(source_names: t.List[CODE_SOURCES]) -> t.List[int]:
return [sources[name] for name in source_names if name in sources]
-def get_language_ids(language_names: t.List[CODE_LANGUAGES]) -> t.List:
+def get_language_ids(language_names: t.List[LANGUAGES]) -> t.List:
"""
Gets a list of language IDs corresponding to the given language names.
:param language_names: A list of language names to look up (e.g., "Python", "JavaScript").
- :type language_names: List[CODE_LANGUAGES]
+ :type language_names: List[LANGUAGES]
:return: A list of IDs corresponding to the given language names.
:rtype: List[int]
"""
From ebf2ea1f3a0a20ef4de57a47eb21afdff1f40ca1 Mon Sep 17 00:00:00 2001
From: Ritchie Mwewa <74001397+rly0nheart@users.noreply.github.com>
Date: Thu, 15 May 2025 01:05:07 +0200
Subject: [PATCH 6/6] Bump version 0.6: Show results in panels... like on
searchcode.com :)
---
README.md | 33 ---------------------------------
1 file changed, 33 deletions(-)
diff --git a/README.md b/README.md
index b772754..36f64f9 100644
--- a/README.md
+++ b/README.md
@@ -162,39 +162,6 @@ pprint(search)
> To fetch all results for a given query, keep incrementing `page` parameter until you get a page with an empty results
> list.
----
-
-### Response Attribute Definitions
-
-| Attribute | Description |
-|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| **searchterm** | Search term supplied to the API through the use of the `q` parameter. |
-| **query** | Identical to `searchterm` and included for historical reasons to maintain backward compatibility. |
-| **matchterm** | Identical to `searchterm` and included for historical reasons to maintain backward compatibility. |
-| **page** | ID of the current page that the query has returned. This is a zero-based index. |
-| **nextpage** | ID of the offset of the next page. Always set to the current page + 1, even if you have reached the end of the results. This is a zero-based index. |
-| **previouspage** | ID of the offset of the previous page. If no previous page is available, it will be set to `null`. This is a zero-based index. |
-| **total** | The total number of results that match the `searchterm` in the index. Note that this value is approximate. It becomes more accurate as you go deeper into the results or use more filters. |
-| **language_filters** | Returns an array containing languages that exist in the result set. |
-| **id** | Unique ID for this language used by searchcode, which can be used in other API calls. |
-| **count** | Total number of results that are written in this language. |
-| **language** | The name of this language. |
-| **source_filters** | Returns an array containing sources that exist in the result set. |
-| **id** | Unique ID for this source used by searchcode, which can be used in other API calls. |
-| **count** | Total number of results that belong to this source. |
-| **source** | The name of this source. |
-| **results** | Returns an array containing the matching code results. |
-| **id** | Unique ID for this code result used by searchcode, which can be used in other API calls. |
-| **filename** | The filename for this file. |
-| **repo** | HTML link to the location of the repository where this code was found. |
-| **linescount** | Total number of lines in the matching file. |
-| **location** | Location inside the repository where this file exists. |
-| **name** | Name of the repository that this file belongs to. |
-| **language** | The identified language of this result. |
-| **url** | URL to searchcode's location of the file. |
-| **md5hash** | Calculated MD5 hash of the file's contents. |
-| **lines** | Contains line numbers and lines which match the `searchterm`. Lines immediately before and after the match are included. If only the filename matches, up to the first 15 lines of the file are returned. |
-
___
### Code Result