mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 00:15:05 +00:00
Merge branch 'main' of github.com:oraios/serena
This commit is contained in:
commit
3dc5a25d77
16 files changed
+419
-223
No files matched your search
@@ -140,3 +140,6 @@ pylint.html
|
||||
/*.yml
|
||||
!myproject.demo.yml
|
||||
|
||||
|
||||
# Cache is in .multilspy
|
||||
**/.multilspy/**
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"cSpell.words": [
|
||||
"multilspy"
|
||||
]
|
||||
}
|
||||
@@ -144,8 +144,7 @@ def read_file(ctx: Context, relative_path: str) -> str:
|
||||
|
||||
class ReadFileTool(Tool):
|
||||
def _execute(self):
|
||||
with self.langsrv.open_file(relative_path):
|
||||
return self.langsrv.get_open_file_text(relative_path)
|
||||
return self.langsrv.retrieve_full_file_content(relative_path)
|
||||
|
||||
return ReadFileTool(ctx).execute()
|
||||
|
||||
|
||||
@@ -11,12 +11,13 @@ import dataclasses
|
||||
import hashlib
|
||||
import json
|
||||
import pickle
|
||||
import time
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import threading
|
||||
from contextlib import asynccontextmanager, contextmanager
|
||||
|
||||
from serena.text_utils import MatchedConsecutiveLines, LineType, TextLine
|
||||
from .lsp_protocol_handler.lsp_constants import LSPConstants
|
||||
from .lsp_protocol_handler import lsp_types as LSPTypes
|
||||
|
||||
@@ -34,6 +35,15 @@ from typing import AsyncIterator, Iterator, List, Dict, Optional, Union, Tuple
|
||||
from .type_helpers import ensure_all_methods_implemented
|
||||
|
||||
|
||||
# Serena dependencies
|
||||
# We will need to watch out for circular imports, but it's probably better to not
|
||||
# move all generic util code from serena into multilspy.
|
||||
# It does however make sense to integrate many text-related utils into the language server
|
||||
# since it caches (in-memory) file contents, so we can avoid reading from disk.
|
||||
# Moreover, the way we want to use the language server (for retrieving actual content),
|
||||
# it makes sense to have more content-related utils directly in it.
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class LSPFileBuffer:
|
||||
"""
|
||||
@@ -68,7 +78,6 @@ class LanguageServer:
|
||||
The LanguageServer class provides a language agnostic interface to the Language Server Protocol.
|
||||
It is used to communicate with Language Servers of different programming languages.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def create(cls, config: MultilspyConfig, logger: MultilspyLogger, repository_root_path: str) -> "LanguageServer":
|
||||
"""
|
||||
@@ -347,28 +356,6 @@ class LanguageServer:
|
||||
)
|
||||
return deleted_text
|
||||
|
||||
def get_open_file_text(self, relative_file_path: str) -> str:
|
||||
"""
|
||||
Get the contents of the given opened file as per the Language Server.
|
||||
|
||||
:param relative_file_path: The relative path of the file to open.
|
||||
"""
|
||||
if not self.server_started:
|
||||
self.logger.log(
|
||||
"get_open_file_text called before Language Server started",
|
||||
logging.ERROR,
|
||||
)
|
||||
raise MultilspyException("Language Server not started")
|
||||
|
||||
absolute_file_path = str(PurePath(self.repository_root_path, relative_file_path))
|
||||
uri = pathlib.Path(absolute_file_path).as_uri()
|
||||
|
||||
# Ensure the file is open
|
||||
assert uri in self.open_file_buffers
|
||||
|
||||
file_buffer = self.open_file_buffers[uri]
|
||||
return file_buffer.contents
|
||||
|
||||
async def request_definition(
|
||||
self, relative_file_path: str, line: int, column: int
|
||||
) -> List[multilspy_types.Location]:
|
||||
@@ -514,7 +501,62 @@ class LanguageServer:
|
||||
ret.append(multilspy_types.Location(**new_item))
|
||||
|
||||
return ret
|
||||
|
||||
async def request_references_with_content(
|
||||
self, relative_file_path: str, line: int, column: int, context_lines_before: int = 0, context_lines_after: int = 0
|
||||
) -> List[MatchedConsecutiveLines]:
|
||||
"""
|
||||
Like request_references, but returns the content of the lines containing the references, not just the locations.
|
||||
|
||||
:param relative_file_path: The relative path of the file that has the symbol for which references should be looked up
|
||||
:param line: The line number of the symbol
|
||||
:param column: The column number of the symbol
|
||||
:param context_lines_before: The number of lines to include in the context before the line containing the reference
|
||||
:param context_lines_after: The number of lines to include in the context after the line containing the reference
|
||||
|
||||
:return: A list of MatchedConsecutiveLines objects, one for each reference.
|
||||
"""
|
||||
references = await self.request_references(relative_file_path, line, column)
|
||||
return [self.retrieve_content_around_line(ref["relativePath"], ref["range"]["start"]["line"], context_lines_before, context_lines_after) for ref in references]
|
||||
|
||||
def retrieve_full_file_content(self, relative_file_path: str) -> str:
|
||||
"""
|
||||
Retrieve the full content of the given file.
|
||||
"""
|
||||
with self.open_file(relative_file_path) as file_data:
|
||||
return file_data.contents
|
||||
|
||||
def retrieve_content_around_line(self, relative_file_path: str, line: int, context_lines_before: int = 0, context_lines_after: int = 0) -> MatchedConsecutiveLines:
|
||||
"""
|
||||
Retrieve the content of the given file around the given line.
|
||||
|
||||
:param relative_file_path: The relative path of the file to retrieve the content from
|
||||
:param line: The line number to retrieve the content around
|
||||
:param context_lines_before: The number of lines to retrieve before the given line
|
||||
:param context_lines_after: The number of lines to retrieve after the given line
|
||||
|
||||
:return MatchedConsecutiveLines: A container with the desired lines.
|
||||
"""
|
||||
with self.open_file(relative_file_path) as file_data:
|
||||
file_contents = file_data.contents
|
||||
|
||||
line_contents = file_contents.split("\n")
|
||||
start_lineno = max(0, line - context_lines_before)
|
||||
end_lineno = min(len(line_contents) - 1, line + context_lines_after)
|
||||
# instantiate TextLines with the write LineType
|
||||
text_lines: list[TextLine] = []
|
||||
# before the line
|
||||
for lineno in range(start_lineno, line):
|
||||
text_lines.append(TextLine(line_number=lineno, line_content=line_contents[lineno], match_type=LineType.BEFORE_MATCH))
|
||||
# the line
|
||||
text_lines.append(TextLine(line_number=line, line_content=line_contents[line], match_type=LineType.MATCH))
|
||||
# after the line
|
||||
for lineno in range(line + 1, end_lineno + 1):
|
||||
text_lines.append(TextLine(line_number=lineno, line_content=line_contents[lineno], match_type=LineType.AFTER_MATCH))
|
||||
|
||||
return MatchedConsecutiveLines(lines=text_lines, source_file_path=relative_file_path)
|
||||
|
||||
|
||||
async def request_completions(
|
||||
self, relative_file_path: str, line: int, column: int, allow_incomplete: bool = False
|
||||
) -> List[multilspy_types.CompletionItem]:
|
||||
@@ -702,6 +744,24 @@ class LanguageServer:
|
||||
|
||||
# ----------------------------- FROM HERE ON MODIFICATIONS BY MISCHA --------------------
|
||||
|
||||
def retrieve_symbol_body(self, symbol: multilspy_types.UnifiedSymbolInformation) -> str:
|
||||
"""
|
||||
Load the body of the given symbol. If the body is already contained in the symbol, just return it.
|
||||
"""
|
||||
existing_body = symbol.get("body", None)
|
||||
if existing_body:
|
||||
return existing_body
|
||||
|
||||
assert "location" in symbol
|
||||
symbol_start_line = symbol["location"]["range"]["start"]["line"]
|
||||
symbol_end_line = symbol["location"]["range"]["end"]["line"]
|
||||
assert "relativePath" in symbol["location"]
|
||||
symbol_file = self.retrieve_full_file_content(symbol["location"]["relativePath"])
|
||||
symbol_lines = symbol_file.split("\n")
|
||||
symbol_body = "\n".join(symbol_lines[symbol_start_line:symbol_end_line])
|
||||
return symbol_body
|
||||
|
||||
|
||||
async def request_parsed_files(self) -> list[str]:
|
||||
"""This is slow, as it finds all files by finding all symbols.
|
||||
|
||||
@@ -724,6 +784,7 @@ class LanguageServer:
|
||||
column: int,
|
||||
include_imports: bool = True,
|
||||
include_self: bool = False,
|
||||
include_body: bool = False,
|
||||
) -> List[multilspy_types.UnifiedSymbolInformation]:
|
||||
"""
|
||||
Finds all symbols that reference the symbol at the given location.
|
||||
@@ -738,6 +799,7 @@ class LanguageServer:
|
||||
will not be easily distinguishable from definitions.
|
||||
:param include_self: whether to include the references that is the "input symbol" itself.
|
||||
Only has an effect if the relative_file_path, line and column point to a symbol, for example a definition.
|
||||
:param include_body: whether to include the body of the symbols in the result.
|
||||
:return: List of symbols that reference the target symbol.
|
||||
"""
|
||||
if not self.server_started:
|
||||
@@ -763,7 +825,7 @@ class LanguageServer:
|
||||
with self.open_file(ref_path) as file_data:
|
||||
# Get the containing symbol for this reference
|
||||
containing_symbol = await self.request_containing_symbol(
|
||||
ref_path, ref_line, ref_col
|
||||
ref_path, ref_line, ref_col, include_body=include_body
|
||||
)
|
||||
if containing_symbol is None:
|
||||
# TODO: HORRIBLE HACK! I don't know how to do it better for now...
|
||||
@@ -833,6 +895,7 @@ class LanguageServer:
|
||||
line: int,
|
||||
column: Optional[int] = None,
|
||||
strict: bool = False,
|
||||
include_body: bool = False,
|
||||
) -> multilspy_types.UnifiedSymbolInformation | None:
|
||||
"""
|
||||
Finds the first symbol containing the position for the given file.
|
||||
@@ -857,6 +920,7 @@ class LanguageServer:
|
||||
Setting to True is useful for example for finding the parent of a symbol, as with strict=False,
|
||||
and the line pointing to a symbol itself, the containing symbol will be the symbol itself
|
||||
(and not the parent).
|
||||
:param include_body: Whether to include the body of the symbol in the result.
|
||||
:return: The container symbol (if found) or None.
|
||||
"""
|
||||
# checking if the line is empty, unfortunately ugly and duplicating code, but I don't want to refactor
|
||||
@@ -938,13 +1002,19 @@ class LanguageServer:
|
||||
|
||||
if containing_symbols:
|
||||
# Return the one with the greatest starting position (i.e. the innermost container).
|
||||
return max(containing_symbols, key=lambda s: s["location"]["range"]["start"]["line"])
|
||||
containing_symbol = max(containing_symbols, key=lambda s: s["location"]["range"]["start"]["line"])
|
||||
if include_body:
|
||||
containing_symbol["body"] = self.retrieve_symbol_body(containing_symbol)
|
||||
return containing_symbol
|
||||
else:
|
||||
return None
|
||||
|
||||
async def request_container_of_symbol(self, symbol: multilspy_types.UnifiedSymbolInformation) -> multilspy_types.UnifiedSymbolInformation | None:
|
||||
async def request_container_of_symbol(self, symbol: multilspy_types.UnifiedSymbolInformation, include_body: bool = False) -> multilspy_types.UnifiedSymbolInformation | None:
|
||||
"""
|
||||
Finds the container of the given symbol if there is one.
|
||||
|
||||
:param symbol: The symbol to find the container of.
|
||||
:param include_body: whether to include the body of the symbol in the result.
|
||||
"""
|
||||
assert "location" in symbol
|
||||
return await self.request_containing_symbol(
|
||||
@@ -952,6 +1022,7 @@ class LanguageServer:
|
||||
symbol["location"]["range"]["start"]["line"],
|
||||
symbol["location"]["range"]["start"]["character"],
|
||||
strict=True,
|
||||
include_body=include_body,
|
||||
)
|
||||
|
||||
async def request_defining_symbol(
|
||||
@@ -959,6 +1030,7 @@ class LanguageServer:
|
||||
relative_file_path: str,
|
||||
line: int,
|
||||
column: int,
|
||||
include_body: bool = False,
|
||||
) -> Optional[multilspy_types.UnifiedSymbolInformation]:
|
||||
"""
|
||||
Finds the symbol that defines the symbol at the given location.
|
||||
@@ -969,6 +1041,7 @@ class LanguageServer:
|
||||
:param relative_file_path: The relative path to the file.
|
||||
:param line: The 0-indexed line number.
|
||||
:param column: The 0-indexed column number.
|
||||
:param include_body: whether to include the body of the symbol in the result.
|
||||
:return: The symbol information for the definition, or None if not found.
|
||||
"""
|
||||
if not self.server_started:
|
||||
@@ -991,7 +1064,7 @@ class LanguageServer:
|
||||
|
||||
# Find the symbol at or containing this location
|
||||
defining_symbol = await self.request_containing_symbol(
|
||||
def_path, def_line, def_col, strict=False
|
||||
def_path, def_line, def_col, strict=False, include_body=include_body
|
||||
)
|
||||
|
||||
return defining_symbol
|
||||
@@ -1080,14 +1153,6 @@ class SyncLanguageServer:
|
||||
Delete text between the given start and end positions in the given file and return the deleted text.
|
||||
"""
|
||||
return self.language_server.delete_text_between_positions(relative_file_path, start, end)
|
||||
|
||||
def get_open_file_text(self, relative_file_path: str) -> str:
|
||||
"""
|
||||
Get the contents of the given opened file as per the Language Server.
|
||||
|
||||
:param relative_file_path: The relative path of the file to open.
|
||||
"""
|
||||
return self.language_server.get_open_file_text(relative_file_path)
|
||||
|
||||
@contextmanager
|
||||
def start_server(self) -> Iterator["SyncLanguageServer"]:
|
||||
@@ -1137,7 +1202,26 @@ class SyncLanguageServer:
|
||||
self.language_server.request_references(file_path, line, column), self.loop
|
||||
).result()
|
||||
return result
|
||||
|
||||
def request_references_with_content(
|
||||
self, relative_file_path: str, line: int, column: int, context_lines_before: int = 0, context_lines_after: int = 0
|
||||
) -> List[MatchedConsecutiveLines]:
|
||||
"""
|
||||
Like request_references, but returns the content of the lines containing the references, not just the locations.
|
||||
|
||||
:param relative_file_path: The relative path of the file that has the symbol for which references should be looked up
|
||||
:param line: The line number of the symbol
|
||||
:param column: The column number of the symbol
|
||||
:param context_lines_before: The number of lines to include in the context before the line containing the reference
|
||||
:param context_lines_after: The number of lines to include in the context after the line containing the reference
|
||||
|
||||
:return: A list of MatchedConsecutiveLines objects, one for each reference.
|
||||
"""
|
||||
result = asyncio.run_coroutine_threadsafe(
|
||||
self.language_server.request_references_with_content(relative_file_path, line, column, context_lines_before, context_lines_after), self.loop
|
||||
).result()
|
||||
return result
|
||||
|
||||
def request_completions(
|
||||
self, relative_file_path: str, line: int, column: int, allow_incomplete: bool = False
|
||||
) -> List[multilspy_types.CompletionItem]:
|
||||
@@ -1189,6 +1273,15 @@ class SyncLanguageServer:
|
||||
|
||||
# ----------------------------- FROM HERE ON MODIFICATIONS BY MISCHA --------------------
|
||||
|
||||
def retrieve_symbol_body(self, symbol: multilspy_types.UnifiedSymbolInformation) -> str:
|
||||
"""
|
||||
Load the body of the given symbol. If the body is already contained in the symbol, just return it.
|
||||
|
||||
:param symbol: The symbol to retrieve the body of.
|
||||
:return: The body of the symbol.
|
||||
"""
|
||||
return self.language_server.retrieve_symbol_body(symbol)
|
||||
|
||||
def request_parsed_files(self) -> list[str]:
|
||||
"""This is slow, as it finds all files by finding all symbols.
|
||||
|
||||
@@ -1202,6 +1295,7 @@ class SyncLanguageServer:
|
||||
def request_referencing_symbols(
|
||||
self, relative_file_path: str, line: int, column: int,
|
||||
include_imports: bool = True, include_self: bool = False,
|
||||
include_body: bool = False,
|
||||
) -> List[multilspy_types.UnifiedSymbolInformation]:
|
||||
"""
|
||||
Finds all symbols that reference the symbol at the given location.
|
||||
@@ -1216,6 +1310,7 @@ class SyncLanguageServer:
|
||||
will not be easily distinguishable from definitions.
|
||||
:param include_self: whether to include the references that is the "input symbol" itself.
|
||||
Only has an effect if the relative_file_path, line and column point to a symbol, for example a definition.
|
||||
:param include_body: whether to include the body of the symbols in the result.
|
||||
:return: List of symbols that reference the target symbol.
|
||||
"""
|
||||
assert self.loop
|
||||
@@ -1225,7 +1320,8 @@ class SyncLanguageServer:
|
||||
line,
|
||||
column,
|
||||
include_imports=include_imports,
|
||||
include_self=include_self
|
||||
include_self=include_self,
|
||||
include_body=include_body,
|
||||
),
|
||||
self.loop
|
||||
).result()
|
||||
@@ -1233,7 +1329,8 @@ class SyncLanguageServer:
|
||||
|
||||
def request_containing_symbol(
|
||||
self, relative_file_path: str, line: int,
|
||||
column: Optional[int] = None, strict: bool = False
|
||||
column: Optional[int] = None, strict: bool = False,
|
||||
include_body: bool = False,
|
||||
) -> multilspy_types.UnifiedSymbolInformation | None:
|
||||
"""
|
||||
Finds the first symbol containing the position for the given file.
|
||||
@@ -1258,26 +1355,31 @@ class SyncLanguageServer:
|
||||
Setting to true is useful for example for finding the parent of a symbol, as with strict=False,
|
||||
and the line pointing to a symbol itself, the containing symbol will be the symbol itself
|
||||
(and not the parent).
|
||||
:param include_body: whether to include the body of the symbol in the result.
|
||||
:return: The container symbol (if found) or None.
|
||||
"""
|
||||
assert self.loop
|
||||
result = asyncio.run_coroutine_threadsafe(
|
||||
self.language_server.request_containing_symbol(relative_file_path, line, column=column, strict=strict), self.loop
|
||||
self.language_server.request_containing_symbol(relative_file_path, line, column=column, strict=strict, include_body=include_body), self.loop
|
||||
).result()
|
||||
return result
|
||||
|
||||
def request_container_of_symbol(self, symbol: multilspy_types.UnifiedSymbolInformation) -> multilspy_types.UnifiedSymbolInformation | None:
|
||||
def request_container_of_symbol(self, symbol: multilspy_types.UnifiedSymbolInformation, include_body: bool = False) -> multilspy_types.UnifiedSymbolInformation | None:
|
||||
"""
|
||||
Finds the container of the given symbol if there is one.
|
||||
|
||||
:param symbol: The symbol to find the container of.
|
||||
:param include_body: whether to include the body of the symbol in the result.
|
||||
"""
|
||||
assert self.loop
|
||||
result = asyncio.run_coroutine_threadsafe(
|
||||
self.language_server.request_container_of_symbol(symbol), self.loop
|
||||
self.language_server.request_container_of_symbol(symbol, include_body=include_body), self.loop
|
||||
).result()
|
||||
return result
|
||||
|
||||
def request_defining_symbol(
|
||||
self, relative_file_path: str, line: int, column: int
|
||||
self, relative_file_path: str, line: int, column: int,
|
||||
include_body: bool = False,
|
||||
) -> Optional[multilspy_types.UnifiedSymbolInformation]:
|
||||
"""
|
||||
Finds the symbol that defines the symbol at the given location.
|
||||
@@ -1288,14 +1390,33 @@ class SyncLanguageServer:
|
||||
:param relative_file_path: The relative path to the file.
|
||||
:param line: The 0-indexed line number.
|
||||
:param column: The 0-indexed column number.
|
||||
:param include_body: whether to include the body of the symbol in the result.
|
||||
:return: The symbol information for the definition, or None if not found.
|
||||
"""
|
||||
assert self.loop
|
||||
result = asyncio.run_coroutine_threadsafe(
|
||||
self.language_server.request_defining_symbol(relative_file_path, line, column), self.loop
|
||||
self.language_server.request_defining_symbol(relative_file_path, line, column, include_body=include_body), self.loop
|
||||
).result()
|
||||
return result
|
||||
|
||||
def retrieve_full_file_content(self, relative_file_path: str) -> str:
|
||||
"""
|
||||
Retrieve the full content of the given file.
|
||||
"""
|
||||
return self.language_server.retrieve_full_file_content(relative_file_path)
|
||||
|
||||
def retrieve_content_around_line(self, relative_file_path: str, line: int, context_lines_before: int = 0, context_lines_after: int = 0) -> MatchedConsecutiveLines:
|
||||
"""
|
||||
Retrieve the content of the given file around the given line.
|
||||
|
||||
:param relative_file_path: The relative path of the file to retrieve the content from
|
||||
:param line: The line number to retrieve the content around
|
||||
:param context_lines_before: The number of lines to retrieve before the given line
|
||||
:param context_lines_after: The number of lines to retrieve after the given line
|
||||
:return MatchedConsecutiveLines: A container with the desired lines.
|
||||
"""
|
||||
return self.language_server.retrieve_content_around_line(relative_file_path, line, context_lines_before, context_lines_after)
|
||||
|
||||
def start(self) -> "SyncLanguageServer":
|
||||
"""
|
||||
Starts the language server process and connects to it. Call shutdown when ready.
|
||||
|
||||
@@ -211,6 +211,9 @@ class UnifiedSymbolInformation(TypedDict):
|
||||
selectionRange: NotRequired[Range]
|
||||
""" The range that should be selected and revealed when this symbol is being picked, e.g the name of a function.
|
||||
Must be contained by the `range`. """
|
||||
|
||||
body: NotRequired[str]
|
||||
""" The body of the symbol. """
|
||||
|
||||
TreeRepr = Dict[int, List['TreeRepr']]
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
"""Top-level module."""
|
||||
|
||||
|
||||
class SampleClass:
|
||||
"""Sample docstring. Note that init docstrings should be underneath the class and not the init method itself
|
||||
(this looks prettier in sphinx). They still will be rendered correctly in pycharm's quick documentation.
|
||||
|
||||
:param param: some parameter
|
||||
"""
|
||||
|
||||
def __init__(self, param: str | None = None):
|
||||
self.hello = "hello "
|
||||
self.param = param
|
||||
|
||||
def sample_method(self, name: str) -> str:
|
||||
""">>> from serena.sample_module import SampleClass
|
||||
>>>
|
||||
>>> greeter = SampleClass()
|
||||
>>> greeter.sample_method("Oraios AI")
|
||||
'hello Oraios AI'.
|
||||
|
||||
:param name:
|
||||
:return:
|
||||
"""
|
||||
return self.hello + name
|
||||
Whitespace-only changes.
@@ -1,6 +0,0 @@
|
||||
"""Module in a separate package."""
|
||||
|
||||
|
||||
def hello_stranger() -> None:
|
||||
"""Hello."""
|
||||
print("Hello from a separate package!")
|
||||
+31
-12
@@ -1,5 +1,5 @@
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
@@ -37,27 +37,46 @@ class TextLine:
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class TextSearchMatch:
|
||||
"""Represents a match found in a text file or a string."""
|
||||
class MatchedConsecutiveLines:
|
||||
"""Represents a collection of consecutive lines found through some criterion in a text file or a string.
|
||||
May include lines before, after, and matched.
|
||||
"""
|
||||
|
||||
context_lines: list[TextLine]
|
||||
lines: list[TextLine]
|
||||
"""All lines in the context of the match. At least one of them should be of match_type MATCH."""
|
||||
source_file_path: str | None = None
|
||||
"""Path to the file where the match was found."""
|
||||
"""Path to the file where the match was found (Metadata)."""
|
||||
|
||||
# set in post-init
|
||||
lines_before_matched: list[TextLine] = field(default_factory=list)
|
||||
matched_lines: list[TextLine] = field(default_factory=list)
|
||||
lines_after_matched: list[TextLine] = field(default_factory=list)
|
||||
|
||||
def __post_init__(self):
|
||||
for line in self.lines:
|
||||
if line.match_type == LineType.BEFORE_MATCH:
|
||||
self.lines_before_matched.append(line)
|
||||
elif line.match_type == LineType.MATCH:
|
||||
self.matched_lines.append(line)
|
||||
elif line.match_type == LineType.AFTER_MATCH:
|
||||
self.lines_after_matched.append(line)
|
||||
|
||||
assert len(self.matched_lines) > 0, "At least one matched line is required"
|
||||
|
||||
@property
|
||||
def start_line(self) -> int:
|
||||
return self.context_lines[0].line_number
|
||||
return self.lines[0].line_number
|
||||
|
||||
@property
|
||||
def end_line(self) -> int:
|
||||
return self.context_lines[-1].line_number
|
||||
return self.lines[-1].line_number
|
||||
|
||||
@property
|
||||
def num_matched_lines(self) -> int:
|
||||
return len([line for line in self.context_lines if line.match_type == LineType.MATCH])
|
||||
return len(self.matched_lines)
|
||||
|
||||
def to_display_string(self) -> str:
|
||||
return "\n".join([line.format_line() for line in self.context_lines])
|
||||
return "\n".join([line.format_line() for line in self.lines])
|
||||
|
||||
|
||||
def search_text(
|
||||
@@ -68,7 +87,7 @@ def search_text(
|
||||
context_lines_before: int = 0,
|
||||
context_lines_after: int = 0,
|
||||
is_glob: bool = False,
|
||||
) -> list[TextSearchMatch]:
|
||||
) -> list[MatchedConsecutiveLines]:
|
||||
"""
|
||||
Search for a pattern in text content. Supports both regex and glob-like patterns.
|
||||
|
||||
@@ -160,7 +179,7 @@ def search_text(
|
||||
|
||||
context_lines.append(TextLine(line_number=line_num, line_content=lines[i], match_type=match_type))
|
||||
|
||||
matches.append(TextSearchMatch(context_lines=context_lines, source_file_path=source_file_path))
|
||||
matches.append(MatchedConsecutiveLines(lines=context_lines, source_file_path=source_file_path))
|
||||
else:
|
||||
# Search line by line
|
||||
for i, line in enumerate(lines):
|
||||
@@ -183,6 +202,6 @@ def search_text(
|
||||
|
||||
context_lines.append(TextLine(line_number=context_line_num, line_content=lines[j], match_type=match_type))
|
||||
|
||||
matches.append(TextSearchMatch(context_lines=context_lines, source_file_path=source_file_path))
|
||||
matches.append(MatchedConsecutiveLines(lines=context_lines, source_file_path=source_file_path))
|
||||
|
||||
return matches
|
||||
@@ -2,6 +2,10 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from multilspy.language_server import SyncLanguageServer
|
||||
from multilspy.multilspy_config import Language, MultilspyConfig
|
||||
from multilspy.multilspy_logger import MultilspyLogger
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def resources_dir() -> Path:
|
||||
@@ -13,3 +17,22 @@ def resources_dir() -> Path:
|
||||
@pytest.fixture(scope="session")
|
||||
def repo_path() -> Path:
|
||||
return Path(__file__).parent / "resources" / "test_repo"
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def language_server(repo_path: Path):
|
||||
"""Create a SyncLanguageServer instance configured to use the test repository."""
|
||||
config = MultilspyConfig(code_language=Language.PYTHON)
|
||||
logger = MultilspyLogger()
|
||||
|
||||
# Create a language server instance
|
||||
server = SyncLanguageServer.create(config, logger, str(repo_path))
|
||||
|
||||
# Start the server
|
||||
server.start()
|
||||
|
||||
try:
|
||||
yield server
|
||||
finally:
|
||||
# Ensure server is shut down
|
||||
server.stop()
|
||||
@@ -0,0 +1,167 @@
|
||||
"""
|
||||
Basic integration tests for the language server functionality.
|
||||
|
||||
These tests validate the functionality of the language server APIs
|
||||
like request_references using the test repository.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from multilspy.language_server import SyncLanguageServer
|
||||
from serena.text_utils import LineType
|
||||
|
||||
|
||||
class TestLanguageServerBasics:
|
||||
"""Test basic functionality of the language server."""
|
||||
|
||||
def test_request_references_user_class(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
"""Test request_references on the User class."""
|
||||
# Get references to the User class in models.py
|
||||
file_path = str(repo_path / "test_repo" / "models.py")
|
||||
# Line 31 contains the User class definition
|
||||
references = language_server.request_references(file_path, 31, 6)
|
||||
|
||||
# User class should be referenced in multiple files
|
||||
assert len(references) > 0
|
||||
|
||||
# At least two references should be found (one for the class definition itself)
|
||||
assert len(references) > 1
|
||||
|
||||
def test_request_references_item_class(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
"""Test request_references on the Item class."""
|
||||
# Get references to the Item class in models.py
|
||||
file_path = str(repo_path / "test_repo" / "models.py")
|
||||
# Line 56 contains the Item class definition
|
||||
references = language_server.request_references(file_path, 56, 6)
|
||||
|
||||
# Item class should be referenced in multiple places
|
||||
assert len(references) > 0
|
||||
|
||||
# At least one reference should be in services.py (ItemService class)
|
||||
services_references = [ref for ref in references if "services.py" in ref["uri"]]
|
||||
assert len(services_references) > 0
|
||||
|
||||
def test_request_references_function_parameter(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
"""Test request_references on a function parameter."""
|
||||
# Get references to the id parameter in get_user method
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
# Line 24 contains the get_user method with id parameter
|
||||
references = language_server.request_references(file_path, 24, 16)
|
||||
|
||||
# id parameter should be referenced within the method
|
||||
assert len(references) > 0
|
||||
|
||||
def test_request_references_create_user_method(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
# Get references to the create_user method in UserService
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
# Line 15 contains the create_user method definition
|
||||
references = language_server.request_references(file_path, 15, 9)
|
||||
|
||||
# Verify that we get valid references
|
||||
assert len(references) > 1
|
||||
|
||||
def test_retrieve_content_around_line(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
"""Test retrieve_content_around_line functionality with various scenarios."""
|
||||
file_path = str(repo_path / "test_repo" / "models.py")
|
||||
|
||||
# Scenario 1: Just a single line (User class definition)
|
||||
line_31 = language_server.retrieve_content_around_line(file_path, 31)
|
||||
assert len(line_31.lines) == 1
|
||||
assert "class User(BaseModel):" in line_31.lines[0].line_content
|
||||
assert line_31.lines[0].line_number == 31
|
||||
assert line_31.lines[0].match_type == LineType.MATCH
|
||||
|
||||
# Scenario 2: Context above and below
|
||||
with_context_around_user = language_server.retrieve_content_around_line(file_path, 31, 2, 2)
|
||||
assert len(with_context_around_user.lines) == 5
|
||||
# Check line content
|
||||
assert "class User(BaseModel):" in with_context_around_user.matched_lines[0].line_content
|
||||
assert with_context_around_user.num_matched_lines == 1
|
||||
assert " User model representing a system user." in with_context_around_user.lines[4].line_content
|
||||
# Check line numbers
|
||||
assert with_context_around_user.lines[0].line_number == 29
|
||||
assert with_context_around_user.lines[1].line_number == 30
|
||||
assert with_context_around_user.lines[2].line_number == 31
|
||||
assert with_context_around_user.lines[3].line_number == 32
|
||||
assert with_context_around_user.lines[4].line_number == 33
|
||||
# Check match types
|
||||
assert with_context_around_user.lines[0].match_type == LineType.BEFORE_MATCH
|
||||
assert with_context_around_user.lines[1].match_type == LineType.BEFORE_MATCH
|
||||
assert with_context_around_user.lines[2].match_type == LineType.MATCH
|
||||
assert with_context_around_user.lines[3].match_type == LineType.AFTER_MATCH
|
||||
assert with_context_around_user.lines[4].match_type == LineType.AFTER_MATCH
|
||||
|
||||
# Scenario 3a: Only context above
|
||||
with_context_above = language_server.retrieve_content_around_line(file_path, 31, 3, 0)
|
||||
assert len(with_context_above.lines) == 4
|
||||
assert "return cls(id=id, name=name)" in with_context_above.lines[0].line_content
|
||||
assert "class User(BaseModel):" in with_context_above.matched_lines[0].line_content
|
||||
assert with_context_above.num_matched_lines == 1
|
||||
# Check line numbers
|
||||
assert with_context_above.lines[0].line_number == 28
|
||||
assert with_context_above.lines[1].line_number == 29
|
||||
assert with_context_above.lines[2].line_number == 30
|
||||
assert with_context_above.lines[3].line_number == 31
|
||||
# Check match types
|
||||
assert with_context_above.lines[0].match_type == LineType.BEFORE_MATCH
|
||||
assert with_context_above.lines[1].match_type == LineType.BEFORE_MATCH
|
||||
assert with_context_above.lines[2].match_type == LineType.BEFORE_MATCH
|
||||
assert with_context_above.lines[3].match_type == LineType.MATCH
|
||||
|
||||
# Scenario 3b: Only context below
|
||||
with_context_below = language_server.retrieve_content_around_line(file_path, 31, 0, 3)
|
||||
assert len(with_context_below.lines) == 4
|
||||
assert "class User(BaseModel):" in with_context_below.matched_lines[0].line_content
|
||||
assert with_context_below.num_matched_lines == 1
|
||||
assert with_context_below.lines[0].line_number == 31
|
||||
assert with_context_below.lines[1].line_number == 32
|
||||
assert with_context_below.lines[2].line_number == 33
|
||||
assert with_context_below.lines[3].line_number == 34
|
||||
# Check match types
|
||||
assert with_context_below.lines[0].match_type == LineType.MATCH
|
||||
assert with_context_below.lines[1].match_type == LineType.AFTER_MATCH
|
||||
assert with_context_below.lines[2].match_type == LineType.AFTER_MATCH
|
||||
assert with_context_below.lines[3].match_type == LineType.AFTER_MATCH
|
||||
|
||||
# Scenario 4a: Edge case - context above but line is at 0
|
||||
first_line_with_context_around = language_server.retrieve_content_around_line(file_path, 0, 2, 1)
|
||||
assert len(first_line_with_context_around.lines) <= 4 # Should have at most 4 lines (line 0 + 1 below + up to 2 above)
|
||||
assert first_line_with_context_around.lines[0].line_number <= 2 # First line should be at most line 2
|
||||
# Check match type for the target line
|
||||
for line in first_line_with_context_around.lines:
|
||||
if line.line_number == 0:
|
||||
assert line.match_type == LineType.MATCH
|
||||
elif line.line_number < 0:
|
||||
assert line.match_type == LineType.BEFORE_MATCH
|
||||
else:
|
||||
assert line.match_type == LineType.AFTER_MATCH
|
||||
|
||||
# Scenario 4b: Edge case - context above but line is at 1
|
||||
second_line_with_context_above = language_server.retrieve_content_around_line(file_path, 1, 3, 1)
|
||||
assert len(second_line_with_context_above.lines) <= 5 # Should have at most 5 lines (line 1 + 1 below + up to 3 above)
|
||||
assert second_line_with_context_above.lines[0].line_number <= 1 # First line should be at most line 1
|
||||
# Check match type for the target line
|
||||
for line in second_line_with_context_above.lines:
|
||||
if line.line_number == 1:
|
||||
assert line.match_type == LineType.MATCH
|
||||
elif line.line_number < 1:
|
||||
assert line.match_type == LineType.BEFORE_MATCH
|
||||
else:
|
||||
assert line.match_type == LineType.AFTER_MATCH
|
||||
|
||||
# Scenario 4c: Edge case - context below but line is at the end of file
|
||||
# First get the total number of lines in the file
|
||||
all_content = language_server.retrieve_full_file_content(file_path)
|
||||
total_lines = len(all_content.split("\n"))
|
||||
|
||||
last_line_with_context_around = language_server.retrieve_content_around_line(file_path, total_lines - 1, 1, 3)
|
||||
assert len(last_line_with_context_around.lines) <= 5 # Should have at most 5 lines (last line + 1 above + up to 3 below)
|
||||
assert last_line_with_context_around.lines[-1].line_number >= total_lines - 4 # Last line should be at least total_lines - 4
|
||||
# Check match type for the target line
|
||||
for line in last_line_with_context_around.lines:
|
||||
if line.line_number == total_lines - 1:
|
||||
assert line.match_type == LineType.MATCH
|
||||
elif line.line_number < total_lines - 1:
|
||||
assert line.match_type == LineType.BEFORE_MATCH
|
||||
else:
|
||||
assert line.match_type == LineType.AFTER_MATCH
|
||||
@@ -1,83 +0,0 @@
|
||||
"""
|
||||
Basic integration tests for the language server functionality.
|
||||
|
||||
These tests validate the functionality of the language server APIs
|
||||
like request_references using the test repository.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from multilspy.language_server import SyncLanguageServer
|
||||
from multilspy.multilspy_config import Language, MultilspyConfig
|
||||
from multilspy.multilspy_logger import MultilspyLogger
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def language_server(repo_path: Path):
|
||||
"""Create a SyncLanguageServer instance configured to use the test repository."""
|
||||
config = MultilspyConfig(code_language=Language.PYTHON)
|
||||
logger = MultilspyLogger()
|
||||
|
||||
# Create a language server instance
|
||||
server = SyncLanguageServer.create(config, logger, str(repo_path))
|
||||
|
||||
# Start the server
|
||||
server.start()
|
||||
|
||||
try:
|
||||
yield server
|
||||
finally:
|
||||
# Ensure server is shut down
|
||||
server.stop()
|
||||
|
||||
|
||||
class TestLanguageServerBasics:
|
||||
"""Test basic functionality of the language server."""
|
||||
|
||||
def test_request_references_user_class(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
"""Test request_references on the User class."""
|
||||
# Get references to the User class in models.py
|
||||
file_path = str(repo_path / "test_repo" / "models.py")
|
||||
# Line 31 contains the User class definition
|
||||
references = language_server.request_references(file_path, 31, 6)
|
||||
|
||||
# User class should be referenced in multiple files
|
||||
assert len(references) > 0
|
||||
|
||||
# At least two references should be found (one for the class definition itself)
|
||||
assert len(references) > 1
|
||||
|
||||
def test_request_references_item_class(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
"""Test request_references on the Item class."""
|
||||
# Get references to the Item class in models.py
|
||||
file_path = str(repo_path / "test_repo" / "models.py")
|
||||
# Line 56 contains the Item class definition
|
||||
references = language_server.request_references(file_path, 56, 6)
|
||||
|
||||
# Item class should be referenced in multiple places
|
||||
assert len(references) > 0
|
||||
|
||||
# At least one reference should be in services.py (ItemService class)
|
||||
services_references = [ref for ref in references if "services.py" in ref["uri"]]
|
||||
assert len(services_references) > 0
|
||||
|
||||
def test_request_references_function_parameter(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
"""Test request_references on a function parameter."""
|
||||
# Get references to the id parameter in get_user method
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
# Line 24 contains the get_user method with id parameter
|
||||
references = language_server.request_references(file_path, 24, 16)
|
||||
|
||||
# id parameter should be referenced within the method
|
||||
assert len(references) > 0
|
||||
|
||||
def test_request_references_create_user_method(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
# Get references to the create_user method in UserService
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
# Line 15 contains the create_user method definition
|
||||
references = language_server.request_references(file_path, 15, 9)
|
||||
|
||||
# Verify that we get valid references
|
||||
assert len(references) > 1
|
||||
+2
-24
@@ -8,33 +8,10 @@ These tests focus on the following methods:
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from multilspy.language_server import SyncLanguageServer
|
||||
from multilspy.multilspy_config import Language, MultilspyConfig
|
||||
from multilspy.multilspy_logger import MultilspyLogger
|
||||
from multilspy.multilspy_types import SymbolKind
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def language_server(repo_path: Path):
|
||||
"""Create a SyncLanguageServer instance configured to use the test repository."""
|
||||
config = MultilspyConfig(code_language=Language.PYTHON)
|
||||
logger = MultilspyLogger()
|
||||
|
||||
# Create a language server instance
|
||||
server = SyncLanguageServer.create(config, logger, str(repo_path))
|
||||
|
||||
# Start the server
|
||||
server.start()
|
||||
|
||||
try:
|
||||
yield server
|
||||
finally:
|
||||
# Ensure server is shut down
|
||||
server.stop()
|
||||
|
||||
|
||||
class TestLanguageServerSymbols:
|
||||
"""Test the language server's symbol-related functionality."""
|
||||
|
||||
@@ -43,12 +20,13 @@ class TestLanguageServerSymbols:
|
||||
# Test for a position inside the create_user method
|
||||
file_path = str(repo_path / "test_repo" / "services.py")
|
||||
# Line 17 is inside the create_user method body
|
||||
containing_symbol = language_server.request_containing_symbol(file_path, 17, 20)
|
||||
containing_symbol = language_server.request_containing_symbol(file_path, 17, 20, include_body=True)
|
||||
|
||||
# Verify that we found the containing symbol
|
||||
assert containing_symbol is not None
|
||||
assert containing_symbol["name"] == "create_user"
|
||||
assert containing_symbol["kind"] == SymbolKind.Method
|
||||
assert containing_symbol["body"].strip().startswith("def create_user(self")
|
||||
|
||||
def test_references_to_variables(self, language_server: SyncLanguageServer, repo_path: Path):
|
||||
"""Test request_referencing_symbols for a variable."""
|
||||
Binary file not shown.
@@ -1,8 +0,0 @@
|
||||
from serena.sample_module import SampleClass
|
||||
|
||||
|
||||
# the suggested naming convention for unit tests is test_method_name_testDescriptionInCamelCase
|
||||
# this leads to a nicely readable output of pytest
|
||||
def test_sample_class_attributes_greeterSaysHello():
|
||||
greeter = SampleClass()
|
||||
assert greeter.hello == "hello "
|
||||
@@ -21,7 +21,7 @@ class TestTextUtils:
|
||||
assert matches[0].num_matched_lines == 1
|
||||
assert matches[0].start_line == 3
|
||||
assert matches[0].end_line == 3
|
||||
assert matches[0].context_lines[0].line_content.strip() == 'print("Hello, World!")'
|
||||
assert matches[0].lines[0].line_content.strip() == 'print("Hello, World!")'
|
||||
|
||||
def test_search_text_with_regex_pattern(self):
|
||||
"""Test searching with a regex pattern."""
|
||||
@@ -42,10 +42,10 @@ class TestTextUtils:
|
||||
matches = search_text(pattern, content=content)
|
||||
|
||||
assert len(matches) == 3
|
||||
assert matches[0].context_lines[0].match_type == LineType.MATCH
|
||||
assert "def __init__" in matches[0].context_lines[0].line_content
|
||||
assert "def process" in matches[1].context_lines[0].line_content
|
||||
assert "def filter" in matches[2].context_lines[0].line_content
|
||||
assert matches[0].lines[0].match_type == LineType.MATCH
|
||||
assert "def __init__" in matches[0].lines[0].line_content
|
||||
assert "def process" in matches[1].lines[0].line_content
|
||||
assert "def filter" in matches[2].lines[0].line_content
|
||||
|
||||
def test_search_text_with_compiled_regex(self):
|
||||
"""Test searching with a pre-compiled regex pattern."""
|
||||
@@ -68,8 +68,8 @@ class TestTextUtils:
|
||||
matches = search_text(pattern, content=content)
|
||||
|
||||
assert len(matches) == 2
|
||||
assert "DEBUG = True" in matches[0].context_lines[0].line_content
|
||||
assert "MAX_RETRIES = 3" in matches[1].context_lines[0].line_content
|
||||
assert "DEBUG = True" in matches[0].lines[0].line_content
|
||||
assert "MAX_RETRIES = 3" in matches[1].lines[0].line_content
|
||||
|
||||
def test_search_text_with_context_lines(self):
|
||||
"""Test searching with context lines before and after the match."""
|
||||
@@ -91,15 +91,15 @@ class TestTextUtils:
|
||||
|
||||
# Check the first match with context
|
||||
first_match = matches[0]
|
||||
assert len(first_match.context_lines) == 3
|
||||
assert first_match.context_lines[0].match_type == LineType.BEFORE_MATCH
|
||||
assert first_match.context_lines[1].match_type == LineType.MATCH
|
||||
assert first_match.context_lines[2].match_type == LineType.AFTER_MATCH
|
||||
assert len(first_match.lines) == 3
|
||||
assert first_match.lines[0].match_type == LineType.BEFORE_MATCH
|
||||
assert first_match.lines[1].match_type == LineType.MATCH
|
||||
assert first_match.lines[2].match_type == LineType.AFTER_MATCH
|
||||
|
||||
# Verify the content of lines
|
||||
assert "if a > b:" in first_match.context_lines[0].line_content
|
||||
assert "return a * c" in first_match.context_lines[1].line_content
|
||||
assert "elif b > a:" in first_match.context_lines[2].line_content
|
||||
assert "if a > b:" in first_match.lines[0].line_content
|
||||
assert "return a * c" in first_match.lines[1].line_content
|
||||
assert "elif b > a:" in first_match.lines[2].line_content
|
||||
|
||||
def test_search_text_with_multiline_match(self):
|
||||
"""Test searching with multiline pattern matching."""
|
||||
@@ -120,10 +120,10 @@ class TestTextUtils:
|
||||
assert len(matches) == 1
|
||||
multiline_match = matches[0]
|
||||
assert multiline_match.num_matched_lines >= 3
|
||||
assert "if n <= 1:" in multiline_match.context_lines[0].line_content
|
||||
assert "if n <= 1:" in multiline_match.lines[0].line_content
|
||||
|
||||
# All matched lines should have match_type == LineType.MATCH
|
||||
match_lines = [line for line in multiline_match.context_lines if line.match_type == LineType.MATCH]
|
||||
match_lines = [line for line in multiline_match.lines if line.match_type == LineType.MATCH]
|
||||
assert len(match_lines) >= 3
|
||||
|
||||
def test_search_text_with_glob_pattern(self):
|
||||
@@ -146,9 +146,9 @@ class TestTextUtils:
|
||||
matches = search_text("*_user*", content=content, is_glob=True)
|
||||
|
||||
assert len(matches) == 3
|
||||
assert "get_user" in matches[0].context_lines[0].line_content
|
||||
assert "create_user" in matches[1].context_lines[0].line_content
|
||||
assert "update_user" in matches[2].context_lines[0].line_content
|
||||
assert "get_user" in matches[0].lines[0].line_content
|
||||
assert "create_user" in matches[1].lines[0].line_content
|
||||
assert "update_user" in matches[2].lines[0].line_content
|
||||
|
||||
def test_search_text_with_complex_glob_pattern(self):
|
||||
"""Test searching with more complex glob patterns."""
|
||||
@@ -175,7 +175,7 @@ class TestTextUtils:
|
||||
instance_matches = [
|
||||
line.line_content
|
||||
for match in matches
|
||||
for line in match.context_lines
|
||||
for line in match.lines
|
||||
if line.match_type == LineType.MATCH and "isinstance(item," in line.line_content
|
||||
]
|
||||
assert len(instance_matches) >= 2
|
||||
|
||||
Reference in new issue
Block a user