Context
While trying to use the libcst
module, I am experiencing some difficulties updating a documentation of a function.
MWE
To reproduce the error, the following minimal working example (MWE) is included:
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")
Error:
Running python mwe.py
yields:
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'
Question
How can one replace the docstring of a function named: add_three
in some Python code file_content
using the libcst module?
Partial Solution
I found the following solution for a basic example, however, I did not test it on different functions inside classes, with typed arguments, typed returns etc.
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)
For example, this partial solution fails on:
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
"""
because the solution does not verify the name of the function in which the SimpleString
occurs.
Why were you getting the "FrozenInstanceError" ?
As you saw, the CST produced by
libcst
is a graph made of immutable nodes (each one representing a part of the Python language). If you want to change a node, you actually need to make a new copy of it. This is done usingnode.with_changes()
method.So you could do this in your first code snippet. However, there are more "elegant" ways to achieve this, partly documented in libcst's tutorial, as you just started doing in your partial solution.
How can one replace the docstring of a function named: add_three in some Python code file_content using the libcst module?
Use the libcst.CSTTransformer to navigate your way through:
libcst.FunctionDef
)licst.SimpleString
)And finally:
Output: