Skip to content

Commit

Permalink
search request + tests + schema + case-sensitive (jupyterlab-contrib#15)
Browse files Browse the repository at this point in the history
* change route for search

* add test for no result + schema for server response

* adhere to lint

* fix Manifest

* suggested changes and schema validation

* add option for case-sensitive
  • Loading branch information
madhur-tandon authored Feb 21, 2022
1 parent 8b8f049 commit aaddbed
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 16 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ include README.md
include RELEASE.md
include pyproject.toml
recursive-include jupyter-config *.json
recursive-include jupyterlab_search_replace *.json
include conftest.py

include package.json
Expand Down
7 changes: 4 additions & 3 deletions jupyterlab_search_replace/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ def initialize(self) -> None:

@tornado.web.authenticated
async def get(self, path: str = ""):
regex = self.get_query_argument("regex")
query = self.get_query_argument("query")
max_count = self.get_query_argument("max_count", 100)
r = await self._engine.search(regex, path, max_count)
case_sensitive = self.get_query_argument("case_sensitive", False)
r = await self._engine.search(query, path, max_count, case_sensitive)

if r.get("code") is not None:
self.set_status(500)
Expand All @@ -29,6 +30,6 @@ def setup_handlers(web_app):
host_pattern = ".*$"

base_url = web_app.settings["base_url"]
route_pattern = url_path_join(base_url, "search-regex" + path_regex)
route_pattern = url_path_join(base_url, "search" + path_regex)
handlers = [(route_pattern, RouteHandler)]
web_app.add_handlers(host_pattern, handlers)
32 changes: 30 additions & 2 deletions jupyterlab_search_replace/search_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@
MAX_LOG_OUTPUT = 6000 # type: int


def construct_command(query, max_count, case_sensitive):
command = ["rg", "-e", query, "--json", f"--max-count={max_count}"]
if not case_sensitive:
command.append("--ignore-case")
return command


class SearchEngine:
"""Engine to search recursively for a regex pattern in text files of a directory.
Expand Down Expand Up @@ -82,10 +89,16 @@ def log(self) -> logging.Logger:
"""logging.Logger : Extension logger"""
return get_logger()

async def search(self, regex: str, path: str = "", max_count: int = 100):
async def search(
self,
query: str,
path: str = "",
max_count: int = 100,
case_sensitive: bool = False,
):
""""""
# JSON output is described at https://docs.rs/grep-printer/0.1.0/grep_printer/struct.JSON.html
command = ["rg", "-e", regex, "--json", f"--max-count={max_count}"]
command = construct_command(query, max_count, case_sensitive)
cwd = os.path.join(self._root_dir, url2path(path))
code, output = await self._execute(command, cwd=cwd)

Expand Down Expand Up @@ -120,4 +133,19 @@ async def search(self, regex: str, path: str = "", max_count: int = 100):

return {"matches": matches_per_files}
else:
try:
output = json.loads(output)
if output["type"] == "summary":
stats = output["data"]["stats"]
if (
stats["matched_lines"] == 0
and stats["matches"] == 0
and stats["searches"] == 0
and stats["searches_with_match"] == 0
):
return {"matches": []}
except (json.JSONDecodeError, KeyError):
# If parsing the JSON fails or one key is missing
# consider the output as invalid
pass
return {"code": code, "command": command, "message": output}
10 changes: 9 additions & 1 deletion jupyterlab_search_replace/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import json
import shutil

import pytest
from pathlib import Path

SCHEMA_FILE = Path(__file__).parent / "schema.json"
TEST_PATH = "test_lab_search_replace"


@pytest.fixture
def schema():
return json.load(open(SCHEMA_FILE, "r"))


@pytest.fixture
def test_content(jp_root_dir):
full_test_path = jp_root_dir / TEST_PATH
Expand All @@ -15,7 +23,7 @@ def test_content(jp_root_dir):
[
"Unicode strange file, very strange",
"ü notebook with λ",
"Is that strange enough?",
"Is that Strange enough?",
]
)
)
Expand Down
77 changes: 77 additions & 0 deletions jupyterlab_search_replace/tests/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"title": "Model",
"type": "object",
"properties": {
"matches": {
"title": "Matches",
"type": "array",
"items": {
"$ref": "#/definitions/fileMatches"
}
}
},
"required": [
"matches"
],
"definitions": {
"match": {
"title": "match",
"type": "object",
"properties": {
"line": {
"title": "Line",
"type": "string"
},
"match": {
"title": "Match",
"type": "string"
},
"start": {
"title": "Start",
"type": "integer"
},
"end": {
"title": "End",
"type": "integer"
},
"line_number": {
"title": "Line Number",
"type": "integer"
},
"absolute_offset": {
"title": "Absolute Offset",
"type": "integer"
}
},
"required": [
"line",
"match",
"start",
"end",
"line_number",
"absolute_offset"
]
},
"fileMatches": {
"title": "fileMatches",
"type": "object",
"properties": {
"path": {
"title": "Path",
"type": "string"
},
"matches": {
"title": "Matches",
"type": "array",
"items": {
"$ref": "#/definitions/match"
}
}
},
"required": [
"path",
"matches"
]
}
}
}
49 changes: 41 additions & 8 deletions jupyterlab_search_replace/tests/test_handlers.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import json
from jsonschema import validate


async def test_search_get(test_content, jp_fetch):
# When
response = await jp_fetch("search-regex", params={"regex": "strange"}, method="GET")
async def test_search_get(test_content, schema, jp_fetch):
response = await jp_fetch("search", params={"query": "strange"}, method="GET")

# Then
assert response.code == 200
payload = json.loads(response.body)
print(payload)
validate(instance=payload, schema=schema)
assert len(payload["matches"]) == 2
assert len(payload["matches"][0]["matches"]) == 3
assert len(payload["matches"][1]["matches"]) == 3
assert payload["matches"] == [
assert sorted(payload["matches"], key=lambda x: x["path"]) == [
{
"path": "test_lab_search_replace/subfolder/text_sub.txt",
"matches": [
Expand Down Expand Up @@ -62,8 +61,8 @@ async def test_search_get(test_content, jp_fetch):
"absolute_offset": 0,
},
{
"line": "Is that strange enough?",
"match": "strange",
"line": "Is that Strange enough?",
"match": "Strange",
"start": 8,
"end": 15,
"line_number": 3,
Expand All @@ -72,3 +71,37 @@ async def test_search_get(test_content, jp_fetch):
],
},
]


async def test_search_no_match(test_content, schema, jp_fetch):
response = await jp_fetch("search", params={"query": "hello"}, method="GET")
assert response.code == 200
payload = json.loads(response.body)
validate(instance=payload, schema=schema)
assert len(payload["matches"]) == 0


async def test_search_case_sensitive(test_content, schema, jp_fetch):
response = await jp_fetch(
"search", params={"query": "Strange", "case_sensitive": True}, method="GET"
)
assert response.code == 200
payload = json.loads(response.body)
validate(instance=payload, schema=schema)
assert len(payload["matches"]) == 1
assert len(payload["matches"][0]["matches"]) == 1
assert sorted(payload["matches"], key=lambda x: x["path"]) == [
{
"path": "test_lab_search_replace/text_1.txt",
"matches": [
{
"line": "Is that Strange enough?",
"match": "Strange",
"start": 8,
"end": 15,
"line_number": 3,
"absolute_offset": 55,
},
],
}
]
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ tests=
pytest-asyncio
pytest-cov
pytest-tornasync
jsonschema
2 changes: 1 addition & 1 deletion src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export async function requestAPI<T>(
const settings = ServerConnection.makeSettings();
const requestUrl = URLExt.join(
settings.baseUrl,
'search-regex', // API Namespace
'search', // API Namespace
endPoint
);

Expand Down
2 changes: 1 addition & 1 deletion src/searchReplace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export class SearchReplaceModel extends VDomModel {
async getSearchString(search: string): Promise<void> {
try {
const data = await requestAPI<any>(
'?' + new URLSearchParams([['regex', search]]).toString(),
'?' + new URLSearchParams([['query', search]]).toString(),
{
method: 'GET'
}
Expand Down

0 comments on commit aaddbed

Please sign in to comment.