diff --git a/README.md b/README.md index d56e02e..6f3f102 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,19 @@ ## Description -CoderGPT is a command line interface for generating/modifying code. It allows developers to -enhance code by commenting, optimizing, documenting and adding tests to their project using -the power of LLM and GPT. This project is powered by [langchain](https://github.com/langchain-ai/langchain). +CoderGPT is a command line interface for generating/modifying code. It allows developers to enhance code by commenting, optimizing, documenting, and adding tests to their project using the power of LLM and GPT. This project is powered by [langchain](https://github.com/langchain-ai/langchain). + +--- +**NOTE** +Before using CoderGPT, ensure that the environment variable `OPENAI_API_KEY` is set locally on your machine. This key is required for authentication with the OpenAI API which powers the underlying language model. + +```sh +export OPENAI_API_KEY='your-api-key-here' +``` + +Replace `your-api-key-here` with your actual OpenAI API key. This step is crucial for the proper functioning of CoderGPT as it relies on the OpenAI API for generating and modifying code. + +--- ## Installation @@ -194,47 +204,88 @@ code [OPTIONS] COMMAND [ARGS]... 5. `write-tests`: Writes tests for the specified code file. The user can specify a function and/or a class within the file to target with the tests. -```shell -code write-tests [--function ] [--class ] -``` + ```shell + code write-tests [--function ] [--class ] + ``` -#### Example -- Let's consider a python file `example.py`: -```python -# example.py + #### Example + - Let's consider a python file `example.py`: + ```python + # example.py -def add(a, b): - return a + b + def add(a, b): + return a + b -class Calculator: - def subtract(self, a, b): - return a - b -``` -```shell -$ code write-tests example.py --function add --class Calculator -``` -results in test files being generated that contain test cases for the `add` function and the `Calculator` class. The actual content of the test files will depend on the implementation of the `coder.test_writer` method but would typically look something like this: + class Calculator: + def subtract(self, a, b): + return a - b + ``` + ```shell + $ code write-tests example.py --function add --class Calculator + ``` + results in test files being generated that contain test cases for the `add` function and the `Calculator` class. The actual content of the test files will depend on the implementation of the `coder.test_writer` method but would typically look something like this: -```python -import unittest -from example import add, Calculator + ```python + import unittest + from example import add, Calculator -class TestAddFunction(unittest.TestCase): + class TestAddFunction(unittest.TestCase): - def test_addition(self): - self.assertEqual(add(3, 4), 7) + def test_addition(self): + self.assertEqual(add(3, 4), 7) -class TestCalculator(unittest.TestCase): + class TestCalculator(unittest.TestCase): - def setUp(self): - self.calc = Calculator() + def setUp(self): + self.calc = Calculator() - def test_subtract(self): - self.assertEqual(self.calc.subtract(10, 5), 5) -``` + def test_subtract(self): + self.assertEqual(self.calc.subtract(10, 5), 5) + ``` + + In this example, running the command generates unit tests for both the `add` function and the `Calculator` class in the `example.py` file. The tests check if the `add` function correctly adds two numbers and if the `Calculator`'s `subtract` method correctly subtracts one number from another. + +6. `document`: Generates documentation for the specified code file. + + ```shell + code document + ``` + + #### Example + - Let's consider a python file `example.py`: + ```python + # example.py + + def add(a, b): + """Add two numbers and return the result.""" + return a + b + + class Calculator: + """A simple calculator class.""" -In this example, running the command generates unit tests for both the `add` function and the `Calculator` class in the `example.py` file. The tests check if the `add` function correctly adds two numbers and if the `Calculator`'s `subtract` method correctly subtracts one number from another. + def subtract(self, a, b): + """Subtract b from a and return the result.""" + return a - b + ``` + ```shell + $ code document example.py + ``` + results in documentation files being generated that contain documentation for all functions and classes within the `example.py` file. The actual content of the documentation files will depend on the implementation of the `DocumentationGenerator.document` method but would typically look something like this: + + ```rst + add Function + ------------ + + .. autofunction:: example.add + + Calculator Class + ---------------- + + .. autoclass:: example.Calculator + :members: + ``` + In this example, running the command generates documentation for the entire `example.py` file, including both the `add` function and the `Calculator` class. The documentation includes descriptions of the function and class, as well as any public methods of the class. ## Development diff --git a/docs/cli.rst b/docs/cli.rst new file mode 100644 index 0000000..f262b20 --- /dev/null +++ b/docs/cli.rst @@ -0,0 +1,104 @@ +Command line interface for CoderGPT +==================================== + +This module provides a command line interface (CLI) for CoderGPT, a powerful code generation tool designed to assist in various coding tasks, including inspection, explanation, commenting, optimization, test writing, and documentation of code files. + +.. moduleauthor:: Harshad Hegde + +Usage +----- + +To use this CLI, run the following command in your terminal: + +.. code-block:: shell + + python codergpt_cli.py [OPTIONS] COMMAND [ARGS]... + +Options +------- + +-v, --verbose INTEGER + Verbosity level, which can be set to 0, 1, or 2. + +-q, --quiet + Activate quiet mode, limiting output messages. + +--version + Display the current version of CoderGPT and exit. + +Commands +-------- + +**inspect** + Inspect a package to show a file-language map. Requires a path to the package as an argument. + +**explain** + Provide explanations for a specified function or class within a file. This command requires a path and can optionally include a function name and a class name. + +**comment** + Add comments to a code file. This command requires a path to the file and accepts an overwrite flag to indicate whether existing files should be overwritten. + +**optimize** + Optimize a code file by improving its performance or code quality. This command requires a path to the file and can optionally include a function name, a class name, and an overwrite flag. + +**write-tests** + Generate test cases for a specified function or class within a file. This command requires a path and can optionally include a function name and a class name. + +**document** + Write documentation files for a code file. This command requires a path to the file. + +.. note:: All path arguments can be a string path, a :class:`pathlib.Path` object, or a file object. + +Examples +-------- + +Inspect a package: + +.. code-block:: shell + + python codergpt_cli.py inspect /path/to/package + +Explain a function in a file: + +.. code-block:: shell + + python codergpt_cli.py explain -f my_function /path/to/file.py + +Add comments to a file with overwrite enabled: + +.. code-block:: shell + + python codergpt_cli.py comment --overwrite /path/to/file.py + +Optimize a class within a file without overwriting: + +.. code-block:: shell + + python codergpt_cli.py optimize -c MyClass /path/to/file.py + +Write tests for a function: + +.. code-block:: shell + + python codergpt_cli.py write-tests -f my_function /path/to/file.py + +Write documentation for a file: + +.. code-block:: shell + + python codergpt_cli.py document /path/to/file.py + +Parameters and Options +---------------------- + +-path + The path to the code file, package, or directory. This is a required argument for all commands. + +-f, --function + The name of the function to explain, optimize, or write tests for. This is an optional argument for the ``explain``, ``optimize``, and ``write-tests`` commands. + +-c, --classname + The name of the class to explain, optimize, or write tests for. This is an optional argument for the ``explain``, ``optimize``, and ``write-tests`` commands. + +--overwrite/--no-overwrite + A flag indicating whether to overwrite the existing file. This is an optional argument for the ``comment`` and ``optimize`` commands. diff --git a/docs/commenter.rst b/docs/commenter.rst new file mode 100644 index 0000000..f1bf277 --- /dev/null +++ b/docs/commenter.rst @@ -0,0 +1,57 @@ +Commenter Module +================ + +This module contains the CodeCommenter class, which is designed to enhance code readability by automatically adding comments and Sphinx-compatible docstrings to a given piece of code. + +Classes +------- + +CodeCommenter +^^^^^^^^^^^^^ + +.. autoclass:: CodeCommenter + :members: + :undoc-members: + :show-inheritance: + + The CodeCommenter class leverages a runnable chain to analyze code and generate explanatory comments and docstrings. + + .. automethod:: __init__ + .. automethod:: comment + +Methods +------- + +__init__(self, chain) +~~~~~~~~~~~~~~~~~~~~~ + +Initializes the CodeCommenter class with a specified runnable chain. + +:parameter chain: A ``RunnableSerializable`` object that is capable of executing defined tasks. This chain is responsible for the primary operation of analyzing and generating comments for the provided code. +:type chain: RunnableSerializable[Dict, Any] + +comment(self, code, filename, overwrite=False, language=None) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Generates comments for the given code and writes the commented code to a file. + +:parameter code: The code for which comments are to be generated. This should be a string containing the source code. +:type code: str + +:parameter filename: The name of the original file from which the code was extracted. This filename is used to generate a new filename for the commented code unless ``overwrite`` is set to True. +:type filename: str + +:parameter overwrite: Determines whether the original file should be overwritten with the commented code. By default, this is set to False, and the commented code is written to a new file with an "_updated" suffix. +:type overwrite: bool, optional + +:parameter language: The programming language of the code. This is an optional parameter that can be used to specify the language if it cannot be inferred from the code or filename. Providing this information can help the runnable chain to generate more accurate and language-appropriate comments. +:type language: Optional[str], optional + +This method first invokes the runnable chain with the provided code and an instruction to add comments and Sphinx-compatible docstrings in a specific format. The response from the chain is expected to contain the commented code, which is then written to either a new file or the original file based on the ``overwrite`` parameter. + +Dependencies +------------ + +- os: Used for file path manipulation and generating new filenames. +- typing: Provides support for type hints. + diff --git a/docs/developer.rst b/docs/developer.rst index 92f8de9..974f0f8 100644 --- a/docs/developer.rst +++ b/docs/developer.rst @@ -6,155 +6,15 @@ Developer Documentation This section contains documentation for developers who want to contribute or extend the functionality of the project. -Command Line Interface for CoderGPT -=================================== +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + cli + main + explainer + commenter + optimizer + test_writer + expression_evaluator -Overview --------- - -This module provides a command line interface (CLI) for CoderGPT, a powerful code generation tool that facilitates various operations on code files such as inspection, explanation, commenting, and optimization. - -Usage ------ - -To use the CLI, run the following command with the desired options and commands: - -.. code-block:: shell - - python codergpt_cli.py [OPTIONS] COMMAND [ARGS]... - -Options -------- - --v, --verbose INTEGER - Verbosity level (0, 1 or 2). - --q, --quiet - Run in quiet mode. - ---version - Show the version and exit. - -Commands --------- - -inspect - Inspect package to show file-language-map. - -explain - Explain the contents of a code file. - -comment - Add comments to a code file. - -Details -------- - -The CLI is built using the Click library and provides a user-friendly way to interact with the CoderGPT functionalities from the terminal. - -Examples --------- - -Inspecting a package: - -.. code-block:: shell - - python codergpt_cli.py inspect src/ - -Explaining a function within a file: - -.. code-block:: shell - - python codergpt_cli.py explain src/main.py --function my_function - -Adding comments to a file: - -.. code-block:: shell - - python codergpt_cli.py comment src/main.py --overwrite - -Optimizing a class within a file: - -.. code-block:: shell - - python codergpt_cli.py optimize src/main.py --classname MyClass --overwrite - -Environment Variables ---------------------- - -- ``OPENAI_API_KEY``: The API key for OpenAI services required by CoderGPT. - -Dependencies ------------- - -- click -- logging -- pathlib.Path -- typing.TextIO, typing.Union -- codergpt.__version__ -- codergpt.main.CoderGPT - - -Main Python File -================ - -This is the main Python file for the CoderGPT project. - -Classes -------- - -.. autoclass:: CoderGPT - :members: - :undoc-members: - :show-inheritance: - - .. automethod:: __init__ - .. automethod:: inspect_package - .. automethod:: get_code - .. automethod:: explainer - .. automethod:: commenter - .. automethod:: optimizer - .. automethod:: tester - -Description ------------ - -The ``CoderGPT`` class provides a suite of methods to interact with code files, including inspection, explanation, commenting, optimization, and testing. It utilizes language models to perform these tasks. - -Dependencies ------------- - -- os -- pathlib.Path -- typing.Optional, typing.Union -- yaml -- langchain_core.prompts.ChatPromptTemplate -- langchain_openai.ChatOpenAI -- tabulate.tabulate -- codergpt.commenter.commenter.CodeCommenter -- codergpt.constants.EXTENSION_MAP_FILE, codergpt.constants.GPT_3_5_TURBO, codergpt.constants.INSPECTION_HEADERS -- codergpt.explainer.explainer.CodeExplainer -- codergpt.optimizer.optimizer.CodeOptimizer - -Usage ------ - -To use the ``CoderGPT`` class, initialize it with an optional model parameter. Then call its methods with appropriate arguments to perform various operations on code files. - -Example: - -.. code-block:: python - - if __name__ == "__main__": - coder = CoderGPT() - coder.inspect_package("src") - -Environment Variables ---------------------- - -- ``OPENAI_API_KEY``: The API key for OpenAI services. - -Files ------ - -- ``EXTENSION_MAP_FILE``: YAML file mapping file extensions to programming languages. diff --git a/docs/explainer.rst b/docs/explainer.rst new file mode 100644 index 0000000..1cc3086 --- /dev/null +++ b/docs/explainer.rst @@ -0,0 +1,35 @@ +.. automodule:: explainer_module + :members: + :undoc-members: + :show-inheritance: + +Explainer Module +================ + +.. autoclass:: CodeExplainer + :members: + :undoc-members: + :show-inheritance: + + The ``CodeExplainer`` class is designed to extract and explain code from a given file. It utilizes a runnable chain to process and interpret the code, providing explanations for specific functions, classes, or the entire codebase. + + Initialization + -------------- + + .. automethod:: __init__ + + :param chain: A ``RunnableSerializable`` object that is capable of executing tasks. This is essential for the ``CodeExplainer`` class as it relies on this chain to process and explain the code. + + Explanation Method + ------------------ + + .. automethod:: explain + + This method is responsible for explaining the contents of the code. It can target specific functions, classes, or the entire code base depending on the parameters provided. + + :param code: The actual code that you want to be explained. This should be a string containing the code snippet or file contents. + :param function: (Optional) The name of the function within the code that you specifically want explained. If not provided, the explainer may focus on the entire code or specified class. + :param classname: (Optional) The name of the class within the code that you specifically want explained. If not provided, the explainer may focus on the entire code or a specified function. + :param language: (Optional) The programming language of the code. Providing this information can help tailor the explanation process to the specific syntax and semantics of the language. + + The method invokes the runnable chain with the code (and optionally, the specific function or class) to be explained. It then pretty prints the explanation returned by the chain. diff --git a/docs/expression_evaluator.rst b/docs/expression_evaluator.rst new file mode 100644 index 0000000..7a3e7f9 --- /dev/null +++ b/docs/expression_evaluator.rst @@ -0,0 +1,90 @@ +The ExpressionEvaluator Class +============================= + +The ExpressionEvaluator class is designed for evaluating code expressions and extracting the source code of specified functions or classes from a given source code input. It utilizes the Abstract Syntax Tree (AST) provided by Python's ``ast`` module to navigate and interpret the structure of the code. + +.. code-block:: python + + from expression_evaluator import ExpressionEvaluator + +Initialization +-------------- + +To initialize an instance of the ExpressionEvaluator class, the source code to be analyzed needs to be provided, along with optional parameters specifying the names of the function or class to extract. + +.. code-block:: python + + evaluator = ExpressionEvaluator(source_code, function_name="my_function", class_name="MyClass") + +Parameters: + +- ``source_code``: The complete source code as a string. +- ``function_name``: Optional. The name of the function to find and extract from the source code. +- ``class_name``: Optional. The name of the class to find and extract from the source code. + +Attributes +---------- + +- ``function_code``: After evaluation, this attribute contains the extracted source code of the specified function, if found. +- ``class_code``: After evaluation, this attribute contains the extracted source code of the specified class, if found. +- ``function_name``: The name of the function to search for in the source code. +- ``class_name``: The name of the class to search for in the source code. +- ``source_code``: The source code provided during initialization. + +Methods +------- + +visit_FunctionDef +^^^^^^^^^^^^^^^^^ + +This method is called when a function definition node is encountered in the AST. If the name of the function matches the target function name specified during initialization, the source segment of the function is extracted. + +.. code-block:: python + + def visit_FunctionDef(self, node): + ... + +Parameters: + +- ``node``: An instance of ``ast.FunctionDef`` representing a function definition node in the AST. + +visit_ClassDef +^^^^^^^^^^^^^^ + +This method is called when a class definition node is encountered in the AST. If the name of the class matches the target class name specified during initialization, the source segment of the class is extracted. + +.. code-block:: python + + def visit_ClassDef(self, node): + ... + +Parameters: + +- ``node``: An instance of ``ast.ClassDef`` representing a class definition node in the AST. + +Usage +----- + +To utilize the ExpressionEvaluator class, an instance must be created with the source code and optionally the function or class name. After initialization, the Abstract Syntax Tree is traversed, and if the specified function or class is found, its source code is extracted and stored in the ``function_code`` or ``class_code`` attributes respectively. + +Example: + +.. code-block:: python + + source = ''' + def hello_world(): + print("Hello, world!") + + class Greeter: + def greet(self): + print("Hello, world!") + ''' + + evaluator = ExpressionEvaluator(source, function_name="hello_world") + ast.parse(source) + evaluator.visit(ast.parse(source)) + + print(evaluator.function_code) + # Output: def hello_world():\n print("Hello, world!") + +This class provides a straightforward way to extract specific portions of code from a larger source code base, leveraging the power of Python's AST for code analysis and manipulation. diff --git a/docs/main.rst b/docs/main.rst new file mode 100644 index 0000000..1a0ad16 --- /dev/null +++ b/docs/main.rst @@ -0,0 +1,77 @@ +.. automodule:: codergpt + :members: + :undoc-members: + :show-inheritance: + +CoderGPT +======= + +.. autoclass:: CoderGPT + :members: + :undoc-members: + :show-inheritance: + +The ``CoderGPT`` class is designed to integrate various functionalities for working with code files, including inspecting, explaining, commenting, optimizing, testing, and documenting the code. It leverages a language model from OpenAI, specifically GPT-4 Turbo, to perform these tasks. + +.. automethod:: __init__ + +Constructor +----------- + +.. automethod:: CoderGPT.__init__ + +The constructor initializes the ``CoderGPT`` class with a specified model. By default, it uses the GPT-4 Turbo model. It sets up the language model with an API key from the environment variables and constructs a chain of operations starting with a prompt template indicating the role of a world-class software developer. + +Inspect Package +--------------- + +.. automethod:: inspect_package + +This method inspects a given package or directory path, identifying the programming language of each file based on its file extension. It displays a table mapping files to their detected languages and returns this mapping as a dictionary. + +Get Code +-------- + +.. automethod:: get_code + +Extracts and returns the source code of a specified function or class within a file. If neither a function nor a class is specified, it returns the entire source code of the file. + +Explainer +--------- + +.. automethod:: explainer + +Takes a path to a code file and optionally a function or class name, then explains the content or functionality of the specified code. It utilizes a ``CodeExplainer`` instance to perform this explanation. + +Commenter +--------- + +.. automethod:: commenter + +Adds comments to a code file at the specified path. It can optionally overwrite existing comments. This is facilitated by a ``CodeCommenter`` instance. + +Optimizer +--------- + +.. automethod:: optimizer + +Optimizes the code within a file for performance or readability. The method allows specifying a function or class to focus the optimization on. It leverages a ``CodeOptimizer`` to perform these optimizations. + +Test Writer +----------- + +.. automethod:: test_writer + +Generates tests for a specified function or class within a code file. This functionality is provided through a ``CodeTester`` instance. + +Documenter +---------- + +.. automethod:: documenter + +Documents the code in a given file, producing comprehensive documentation that can include descriptions of functionality, parameters, return values, and more. This is accomplished using a ``CodeDocumenter`` instance. + +Main +---- + +The script checks if it is the main module and, if so, creates an instance of ``CoderGPT`` and inspects the "src" directory. This serves as an example of how to use the ``CoderGPT`` class to perform an inspection of a codebase. \ No newline at end of file diff --git a/docs/optimizer.rst b/docs/optimizer.rst new file mode 100644 index 0000000..babe5a1 --- /dev/null +++ b/docs/optimizer.rst @@ -0,0 +1,51 @@ +Optimizer Module +================ + +.. module:: optimizer + :synopsis: Module containing the CodeOptimizer class for optimizing code files. + +.. moduleauthor:: Your Name + +Classes +------- + +.. autoclass:: CodeOptimizer + :members: + :undoc-members: + :show-inheritance: + + The CodeOptimizer class is responsible for optimizing, commenting, and adding Sphinx-style docstrings to code within a given file. It leverages a provided runnable chain capable of executing tasks to perform the optimization. + + .. automethod:: __init__ + + .. automethod:: optimize + +``CodeOptimizer`` +----------------- + +.. py:class:: CodeOptimizer(chain) + + Initializes the CodeOptimizer class with a runnable chain for executing optimization tasks. + + :param chain: A RunnableSerializable object capable of executing tasks. This chain is invoked to perform code optimization, commenting, and documentation. + +.. py:method:: optimize(filename, function=None, classname=None, overwrite=False) + + Optimizes the code within the specified file. This method reads the source code, invokes the optimization chain, and writes the optimized code either to the same file or to a new file with an "_updated" suffix based on the ``overwrite`` flag. + + :param str filename: The path to the code file to be optimized. This is the file from which the source code will be read. + :param str function: (Optional) The name of the function within the file to specifically optimize. If provided, only this function will be targeted for optimization. Default is None. + :param str classname: (Optional) The name of the class within the file to specifically optimize. If provided, only this class will be targeted for optimization. Default is None. + :param bool overwrite: If True, the original file will be overwritten with the optimized code. Otherwise, a new file with the "_updated" suffix will be created to hold the optimized code. Default is False. + + **Example Usage**: + + .. code-block:: python + + from langchain_core.runnables.base import RunnableSerializable + from optimizer import CodeOptimizer + + # Assuming 'chain' is an instance of RunnableSerializable + optimizer = CodeOptimizer(chain) + optimizer.optimize("example.py", overwrite=True) + diff --git a/docs/test_writer.rst b/docs/test_writer.rst new file mode 100644 index 0000000..6389162 --- /dev/null +++ b/docs/test_writer.rst @@ -0,0 +1,38 @@ +Test Writing Module +=================== + +This module contains the ``CodeTester`` class, which is designed to automatically generate and write test cases for given source code files. It uses a runnable chain to generate tests for either specific functions, classes, or entire code files. + +Classes +------- + +.. autoclass:: CodeTester + :members: + :undoc-members: + :show-inheritance: + + .. automethod:: __init__ + .. automethod:: write_tests + +CodeTester +---------- + +.. py:class:: CodeTester(chain) + + The ``CodeTester`` class is responsible for initializing with a runnable chain and writing tests for code. + + .. py:method:: __init__(chain: RunnableSerializable[Dict, Any]) + + Initializes the ``CodeTester`` class with a runnable chain. + + :param chain: A ``RunnableSerializable`` object capable of executing tasks. + + .. py:method:: write_tests(filename: Union[str, Path], function: Optional[str] = None, classname: Optional[str] = None, outfile: Optional[str] = None) + + Writes tests for the code specified in the given file by invoking the runnable chain. The tests can be generated for a specific function, class, or the entire file. The generated test code is then written to an output file. + + :param filename: The path to the code file for which tests are to be generated. + :param function: (Optional) The name of the function within the file to generate tests for. Default is None, indicating that tests should not be limited to a specific function. + :param classname: (Optional) The name of the class within the file to generate tests for. Default is None, indicating that tests should not be limited to a specific class. + :param outfile: (Optional) The path to the output file where the generated test code should be written. If not specified, tests are written to a default directory with a filename prefixed with ``test_``. + diff --git a/src/codergpt/__init__.py b/src/codergpt/__init__.py index 433b590..2c29c2d 100644 --- a/src/codergpt/__init__.py +++ b/src/codergpt/__init__.py @@ -3,6 +3,7 @@ import importlib_metadata from codergpt.commenter import CodeCommenter +from codergpt.documenter import CodeDocumenter from codergpt.explainer import CodeExplainer from codergpt.optimizer import CodeOptimizer @@ -16,4 +17,4 @@ __version__ = "0.0.0" # pragma: no cover -__all__ = ["CoderGPT", "CodeExplainer", "CodeCommenter", "CodeOptimizer"] +__all__ = ["CoderGPT", "CodeExplainer", "CodeCommenter", "CodeOptimizer", "CodeDocumenter"] diff --git a/src/codergpt/cli.py b/src/codergpt/cli.py index 7231abc..f400703 100644 --- a/src/codergpt/cli.py +++ b/src/codergpt/cli.py @@ -169,5 +169,24 @@ def write_test_code(path: Union[str, Path], function: str, classname: str): raise ValueError("The path provided is not a file.") +@main.command("document") +@path_argument +def write_documentation(path: Union[str, Path]): + """ + Write documentation files for the code file. + + :param path: The path to the code file. + """ + # 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.documenter(path=path) + else: + raise ValueError("The path provided is not a file.") + + if __name__ == "__main__": main() diff --git a/src/codergpt/constants.py b/src/codergpt/constants.py index 7e63676..b2fd8e9 100644 --- a/src/codergpt/constants.py +++ b/src/codergpt/constants.py @@ -2,8 +2,10 @@ from pathlib import Path -TEST_DIR = Path(__file__).resolve().parents[2] / "tests" -SRC = Path(__file__).resolve().parents[1] +PROJECT_DIR = Path(__file__).resolve().parents[2] +TEST_DIR = PROJECT_DIR / "tests" +DOCS_DIR = PROJECT_DIR / "docs" +SRC = PROJECT_DIR / "src" PACKAGE_DIR = SRC / "codergpt" EXTENSION_MAP_FILE = PACKAGE_DIR / "extensions.yaml" LANGUAGE_MAP_KEY = "language-map" diff --git a/src/codergpt/documenter/__init__.py b/src/codergpt/documenter/__init__.py index 8e7d6b7..dc2b958 100644 --- a/src/codergpt/documenter/__init__.py +++ b/src/codergpt/documenter/__init__.py @@ -1 +1,5 @@ """Documenter module for codergpt.""" + +from .documenter import CodeDocumenter + +__all__ = ["CodeDocumenter"] diff --git a/src/codergpt/documenter/documenter.py b/src/codergpt/documenter/documenter.py index 8e7d6b7..6935ea1 100644 --- a/src/codergpt/documenter/documenter.py +++ b/src/codergpt/documenter/documenter.py @@ -1 +1,54 @@ """Documenter module for codergpt.""" + +from pathlib import Path +from typing import Any, Dict, Optional, Union + +from langchain_core.runnables.base import RunnableSerializable + +from codergpt.constants import DOCS_DIR + + +class CodeDocumenter: + """Code Explainer class that extracts and explains code from a given file.""" + + def __init__(self, chain: RunnableSerializable[Dict, Any]): + """ + Initialize the CodeDocumenter class with a runnable chain. + + :param chain: A RunnableSerializable object capable of executing tasks. + """ + self.chain = chain + + def document( + self, + filename: Union[str, Path], + outfile: Optional[str] = None, + ): + """ + Document the contents of the code file by invoking the runnable chain. + + :param code: The string containing the code to be documented. + :param function: The name of the function to document. Default is None. + :param classname: The name of the class to document + """ + if isinstance(filename, str): + filename = Path(filename) + with open(filename, "r") as source_file: + source_code = source_file.read() + response = self.chain.invoke( + { + "input": f"Document the following code: \n\n```\n{source_code}\n```" + "This should be in reStructuredText (RST, ReST, or reST)" + " format that is Sphinx compatible. Return only the documentation content." + } + ) + + # Extract the commented code from the response if necessary + documentation = response.content + if outfile: + destination_path = outfile + else: + destination_path = DOCS_DIR / f"{filename.stem}.rst" + # Write the documentation to the new file + with open(destination_path, "w") as updated_file: + updated_file.write(documentation) diff --git a/src/codergpt/explainer/explainer.py b/src/codergpt/explainer/explainer.py index dd651f6..9c35060 100644 --- a/src/codergpt/explainer/explainer.py +++ b/src/codergpt/explainer/explainer.py @@ -22,7 +22,7 @@ def explain( """ Explain the contents of the code file by invoking the runnable chain. - :param path: The path to the code file to be explained. + :param code: The code 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. """ diff --git a/src/codergpt/main.py b/src/codergpt/main.py index 9749897..f8e577b 100644 --- a/src/codergpt/main.py +++ b/src/codergpt/main.py @@ -11,6 +11,7 @@ from codergpt.commenter.commenter import CodeCommenter from codergpt.constants import EXTENSION_MAP_FILE, GPT_4_TURBO, INSPECTION_HEADERS +from codergpt.documenter.documenter import CodeDocumenter from codergpt.explainer.explainer import CodeExplainer from codergpt.optimizer.optimizer import CodeOptimizer from codergpt.test_writer.test_writer import CodeTester @@ -136,6 +137,15 @@ def test_writer(self, path: Union[str, Path], function: str = None, classname: s code_tester = CodeTester(self.chain) code_tester.write_tests(filename=path, function=function, classname=classname) + def documenter(self, path: Union[str, Path]): + """ + Document the code file. + + :param path: The path to the code file. + """ + code_documenter = CodeDocumenter(self.chain) + code_documenter.document(filename=path) + if __name__ == "__main__": coder = CoderGPT() diff --git a/src/codergpt/test_writer/test_writer.py b/src/codergpt/test_writer/test_writer.py index 1e01289..74b2261 100644 --- a/src/codergpt/test_writer/test_writer.py +++ b/src/codergpt/test_writer/test_writer.py @@ -41,14 +41,14 @@ def write_tests( response = self.chain.invoke( { "input": f"Write tests for the function '{function}' in \n\n```\n{source_code}\n```" - "Return just the code block. Also explain the tests in a systematic way as a comment." + "Return just the code block since all this will be a test file." } ) if classname: response = self.chain.invoke( { "input": f"Write tests for the class '{classname}' in \n\n```\n{source_code}\n```" - "Also explain the tests in a systematic way." + "Return just the code block since all this will be a test file." } ) else: @@ -56,7 +56,7 @@ def write_tests( response = self.chain.invoke( { "input": f"Write tests for the following code: \n\n```\n{source_code}\n```" - "Also explain the tests in a systematic way." + "Return just the code block since all this will be a test file." } ) test_code = response.content diff --git a/tests/test_documenter.py b/tests/test_documenter.py new file mode 100644 index 0000000..8b216c9 --- /dev/null +++ b/tests/test_documenter.py @@ -0,0 +1,115 @@ +""" +Tests for the CodeDocumenter class. + +This module contains test cases for testing +the functionality of the CodeDocumenter class, ensuring it behaves as expected +under various conditions. + +.. module:: test_documenter + :synopsis: Tests for the CodeDocumenter class. +""" + +import unittest +from pathlib import Path +from unittest.mock import Mock, patch + +from codergpt.constants import DOCS_DIR +from codergpt.documenter import CodeDocumenter + + +class TestCodeDocumenter(unittest.TestCase): + """ + Test suite for the CodeDocumenter class. + + This class contains setup methods and test cases to validate the + functionality of the CodeDocumenter class. + """ + + def setUp(self): + """ + Set up method to prepare the test fixture before each test method. + + This method is called before each individual test function, and it + initializes a mock chain and a CodeDocumenter instance to be used + across different test cases. + """ + self.mock_chain = Mock() + self.documenter = CodeDocumenter(chain=self.mock_chain) + + @patch("builtins.open", new_callable=unittest.mock.mock_open, read_data="def example():\n pass") + def test_document_with_mock_chain(self, mock_open): + """ + Test the document method with a mocked chain. + + This test case checks if the document method correctly interacts with + the provided mock objects when documenting code with a specified output + file. + + :param mock_open: A mock object for the open function. + :type mock_open: unittest.mock.MagicMock + """ + # Setup + filename = "example.py" + outfile = "output.rst" + self.mock_chain.invoke.return_value = Mock(content="Documented content") + + # Execute + self.documenter.document(filename, outfile) + + # Assert + self.mock_chain.invoke.assert_called_once() + mock_open.assert_called_with(outfile, "w") + mock_open().write.assert_called_once_with("Documented content") + + @patch("builtins.open", new_callable=unittest.mock.mock_open, read_data="def example():\n pass") + def test_document_without_outfile(self, mock_open): + """ + Test the document method without specifying an output file. + + This test case checks if the document method correctly handles the + case where no output file is specified, defaulting to saving the + document in the DOCS_DIR directory. + + :param mock_open: A mock object for the open function. + :type mock_open: unittest.mock.MagicMock + """ + # Setup + filename = "example.py" + self.mock_chain.invoke.return_value = Mock(content="Documented content") + + # Execute + self.documenter.document(filename) + + # Assert + expected_path = DOCS_DIR / "example.rst" + mock_open.assert_called_with(expected_path, "w") + mock_open().write.assert_called_once_with("Documented content") + + def test_document_with_pathlib_path(self): + """ + Test the document method with a Pathlib path as input. + + This test case verifies that the document method correctly handles + input filenames provided as Pathlib Path objects, documenting the code + with a specified output file. + + The open function is patched and spied on to ensure the file operations + are performed as expected. + """ + # Setup + filename = Path("example.py") + outfile = "output.rst" + self.mock_chain.invoke.return_value = Mock(content="Documented content") + + # Patch and Spy + with patch("builtins.open", unittest.mock.mock_open()) as mock_open: + self.documenter.document(filename, outfile) + + # Assert + self.mock_chain.invoke.assert_called_once() + mock_open.assert_called_with(outfile, "w") + mock_open().write.assert_called_once_with("Documented content") + + +if __name__ == "__main__": + unittest.main()