From afdc5b9d0f3168c379d4c05bcc295dbec44a1e52 Mon Sep 17 00:00:00 2001 From: Harshad Hegde Date: Fri, 9 Feb 2024 12:43:45 -0600 Subject: [PATCH] Code Explanation complete. --- pyproject.toml | 2 +- src/codergpt/cli.py | 24 ++++- src/codergpt/commenter/__init__.py | 1 + src/codergpt/explainer/explainer.py | 140 ++++++++++++++++++++++++++++ src/codergpt/main.py | 17 ++-- 5 files changed, 171 insertions(+), 13 deletions(-) create mode 100644 src/codergpt/commenter/__init__.py create mode 100644 src/codergpt/explainer/explainer.py diff --git a/pyproject.toml b/pyproject.toml index 91c743a..147342f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ line-length = 120 target-version = ["py38", "py39", "py310"] [tool.ruff] -extend-ignore = [ +lint.extend-ignore = [ "D211", # `no-blank-line-before-class` "D212", # `multi-line-summary-first-line` "D203", # `blank-line-before-docstring` diff --git a/src/codergpt/cli.py b/src/codergpt/cli.py index 56ca41b..b815799 100644 --- a/src/codergpt/cli.py +++ b/src/codergpt/cli.py @@ -15,6 +15,10 @@ logger = logging.getLogger(__name__) +path_argument = click.argument("path", type=click.Path(exists=True)) + +coder = CoderGPT() + @click.group() @click.option("-v", "--verbose", count=True) @@ -38,12 +42,28 @@ def main(verbose: int, quiet: bool): @main.command() -@click.argument("path", type=click.Path(exists=True)) +@path_argument def inspect(path: Union[str, Path, TextIO]): """Inspect package to show file-language-map.""" - coder = CoderGPT() coder.inspect_package(path=path) +@main.command() +@path_argument +@click.option("-f", "--function", help="Function name to explain.") +@click.option("-c", "--classname", help="Class name to explain.") +def explain(path: Union[str, Path], function: str, classname: str): + """Inspect package to show file-language-map.""" + # Ensure path is a string or Path object for consistency + if isinstance(path, str): + path = Path(path) + + # Check if path is a file + if path.is_file(): + coder.explainer(path=path, function=function, classname=classname) + else: + raise ValueError("The path provided is not a file.") + + if __name__ == "__main__": main() diff --git a/src/codergpt/commenter/__init__.py b/src/codergpt/commenter/__init__.py new file mode 100644 index 0000000..ab79748 --- /dev/null +++ b/src/codergpt/commenter/__init__.py @@ -0,0 +1 @@ +"""Commenter module for the package.""" diff --git a/src/codergpt/explainer/explainer.py b/src/codergpt/explainer/explainer.py new file mode 100644 index 0000000..48bea1b --- /dev/null +++ b/src/codergpt/explainer/explainer.py @@ -0,0 +1,140 @@ +"""Explainer Module.""" + +import ast +from pathlib import Path +from typing import Any, Dict, Optional, Union + +from langchain_core.runnables.base import RunnableSerializable + + +class ExpressionEvaluator(ast.NodeVisitor): + """Evaluate the code expression and extract the source code of the specified function or class.""" + + def __init__(self, source_code, function_name=None, class_name=None): + """ + Initialize the ExpressionEvaluator class. + + :param function_name: The name of the function to find in the source code. + :type function_name: str or None + :param class_name: The name of the class to find in the source code. + :type class_name: str or None + """ + self.function_code = None + self.class_code = None + self.function_name = function_name + self.class_name = class_name + self.source_code = source_code + + def visit_FunctionDef(self, node): + """ + Visit a FunctionDef (function definition) node in the AST. + + If the function name matches the target function name, it extracts the source segment. + + :param node: The node representing a function definition in the AST. + :type node: ast.FunctionDef + """ + if self.function_name == node.name: + self.function_code = ast.get_source_segment(self.source_code, node) + # Continue the traversal in case there are nested functions or classes + self.generic_visit(node) + + def visit_ClassDef(self, node): + """ + Visit a ClassDef (class definition) node in the AST. + + If the class name matches the target class name, it extracts the source segment. + + :param node: The node representing a class definition in the AST. + :type node: ast.ClassDef + """ + if self.class_name == node.name: + self.class_code = ast.get_source_segment(self.source_code, node) + # Continue the traversal in case there are nested functions or classes + self.generic_visit(node) + + +class CodeExplainer: + """Code Explainer class that extracts and explains code from a given file.""" + + def __init__(self, chain: RunnableSerializable[Dict, Any]): + """ + Initialize the CodeExplainer class with a runnable chain. + + :param chain: A RunnableSerializable object capable of executing tasks. + """ + self.chain = chain + + def get_function_code( + self, filename: str, function_name: Optional[str] = None, class_name: Optional[str] = None + ) -> Optional[str]: + """ + Extract and return the source code of the specified function or class from a file. + + :param filename: The path to the file containing the code. + :param function_name: The name of the function to extract code for. Default is None. + :param class_name: The name of the class to extract code for. Default is None. + :return: The extracted source code of the specified function or class, if found. + """ + with open(filename, "r") as source_file: + source_code = source_file.read() + + # Parse the source code into an AST + parsed_code = ast.parse(source_code) + + # Create a visitor instance and walk through the AST + visitor = ExpressionEvaluator(source_code=source_code, function_name=function_name, class_name=class_name) + visitor.visit(parsed_code) + if function_name: + return visitor.function_code + elif class_name: + return visitor.class_code + + def explain(self, path: Union[str, Path], function: Optional[str] = None, classname: Optional[str] = None): + """ + Explain the contents of the code file by invoking the runnable chain. + + :param path: The path to the code file to be explained. + :param function: The name of the function to explain. Default is None. + :param classname: The name of the class to explain. Default is None. + """ + if function: + code = self.get_function_code(filename=path, function_name=function) + response = self.chain.invoke({"input": f"Explain the following code: \n\n```\n{code}\n```"}) + + # Pretty print the response + print(f"Explanation for '{function}':\n{response.content}") + elif classname: + code = self.get_function_code(filename=path, class_name=classname) + response = self.chain.invoke({"input": f"Explain the following code: \n\n```\n{code}\n```"}) + # Pretty print the response + print(f"Explanation for '{classname}':\n{response.content}") + + # # Ensure path is a string or Path object for consistency + # if isinstance(path, str): + # path = Path(path) + + # with open(path, "r") as file: + # code = file.read() + + # # Guess the lexer based on the file name and content + # lexer = guess_lexer_for_filename(path.name, code) + + # # Tokenize the code using the guessed lexer + # tokens = list(pygments.lex(code, lexer)) + + # all_functions = [] + # all_classes = [] + # # Process the tokens (for demonstration, just print them) + # for token_type, token_value in tokens: + # if token_type in Token.Name.Function: + # print(f"{token_type}: {token_value}") + # all_functions.append(token_value) + # elif token_type in Token.Name.Class: + # all_classes.append(token_value) + # else: + # continue + + # elif token_type in Token.Comment: + # print(f"Comment: {token_value}") + # Add more conditions as needed to handle different parts of the code diff --git a/src/codergpt/main.py b/src/codergpt/main.py index 13e90bb..4aab30d 100644 --- a/src/codergpt/main.py +++ b/src/codergpt/main.py @@ -10,6 +10,7 @@ from tabulate import tabulate from codergpt.constants import EXTENSION_MAP_FILE, INSPECTION_HEADERS +from codergpt.explainer.explainer import CodeExplainer class CoderGPT: @@ -28,16 +29,6 @@ def __init__(self): # This chain will be used to send the input to the AI in the correct context. self.chain = self.prompt | self.llm - # # Invoke the chain with the specific input asking about the programming language - # # associated with the given file extension. The input is formatted to include the - # # file extension in the question and requests a one-word response. - # response = chain.invoke( - # { - # "input": f"Given the file extensions {ext},\ - # return just programming language name e.g. 'Python' or 'JavaScript'." - # } - # ) - def inspect_package(self, path: Union[str, Path]): """Inspecting the code and displaying a mapping of files to their languages in a table.""" print("Inspecting the code.") @@ -72,6 +63,12 @@ def inspect_package(self, path: Union[str, Path]): # Display the results as a table print(tabulate(file_language_list, headers=INSPECTION_HEADERS)) + def explainer(self, path: Union[str, Path], function: str = None, classname=None): + """Explains contents of the code file.""" + # Ensure path is a string or Path object for consistency + code_explainer = CodeExplainer(self.chain) + code_explainer.explain(path=path, function=function, classname=classname) + if __name__ == "__main__": coder = CoderGPT()