From 7e8b360538cd4c056671361bf9cafba316f39b86 Mon Sep 17 00:00:00 2001 From: Elifarley Date: Tue, 10 Dec 2024 13:35:35 -0300 Subject: [PATCH 1/4] chore: Bump grep-ast dependency to version 0.4.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 4510466..a94fb2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ classifiers = [ keywords = ["cedarscript", "code-editing", "refactoring", "code-analysis", "sql-like", "ai-assisted-development"] dependencies = [ "cedarscript-ast-parser>=0.6.1", - "grep-ast==0.3.3", + "grep-ast==0.4.1", # https://github.com/tree-sitter/py-tree-sitter/issues/303 # https://github.com/grantjenks/py-tree-sitter-languages/issues/64 "tree-sitter==0.21.3", # 0.22 breaks tree-sitter-languages From b70267508c1822f43134fbe7dfa5f457318fea67 Mon Sep 17 00:00:00 2001 From: Elifarley Date: Tue, 10 Dec 2024 14:11:00 -0300 Subject: [PATCH 2/4] pytest-cov>=6.0.0 --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index a94fb2e..98eae24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,6 +40,7 @@ Repository = "https://github.com/CEDARScript/cedarscript-editor-python.git" [project.optional-dependencies] dev = [ "pytest>=7.0", + "pytest-cov>=6.0.0", "black>=22.0", "isort>=5.0", "flake8>=4.0", From 582e57565d8cdbabc849ad05884763161cdcc5de Mon Sep 17 00:00:00 2001 From: Elifarley Date: Wed, 11 Dec 2024 02:26:36 -0300 Subject: [PATCH 3/4] tests: Support skipping tests that don't run under Windows; Add test for decorated identifiers --- tests/corpus/move.decorated-method/1.py | 24 +++++++++++++++++++ tests/corpus/move.decorated-method/chat.xml | 20 ++++++++++++++++ .../move.decorated-method/expected.1.py | 24 +++++++++++++++++++ .../1.py | 0 .../1.txt | 0 .../chat.xml | 0 .../expected.1.py | 0 .../expected.1.txt | 0 tests/test_corpus.py | 7 +++++- 9 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 tests/corpus/move.decorated-method/1.py create mode 100644 tests/corpus/move.decorated-method/chat.xml create mode 100644 tests/corpus/move.decorated-method/expected.1.py rename tests/corpus/{update.identifier.ed-script-filter => update.identifier.ed-script-filter!nowindows}/1.py (100%) rename tests/corpus/{update.identifier.ed-script-filter => update.identifier.ed-script-filter!nowindows}/1.txt (100%) rename tests/corpus/{update.identifier.ed-script-filter => update.identifier.ed-script-filter!nowindows}/chat.xml (100%) rename tests/corpus/{update.identifier.ed-script-filter => update.identifier.ed-script-filter!nowindows}/expected.1.py (100%) rename tests/corpus/{update.identifier.ed-script-filter => update.identifier.ed-script-filter!nowindows}/expected.1.txt (100%) diff --git a/tests/corpus/move.decorated-method/1.py b/tests/corpus/move.decorated-method/1.py new file mode 100644 index 0000000..77b936c --- /dev/null +++ b/tests/corpus/move.decorated-method/1.py @@ -0,0 +1,24 @@ +class SpyderKernel(IPythonKernel): + """Spyder kernel for Jupyter.""" + + shell_class = SpyderShell + @comm_handler + def safe_exec(self, filename): + """Safely execute a file using IPKernelApp._exec_file.""" + self.parent._exec_file(filename) + + @comm_handler + def get_fault_text(self, fault_filename, main_id, ignore_ids): + """Get fault text from old run.""" + # Read file + try: + with open(fault_filename, 'r') as f: + fault = f.read() + except FileNotFoundError: + return + return text + + def get_system_threads_id(self): + """Return the list of system threads id.""" + ignore_threads = [ + ] diff --git a/tests/corpus/move.decorated-method/chat.xml b/tests/corpus/move.decorated-method/chat.xml new file mode 100644 index 0000000..1ffa66e --- /dev/null +++ b/tests/corpus/move.decorated-method/chat.xml @@ -0,0 +1,20 @@ + +```CEDARScript +-- 1. Move the `get_fault_text` method from the `SpyderKernel` class to be a top-level function +UPDATE METHOD "SpyderKernel.get_fault_text" +FROM FILE "1.py" +MOVE WHOLE +INSERT BEFORE CLASS "SpyderKernel" +RELATIVE INDENTATION 0; + +-- 2. Update the copied function to remove references to `self` +UPDATE FUNCTION "get_fault_text" +FROM FILE r"1.py" +REPLACE WHOLE WITH CASE +WHEN REGEX r'''def get_fault_text\(''' THEN SUB +r'''def get_fault_text\(self, fault_filename, main_id, ignore_ids\):''' +r'''def get_fault_text(fault_filename, main_id, ignore_ids):''' +END; + +``` + diff --git a/tests/corpus/move.decorated-method/expected.1.py b/tests/corpus/move.decorated-method/expected.1.py new file mode 100644 index 0000000..c9e571c --- /dev/null +++ b/tests/corpus/move.decorated-method/expected.1.py @@ -0,0 +1,24 @@ +@comm_handler +def get_fault_text(fault_filename, main_id, ignore_ids): + """Get fault text from old run.""" + # Read file + try: + with open(fault_filename, 'r') as f: + fault = f.read() + except FileNotFoundError: + return + return text +class SpyderKernel(IPythonKernel): + """Spyder kernel for Jupyter.""" + + shell_class = SpyderShell + @comm_handler + def safe_exec(self, filename): + """Safely execute a file using IPKernelApp._exec_file.""" + self.parent._exec_file(filename) + + + def get_system_threads_id(self): + """Return the list of system threads id.""" + ignore_threads = [ + ] diff --git a/tests/corpus/update.identifier.ed-script-filter/1.py b/tests/corpus/update.identifier.ed-script-filter!nowindows/1.py similarity index 100% rename from tests/corpus/update.identifier.ed-script-filter/1.py rename to tests/corpus/update.identifier.ed-script-filter!nowindows/1.py diff --git a/tests/corpus/update.identifier.ed-script-filter/1.txt b/tests/corpus/update.identifier.ed-script-filter!nowindows/1.txt similarity index 100% rename from tests/corpus/update.identifier.ed-script-filter/1.txt rename to tests/corpus/update.identifier.ed-script-filter!nowindows/1.txt diff --git a/tests/corpus/update.identifier.ed-script-filter/chat.xml b/tests/corpus/update.identifier.ed-script-filter!nowindows/chat.xml similarity index 100% rename from tests/corpus/update.identifier.ed-script-filter/chat.xml rename to tests/corpus/update.identifier.ed-script-filter!nowindows/chat.xml diff --git a/tests/corpus/update.identifier.ed-script-filter/expected.1.py b/tests/corpus/update.identifier.ed-script-filter!nowindows/expected.1.py similarity index 100% rename from tests/corpus/update.identifier.ed-script-filter/expected.1.py rename to tests/corpus/update.identifier.ed-script-filter!nowindows/expected.1.py diff --git a/tests/corpus/update.identifier.ed-script-filter/expected.1.txt b/tests/corpus/update.identifier.ed-script-filter!nowindows/expected.1.txt similarity index 100% rename from tests/corpus/update.identifier.ed-script-filter/expected.1.txt rename to tests/corpus/update.identifier.ed-script-filter!nowindows/expected.1.txt diff --git a/tests/test_corpus.py b/tests/test_corpus.py index 02a2c15..c66d1ef 100644 --- a/tests/test_corpus.py +++ b/tests/test_corpus.py @@ -1,5 +1,6 @@ import re import shutil +import sys import tempfile import pytest from pathlib import Path @@ -40,6 +41,10 @@ def editor(tmp_path_factory): @pytest.mark.parametrize('test_case', get_test_cases()) def test_corpus(editor: CEDARScriptEditor, test_case: str): """Test CEDARScript commands from chat.xml files in corpus.""" + if test_case.casefold().endswith('!nowindows'): + if sys.platform == 'win32': + pytest.skip(f"Cannot run under Windows: {test_case.removesuffix('!nowindows')}") + try: corpus_dir = Path(__file__).parent / 'corpus' test_dir = corpus_dir / test_case @@ -93,7 +98,7 @@ def check_expected_files(dir_path: Path): if str(rel_path).startswith("."): continue expected_file = test_dir / f"expected.{rel_path}" - assert expected_file.exists(), f"'expecteed.*' file not found: {expected_file}" + assert expected_file.exists(), f"'expected.*' file not found: {expected_file}" expected_content = file_to_lines(expected_file, rel_path) actual_content = file_to_lines(path, rel_path) From 7e82824ecab90b36c40f71db335eadc72445dfc4 Mon Sep 17 00:00:00 2001 From: Elifarley Date: Wed, 11 Dec 2024 02:30:23 -0300 Subject: [PATCH 4/4] fix: handle decorated identifiers --- .../tree_sitter_identifier_finder.py | 62 +++++- .../tree_sitter_identifier_queries.py | 204 ++++++++---------- src/text_manipulation/range_spec.py | 13 +- src/text_manipulation/text_editor_kit.py | 6 +- 4 files changed, 159 insertions(+), 126 deletions(-) diff --git a/src/cedarscript_editor/tree_sitter_identifier_finder.py b/src/cedarscript_editor/tree_sitter_identifier_finder.py index dab8e30..c273a96 100644 --- a/src/cedarscript_editor/tree_sitter_identifier_finder.py +++ b/src/cedarscript_editor/tree_sitter_identifier_finder.py @@ -128,7 +128,8 @@ def find_identifiers( match identifier_type: case 'method': identifier_type = 'function' - candidate_nodes = self.language.query(self.query_info[identifier_type].format(name=name)).captures(self.tree.root_node) + _query = self.query_info[identifier_type].format(name=name) + candidate_nodes = self.language.query(_query).captures(self.tree.root_node) if not candidate_nodes: return [] # Convert captures to boundaries and filter by parent @@ -198,9 +199,9 @@ def parents(self) -> list[ParentInfo]: current = self.node.parent while current: - # Check if current node is a container type we care about - if current.type.endswith('_definition'): - # Try to find the name node - exact field depends on language + # Check if current node is a container type we care about - TODO exact field depends on language + if current.type.endswith('_definition') and current.type != 'decorated_definition': + # Try to find the name node - TODO exact field depends on language name = None for child in current.children: if child.type == 'identifier' or child.type == 'name': @@ -242,14 +243,13 @@ def associate_identifier_parts(captures: Iterable[CaptureInfo], lines: Sequence[ raise ValueError(f'Parent node not found for [{capture.capture_type} - {capture.node_type}] ({capture.node.text.decode("utf-8").strip()})') match capture_type: case 'body': - parent = parent._replace(body=range_spec) + parent.body=range_spec case 'docstring': - parent = parent._replace(docstring=range_spec) + parent.docstring=range_spec case 'decorator': - parent = parent.decorators.append(range_spec) + parent.append_decorator(range_spec) case _ as invalid: raise ValueError(f'Invalid capture type: {invalid}') - identifier_map[parent_key] = parent return sorted(identifier_map.values(), key=lambda x: x.whole.start) @@ -260,6 +260,8 @@ def find_parent_definition(node): while node.parent: node = node.parent if node.type.endswith('_definition'): + if node.type == 'decorated_definition': + node = node.named_children[0].next_named_sibling return node return None @@ -278,4 +280,46 @@ def capture2identifier_boundaries(captures, lines: Sequence[str]) -> list[Identi unique_captures = {} for capture in captures: unique_captures[f'{capture.range[0]}:{capture.capture_type}'] = capture - return associate_identifier_parts(unique_captures.values(), lines) + # unique_captures={ + # '157:function.decorator': CaptureInfo(capture_type='function.decorator', node=), + # '158:function.definition': CaptureInfo(capture_type='function.definition', node=), + # '159:function.body': CaptureInfo(capture_type='function.body', node=) + # } + return associate_identifier_parts(sort_captures(unique_captures), lines) + +def parse_capture_key(key): + """ + Parses the dictionary key into line number and capture type. + Args: + key (str): The key in the format 'line_number:capture_type'. + Returns: + tuple: (line_number as int, capture_type as str) + """ + line_number, capture_type = key.split(':') + return int(line_number), capture_type.split('.')[-1] + +def get_sort_priority(): + """ + Returns a dictionary mapping capture types to their sort priority. + Returns: + dict: Capture type priorities. + """ + return {'definition': 1, 'decorator': 2, 'body': 3, 'docstring': 4} + +def sort_captures(captures): + """ + Sorts the values of the captures dictionary by capture type and line number. + Args: + captures (dict): The dictionary to sort. + Returns: + list: Sorted list of values. + """ + priority = get_sort_priority() + sorted_items = sorted( + captures.items(), + key=lambda item: ( + priority[parse_capture_key(item[0])[1]], # Sort by capture type priority + parse_capture_key(item[0])[0] # Then by line number + ) + ) + return [value for _, value in sorted_items] diff --git a/src/cedarscript_editor/tree_sitter_identifier_queries.py b/src/cedarscript_editor/tree_sitter_identifier_queries.py index d232313..0253d9b 100644 --- a/src/cedarscript_editor/tree_sitter_identifier_queries.py +++ b/src/cedarscript_editor/tree_sitter_identifier_queries.py @@ -30,126 +30,110 @@ # except KeyError: # return +_common_template = """ + ; Common pattern for body and docstring capture + body: (block + . + (expression_statement + (string) @{type}.docstring)? + . + ) @{type}.body +""" + +_definition_base_template = """ + name: (identifier) @_{type}_name + (#match? @_{type}_name "^{{name}}$") + (#set! role name) +""" LANG_TO_TREE_SITTER_QUERY = { "python": { 'function': """ - ; Regular and async function definitions with optional docstring - (function_definition - name: (identifier) @_function_name - (#match? @_function_name "^{name}$") - body: (block) @function.body) @function.definition - +; Function Definitions +(function_definition + {definition_base} + {common_body} +) @function.definition + +; Decorated Function Definitions +(decorated_definition + (decorator)+ @function.decorator (function_definition - name: (identifier) @_function_name - (#match? @_function_name "^{name}$") - body: (block - . - (expression_statement - (string) @function.docstring)? - . - (_)*)) - - ; Decorated function definitions (including async) with optional docstring - (decorated_definition - (decorator)+ - (function_definition - name: (identifier) @_function_name - (#match? @_function_name "^{name}$") - body: (block) @function.body)) @function.definition - - (decorated_definition - (decorator)+ - (function_definition - name: (identifier) @_function_name - (#match? @_function_name "^{name}$") - body: (block - . - (expression_statement - (string) @function.docstring)? - . - (_)*))) - - ; Method definitions in classes (including async and decorated) with optional docstring - (class_definition - body: (block + {definition_base} + {common_body} + ) @function.definition +) + +; Methods in Classes +(class_definition + body: (block (function_definition - name: (identifier) @_function_name - (#match? @_function_name "^{name}$") - body: (block) @function.body) @function.definition)) - - (class_definition - body: (block - (function_definition - name: (identifier) @_function_name - (#match? @_function_name "^{name}$") - body: (block - . - (expression_statement - (string) @function.docstring)? - . - (_)*)))) -""", + {definition_base} + {common_body} + ) @function.definition + ) +) + +; Decorated Methods in Classes +(class_definition + body: (block + (decorated_definition + (decorator)+ @function.decorator + (function_definition + {definition_base} + {common_body} + ) @function.definition + ) + ) +) +""".format( + definition_base=_definition_base_template.format(type="function"), + common_body=_common_template.format(type="function") + ), 'class': """ - ; Regular and decorated class definitions (including nested) with optional docstring - (class_definition - name: (identifier) @_class_name - (#match? @_class_name "^{name}$") - body: (block) @class.body) @class.definition - +; Class Definitions +(class_definition + {definition_base} + {common_body} +) @class.definition + +; Decorated Class Definitions +(decorated_definition + (decorator)+ @class.decorator (class_definition - name: (identifier) @_class_name - (#match? @_class_name "^{name}$") - body: (block - . - (expression_statement - (string) @class.docstring)? - . - (_)*)) - - ; Decorated class definitions - (decorated_definition - (decorator)+ - (class_definition - name: (identifier) @_class_name - (#match? @_class_name "^{name}$") - body: (block) @class.body)) @class.definition - - (decorated_definition - (decorator)+ - (class_definition - name: (identifier) @_class_name - (#match? @_class_name "^{name}$") - body: (block - . - (expression_statement - (string) @class.docstring)? - . - (_)*))) - - ; Nested class definitions within other classes - (class_definition - body: (block - (class_definition - name: (identifier) @_class_name - (#match? @_class_name "^{name}$") - body: (block) @class.body) @class.definition)) - - (class_definition - body: (block + {definition_base} + {common_body} + ) @class.definition +) + +; Nested Classes +(class_definition + body: (block (class_definition - name: (identifier) @_class_name - (#match? @_class_name "^{name}$") - body: (block - . - (expression_statement - (string) @class.docstring)? - . - (_)*)))) -""" - }, - "kotlin": { + {definition_base} + {common_body} + ) @class.definition + ) +) + +; Decorated Nested Classes +(class_definition + body: (block + (decorated_definition + (decorator)+ @class.decorator + (class_definition + {definition_base} + {common_body} + ) @class.definition + ) + ) +) +""".format( + definition_base=_definition_base_template.format(type="class"), + common_body=_common_template.format(type="class") + ) + }, "kotlin": { 'function': """ ; Regular function definitions with optional annotations and KDoc (function_declaration diff --git a/src/text_manipulation/range_spec.py b/src/text_manipulation/range_spec.py index 8f02e8e..605d5a8 100644 --- a/src/text_manipulation/range_spec.py +++ b/src/text_manipulation/range_spec.py @@ -13,6 +13,7 @@ from collections.abc import Sequence from typing import NamedTuple, TypeAlias from functools import total_ordering +from dataclasses import dataclass, field from cedarscript_ast_parser import Marker, RelativeMarker, RelativePositionType, MarkerType, BodyOrWhole @@ -331,8 +332,8 @@ class ParentInfo(NamedTuple): ParentRestriction: TypeAlias = RangeSpec | str | None - -class IdentifierBoundaries(NamedTuple): +@dataclass +class IdentifierBoundaries: """ Represents the boundaries of an identifier in code, including its whole range and body range. @@ -347,8 +348,12 @@ class IdentifierBoundaries(NamedTuple): whole: RangeSpec body: RangeSpec | None = None docstring: RangeSpec | None = None - decorators: list[RangeSpec] = [] - parents: list[ParentInfo] = [] + decorators: list[RangeSpec] = field(default_factory=list) + parents: list[ParentInfo] = field(default_factory=list) + + def append_decorator(self, decorator: RangeSpec): + self.decorators.append(decorator) + self.whole = self.whole._replace(start = min(self.whole.start, decorator.start)) def __str__(self): return f'IdentifierBoundaries({self.whole} (BODY: {self.body}) )' diff --git a/src/text_manipulation/text_editor_kit.py b/src/text_manipulation/text_editor_kit.py index 836992e..0c0a342 100644 --- a/src/text_manipulation/text_editor_kit.py +++ b/src/text_manipulation/text_editor_kit.py @@ -8,7 +8,7 @@ from collections.abc import Sequence from typing import Protocol, runtime_checkable -from os import PathLike +from os import PathLike, path from cedarscript_ast_parser import Marker, RelativeMarker, RelativePositionType, Segment, MarkerType, BodyOrWhole from .range_spec import IdentifierBoundaries, RangeSpec @@ -24,7 +24,7 @@ def read_file(file_path: str | PathLike) -> str: Returns: str: The contents of the file as a string. """ - with open(file_path, 'r') as file: + with open(path.normpath(file_path), 'r') as file: return file.read() @@ -36,7 +36,7 @@ def write_file(file_path: str | PathLike, lines: Sequence[str]): file_path (str | PathLike): The path to the file to be written. lines (Sequence[str]): The lines to be written to the file. """ - with open(file_path, 'w') as file: + with open(path.normpath(file_path), 'w') as file: file.writelines([line + '\n' for line in lines]) pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy