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]
"""