Замена 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 для навигации:
- Вам нужно найти в CST узел, представляющий вашу функцию (
libcst.FunctionDef
) - Затем вам нужно найти узел, представляющий документацию вашей функции (
licst.SimpleString
) - Обновите этот узел документации
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