替换 libcst 函数定义中的 SimpleString? (dataclasses.FrozenInstanceError:无法分配给字段“body”)

问题描述 投票:0回答:1

背景

在尝试使用

libcst
模块时,我在更新函数文档时遇到一些困难。

MWE

为了重现该错误,包含以下最小工作示例 (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'

问题

如何使用 libcst 模块替换某些 Python 代码

add_three
中名为
file_content
的函数的文档字符串?

部分解决方案

我找到了以下基本示例的解决方案,但是,我没有在类内的不同函数(使用类型化参数、类型化返回等)上测试它。

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
的函数的名称。

python replace docstring libcst
1个回答
0
投票

您为什么会收到“FrozenInstanceError”?

如您所见,

libcst
生成的 CST 是由不可变节点组成的图(每个节点代表 Python 语言的一部分)。如果要更改节点,实际上需要为其创建一个新副本。 这是使用
node.with_changes()
方法完成的。

因此您可以在第一个代码片段中执行此操作。 然而,还有更多“优雅”的方法来实现这一点,部分记录在 libcst 的教程中,正如您刚刚在部分解决方案中开始做的那样。

如何使用 libcst 模块替换某些 Python 代码 file_content 中名为 add_third 的函数的文档字符串?

使用 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 = ''

    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 value"""
            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
© www.soinside.com 2019 - 2024. All rights reserved.