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