Замена SimpleString внутри определения функции libcst? (dataclasses.FrozenInstanceError: невозможно назначить поле «тело»)

Контекст

При попытке использоватьlibcstмодуля, у меня возникли некоторые трудности с обновлением документации по функции.

МВЕ

Чтобы воспроизвести ошибку, включен следующий минимальный рабочий пример (MWE):

      from libcst import (  # type: ignore[import]
    Expr,
    FunctionDef,
    IndentedBlock,
    MaybeSentinel,
    SimpleStatementLine,
    SimpleString,
    parse_module,
)

original_content: str = """
\"\"\"Example python file with a function.\"\"\"


from typeguard import typechecked


@typechecked
def add_three(*, x: int) -> int:
    \"\"\"ORIGINAL This is a new docstring core.
    that consists of multiple lines. It also has an empty line inbetween.

    Here is the emtpy line.\"\"\"
    return x + 2

"""
new_docstring_core: str = """\"\"\"This is a new docstring core.
    that consists of multiple lines. It also has an empty line inbetween.

    Here is the emtpy line.\"\"\""""


def replace_docstring(
    original_content: str, func_name: str, new_docstring: str
) -> str:
    """Replaces the docstring in a Python function."""
    module = parse_module(original_content)
    for node in module.body:
        if isinstance(node, FunctionDef) and node.name.value == func_name:
            print("Got function node.")
            # print(f'node.body={node.body}')
            if isinstance(node.body, IndentedBlock):
                if isinstance(node.body.body[0], SimpleStatementLine):
                    simplestatementline: SimpleStatementLine = node.body.body[
                        0
                    ]

                    print("Got SimpleStatementLine")
                    print(f"simplestatementline={simplestatementline}")

                    if isinstance(simplestatementline.body[0], Expr):
                        print(
                            f"simplestatementline.body={simplestatementline.body}"
                        )

                        simplestatementline.body = (
                            Expr(
                                value=SimpleString(
                                    value=new_docstring,
                                    lpar=[],
                                    rpar=[],
                                ),
                                semicolon=MaybeSentinel.DEFAULT,
                            ),
                        )


replace_docstring(
    original_content=original_content,
    func_name="add_three",
    new_docstring=new_docstring_core,
)
print("done")

Ошибка:

Бегpython mwe.pyдает:

      Traceback (most recent call last):
  File "/home/name/git/Hiveminds/jsonmodipy/mwe0.py", line 68, in <module>
    replace_docstring(
  File "/home/name/git/Hiveminds/jsonmodipy/mwe0.py", line 56, in replace_docstring
    simplestatementline.body = (
    ^^^^^^^^^^^^^^^^^^^^^^^^
  File "<string>", line 4, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'body'

Вопрос

Как можно заменить строку документации функции с именем:add_threeв каком-то коде Pythonfile_contentиспользуя модуль libcst?

Частичное решение

Я нашел следующее решение для базового примера, однако я не тестировал его на различных функциях внутри классов, с типизированными аргументами, типизированными возвратами и т. д.

      from pprint import pprint
import libcst as cst
import libcst.matchers as m


src = """\
import foo
from a.b import foo_method


class C:
    def do_something(self, x):
        \"\"\"Some first line documentation
        Some second line documentation

        Args:something.
        \"\"\"
        return foo_method(x)
"""
new_docstring:str = """\"\"\"THIS IS A NEW DOCSTRING
        Some first line documentation
        Some second line documentation

        Args:somethingSTILLCHANGED.
        \"\"\""""

class ImportFixer(cst.CSTTransformer):
    def leave_SimpleStatementLine(self, orignal_node, updated_node):
        """Replace imports that match our criteria."""
        
        if m.matches(updated_node.body[0], m.Expr()):
            expr=updated_node.body[0]
            if m.matches(expr.value, m.SimpleString()):
                simplestring=expr.value
                print(f'GOTT={simplestring}')
                return updated_node.with_changes(body=[
                    cst.Expr(value=cst.SimpleString(value=new_docstring))
                ])
        return updated_node

source_tree = cst.parse_module(src)
transformer = ImportFixer()
modified_tree = source_tree.visit(transformer)

print("Original:")
print(src)
print("\n\n\n\nModified:")
print(modified_tree.code)

Например, это частичное решение не работает в следующих случаях:

      src = """\
import foo
from a.b import foo_method


class C:
    def do_something(self, x):
        \"\"\"Some first line documentation
        Some second line documentation

        Args:something.
        \"\"\"
        return foo_method(x)
    
def do_another_thing(y:List[str]) -> int:
    \"\"\"Bike\"\"\"
    return 1
    """

поскольку решение не проверяет имя функции, в которойSimpleStringимеет место.

1 ответ

Почему вы получили «FrozenInstanceError»?

Как вы видели, CST производстваlibcst— это граф, состоящий из неизменяемых узлов (каждый из которых представляет часть языка Python). Если вы хотите изменить узел, вам фактически нужно сделать его новую копию. Это делается с помощьюnode.with_changes()метод.

Итак, вы можете сделать это в своем первом фрагменте кода. Однако есть более «элегантные» способы добиться этого, частично описанные в руководстве по libcst , как вы только что начали делать в своем частичном решении.

Как можно заменить строку документации функции с именем: add_three в некотором коде Python file_content с помощью модуля libcst?

Используйте libcst.CSTTransformer для навигации:

  1. Вам нужно найти в CST узел, представляющий вашу функцию (libcst.FunctionDef)
  2. Затем вам нужно найти узел, представляющий документацию вашей функции (licst.SimpleString)
  3. Обновите этот узел документации
      import libcst

class DocUpdater(libcst.CSTTransformer):
    """Upodate the docstring of the function `add_three`"""
    def __init__(self) -> None:
        super().__init__()
        self._docstring: str | None = None

    def visit_FunctionDef(self, node: libcst.FunctionDef) -> Optional[bool]:
        """Trying to find the node defining function `add_three`,
         and get its docstring"""
        if node.name.value == 'add_three':
            self._docstring = f'"""{node.get_docstring(clean=False)}"""'
            """Unfortunatly, get_docstring doesn't return the exact docstring
            node value: you need to add the docstring's triple quotes"""
            return True
        return False

    def leave_SimpleString(
        self, original_node: libcst.SimpleString, updated_node: libcst.SimpleString
    ) -> libcst.BaseExpression:
        """Trying to find the node defining the docstring
        of your function, and update the docstring"""
        if original_node.value == self._docstring:
            return updated_node.with_changes(value='"""My new docstring"""')

        return updated_node

И наконец:

      test = r'''
import foo
from a.b import foo_method


class C:
    def add_three(self, x):
        """Some first line documentation
        Some second line documentation

        Args:something.
        """
        return foo_method(x)

def do_another_thing(y: list[str]) -> int:
    """Bike"""
    return 1
'''

cst = libcst.parse_module(test)
updated_cst = cst.visit(DocUpdater())

print(updated_cst.code)

Выход:

      import foo
from a.b import foo_method


class C:
    def add_three(self, x):
        """My new docstring"""
        return foo_method(x)

def do_another_thing(y: list[str]) -> int:
    """Bike"""
    return 1
Другие вопросы по тегам