diff --git a/README.md b/README.md index d9522cb..36f64f9 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") @@ -163,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 @@ -216,7 +182,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..fd52a59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "searchcode" -version = "0.5.1" +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 2c96917..0000000 --- a/src/searchcode/__app.py +++ /dev/null @@ -1,184 +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.console import Console -from rich.pretty import pprint -from rich.syntax import Syntax - -from . import License -from .__lib import ( - __pkg__, - __version__, - update_window_title, - clear_screen, - print_jsonp, - print_panels, -) -from .api import Searchcode - -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(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 = 0, - per_page: int = 100, - 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 with search string: [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 data for code file with ID: [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/__init__.py b/src/searchcode/__init__.py index 6827e3e..a541d0a 100644 --- a/src/searchcode/__init__.py +++ b/src/searchcode/__init__.py @@ -20,9 +20,11 @@ from .api import Searchcode __pkg__ = "searchcode" -__version__ = "0.5.1" +__version__ = "0.6.0" __author__ = "Ritchie Mwewa" +__all__ = ["Searchcode"] + class License: terms_and_conditions: str = """ diff --git a/src/searchcode/__lib.py b/src/searchcode/__lib.py deleted file mode 100644 index 7bde606..0000000 --- a/src/searchcode/__lib.py +++ /dev/null @@ -1,96 +0,0 @@ -import os -import subprocess -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 - -from . import __pkg__, __version__ - -console = Console() - - -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. - """ - console.set_window_title(f"{__pkg__.capitalize()} v{__version__} - {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"]) 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..94e833e --- /dev/null +++ b/src/searchcode/_cli/app.py @@ -0,0 +1,243 @@ +""" +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 +from types import SimpleNamespace + +import rich_click as click + +from .panels import console, print_panels +from .. import __pkg__, __version__, License +from .._lib import ( + clear_screen, + namespace_to_dict, + update_window_title, +) +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()) + + +@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_panels(data=response) + return + + # normal paginated search + 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, + pages=pages, + languages=languages, + sources=sources, + lines_of_code_lt=lines_of_code_lt, + lines_of_code_gt=lines_of_code_gt, + status=status, + ) + + 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( + 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], + status: console.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): + 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, + ) + 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 + 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 + + +@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"Getting code file [cyan]{id}[/]..."): + data = sc.code(id) + 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/_lib.py b/src/searchcode/_lib.py new file mode 100644 index 0000000..538e861 --- /dev/null +++ b/src/searchcode/_lib.py @@ -0,0 +1,72 @@ +import os +import subprocess +import typing as t +from types import SimpleNamespace + + +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 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 ._cli.panels import console + + console.set_window_title(f"{__pkg__.capitalize()} v{__version__} - {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"]) diff --git a/src/searchcode/api.py b/src/searchcode/api.py index b476dca..d631607 100644 --- a/src/searchcode/api.py +++ b/src/searchcode/api.py @@ -21,7 +21,8 @@ import requests -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"] @@ -36,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, @@ -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 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] """