如何在 Java 中读取任意 Python 文件,从中构建抽象语法树,对其进行修改,然后将修改后的 AST 写回文件?
我尝试了以下方法,首先读取Python代码来生成抽象语法树(AST):
package com.doctestbot.cli;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.python.core.Py;
import org.python.core.PyObject;
import org.python.core.PyString;
import org.python.util.PythonInterpreter;
/**
* A class to retrieve the Python abstract syntax tree using Jython. This is a utility class,
* meaning one only calls its method, and one does not instantiate the object.
*/
public final class PythonAstRetriever {
/**
* Retrieves the Python abstract syntax tree for the given Python code.
*
* @param pythonCode The Python code for which to retrieve the AST.
* @return The Python abstract syntax tree as a PyObject.
*/
@SuppressWarnings({"PMD.LawOfDemeter"})
public static PyObject getPythonAst(String pythonCode) {
// Create a PythonInterpreter
PythonInterpreter interpreter = new PythonInterpreter();
// Access the "ast" module from Python
PyObject astModule = interpreter.get("ast");
// Parse the Python code and generate the AST
PyObject invokeArg = new PyString(pythonCode);
return astModule.invoke("parse", invokeArg, Py.None, Py.None);
}
/**
* Reads the content of a Python code file from the specified file path.
*
* @param filePath The path to the Python code file to read.
* @return The content of the Python code file as a string.
* @throws IOException If an I/O error occurs while reading the file.
*/
public static String readPythonCodeFromFile(String filePath) throws IOException {
Path path = Paths.get(filePath);
return Files.readString(path);
}
// Private constructor to prevent instantiation of the utility class.
private PythonAstRetriever() {
throw new AssertionError("PythonAstRetriever class should not be instantiated.");
}
}
但是,当我运行它时:
String pythonCode =
"\"\"\"Example python file with a function.\"\"\"\n" +
"\n" +
"from typeguard import typechecked\n" +
"\n" +
"@typechecked\n" +
"def add_two(*, x: int) -> int:\n" +
" \"\"\"Adds a value to an incoming number.\"\"\"\n" +
" return x + 2";
PyObject astTree = PythonAstRetriever.getPythonAst(pythonCode);
但是,这会产生错误:
PythonAstRetriever.java:34: error: incompatible types: PyObject cannot be converted to PyObject[]
return astModule.invoke("parse", invokeArg, Py.None, Py.None);
^
Note: Some messages have been simplified; recompile with -Xdiags:verbose to get full output
为了回应评论,下面是完整的堆栈跟踪:
PythonAstRetriever.java:34: error: no suitable method found for invoke(String,PyObject,PyObject,PyObject)
return astModule.invoke("parse", invokeArg, Py.None, Py.None);
^
method PyObject.invoke(String,PyObject[],String[]) is not applicable
(actual and formal argument lists differ in length)
method PyObject.invoke(String,PyObject[]) is not applicable
(actual and formal argument lists differ in length)
method PyObject.invoke(String) is not applicable
(actual and formal argument lists differ in length)
method PyObject.invoke(String,PyObject) is not applicable
(actual and formal argument lists differ in length)
method PyObject.invoke(String,PyObject,PyObject) is not applicable
(actual and formal argument lists differ in length)
method PyObject.invoke(String,PyObject,PyObject[],String[]) is not applicable
(argument mismatch; PyObject cannot be converted to PyObject[])
1 error
FAILURE: Build failed with an exception.
作为对评论的回应,XY 问题是一个修改代码的机器人:更改或编写文档字符串、函数文档和/或函数注释,并为这些函数编写测试。我想对文件代码的每个模块化组件执行单独的修改/创建。因此,我认为使用 AST 可能是一种以分层和模块化方式获取代码组件的有效策略,而不是编写正则表达式或手动 Python 代码解析器。
Py.None
参数上的语法错误已解决。然而,在我看来,将 AST 转换回 python 代码并不简单。因此,这不是 XY 问题的答案。
此代码解决了语法错误:
package com.doctestbot.cli;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.python.core.PyObject;
import org.python.core.PyString;
import org.python.util.PythonInterpreter;
/**
* A class to retrieve the Python abstract syntax tree using Jython. This is a utility class,
* meaning one only calls its method, and one does not instantiate the object.
*/
public final class PythonAstRetriever {
/**
* Retrieves the Python abstract syntax tree for the given Python code.
*
* @param pythonCode The Python code for which to retrieve the AST.
* @return The Python abstract syntax tree as a PyObject.
*/
@SuppressWarnings({"PMD.LawOfDemeter"})
public static PyObject getPythonAst(String pythonCode) {
// Create a PythonInterpreter
PythonInterpreter interpreter = new PythonInterpreter();
System.out.println("pythonCode" + pythonCode);
// Import the ast module
interpreter.exec("import ast");
// Parse the Python code and generate the AST
PyObject invokeArg = new PyString(pythonCode);
PyObject astModule = interpreter.get("ast");
PyObject parseFunction = astModule.__getattr__("parse");
// Return object
return parseFunction.__call__(invokeArg);
}
@SuppressWarnings({"PMD.LawOfDemeter"})
public static String pythonAstToString(PyObject pythonModule) {
// Initialise Python code and imports.
PythonInterpreter interpreter = new PythonInterpreter();
interpreter.exec("import ast");
PyObject astModule = interpreter.get("ast");
// PyObject compileFunction = astModule.__getattr__("compile");
// Get a string representation of the AST
PyObject dumpFunction = astModule.__getattr__("dump");
PyObject astDump = dumpFunction.__call__(pythonModule);
// PyObject compiledCode = compileFunction.__call__(pythonModule, Py.None, Py.None, Py.None);
// Get the code as a string
String generatedCode = astDump.toString();
System.out.println("generatedCode" + generatedCode);
return generatedCode;
}
// Parse the Python code and generate the AST
// PyObject invokeArg = new PyString(pythonCode);
// return astModule.invoke("parse", invokeArg, Py.None, Py.None);
// return (PyObject[]) astModule.invoke("parse", invokeArg, Py.None, Py.None);
// }
/**
* Reads the content of a Python code file from the specified file path.
*
* @param filePath The path to the Python code file to read.
* @return The content of the Python code file as a string.
* @throws IOException If an I/O error occurs while reading the file.
*/
public static String readPythonCodeFromFile(String filePath) throws IOException {
Path path = Paths.get(filePath);
return Files.readString(path);
}
// Private constructor to prevent instantiation of the utility class.
private PythonAstRetriever() {
throw new AssertionError("PythonAstRetriever class should not be instantiated.");
}
}
使用以下测试文件进行测试:
package com.doctestbot;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import com.doctestbot.cli.Constants;
import com.doctestbot.cli.PythonAstRetriever;
import com.doctestbot.cli.SubmoduleManager;
import java.io.IOException;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.python.core.PyObject;
/**
* Test scenarios for parsing and rewriting a Python file.
*
* <p>The following scenarios are tested:
*
* <pre>
* * Tests a Python file with:
* - methods
* - documentation + methods
* - docstring, documentation + methods
*
* * class
* - documentation + class
* - docstring + documentation + class
*
* * class + classmethods
* - documentation + class + classmethods
* - docstring + documentation + class + classmethods
*
* * class + methods
* - documentation + class + methods
* - docstring + documentation + class + methods
*
* * class + classmethods + methods
* - documentation + class + classmethods + methods
* - docstring + documentation + class + classmethods + methods
*
* * gets parsed and rewritten correctly.
* </pre>
*/
@SuppressWarnings({"PMD.AtLeastOneConstructor"})
public class TestPythonParsing {
@BeforeAll
public static void setupOnce() {
SubmoduleManager.checkoutTestRepoBranch(
"test-parsing", "854f5ccb7954350b51d02532295c05b65fbdc6d8");
}
/**
* Tests the addition operation. It verifies that adding two positive integers results in the
* correct sum.
*/
@Test
void testAddition() {
int result = 3 + 5;
assertEquals(8, result, "Addition operation should yield the sum of two numbers.");
assertNotNull(result, "msg");
}
/** Tests parsing and recreating a Python file with only methods. */
@Test
@SuppressWarnings({"PMD.LawOfDemeter"})
public void testParseAndRecreateMethodsOnly() throws IOException {
// Path to the Python code file
String filePath = Constants.testRepoPath + "/src/pythontemplate/methods.py";
// Read Python code from the file
String pythonCode = PythonAstRetriever.readPythonCodeFromFile(filePath);
// Parse the Python code
PyObject astTree = PythonAstRetriever.getPythonAst(pythonCode);
PythonAstRetriever.pythonAstToString(astTree);
// Recreate the Python code from the AST
String recreatedCode = astTree.toString();
System.out.println("recreatedCode" + recreatedCode);
// Assert the parsed and recreated code match
assertEquals(pythonCode, recreatedCode, "Parsed and recreated code should match");
}
}