如何在 Java 中从 Python 代码获取 AST、更改它并将其写回文件?

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

问题

如何在 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-问题

作为对评论的回应,XY 问题是一个修改代码的机器人:更改或编写文档字符串、函数文档和/或函数注释,并为这些函数编写测试。我想对文件代码的每个模块化组件执行单独的修改/创建。因此,我认为使用 AST 可能是一种以分层和模块化方式获取代码组件的有效策略,而不是编写正则表达式或手动 Python 代码解析器。

java abstract-syntax-tree jython
1个回答
0
投票

范围

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");
  }
}

© www.soinside.com 2019 - 2024. All rights reserved.