Skip to content

Commit

Permalink
Return meaningful system codes (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
rob-luke authored Apr 27, 2023
1 parent 4ba6635 commit d62c1c7
Show file tree
Hide file tree
Showing 5 changed files with 125 additions and 27 deletions.
16 changes: 16 additions & 0 deletions .aidev
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
ignore *.yml
ignore Dockerfile
ignore LICENSE

slice dev
only docstring_auditor/*.py
only tests/*.py

slice main
only docstring_auditor/*.py

slice test
only tests/*.py

slice docs
only README.md
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,5 @@ cython_debug/

package-lock.json
package.json
***.draft

91 changes: 68 additions & 23 deletions docstring_auditor/main.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#!/usr/bin/env python3

import os
import sys
import click
import ast
import json
from typing import List, Optional, Dict
from typing import List, Optional, Dict, Tuple
import openai


Expand All @@ -23,7 +24,7 @@ def extract_functions(file_path: str) -> List[Optional[str]]:
Returns
-------
List[str]
List[Optional[str]]
A list of strings, where each string contains the entire code for a
function, including the definition, docstring, and code.
Expand Down Expand Up @@ -65,7 +66,7 @@ def ask_for_critique(function: str) -> Dict[str, str]:
Returns
-------
response_dict : dict
response_dict : Dict[str, str]
A dictionary containing the analysis of the docstring, including function name, errors, warnings, and the corrected docstring if needed.
"""

Expand All @@ -77,6 +78,7 @@ def ask_for_critique(function: str) -> Dict[str, str]:
"You have extensive knowledge of all coding languages and packages."
"You will review the documentation for python functions that I provide."
"The documentation you are helping to write is written for someone with very little coding experience."
"Do not provide errors or warnings about imports."
"Please provide verbose descriptions and ensure no assumptions are made in the documentation."
)

Expand Down Expand Up @@ -117,73 +119,92 @@ def ask_for_critique(function: str) -> Dict[str, str]:
return response_dict


def report_concerns(response_dict: Dict[str, str]) -> bool:
def report_concerns(response_dict: Dict[str, str]) -> Tuple[int, int]:
"""
Inform the user of any concerns with the docstring.
Parameters
----------
response_dict : dict
response_dict : Dict[str, str]
A dictionary containing the function name, error, warning, and solution.
Returns
-------
bool
Returns True if there are any concerns (errors or warnings), otherwise returns False.
Tuple[int, int]
Returns a tuple containing the count of errors and warnings found in the docstring.
"""
function_name = response_dict["function"]
error = response_dict["error"]
warning = response_dict["warning"]
solution = response_dict["solution"]

error_count = 0
warning_count = 0

if not error and not warning:
click.secho(
f"No concerns found with the docstring for the function: {function_name}\n",
fg="green",
)
return False
else:
if error:
click.secho(
f"An error was found in the function: {function_name}\n", fg="red"
)
click.secho(f"{error}\n", fg="red")
error_count += 1
if warning:
click.secho(
f"A warning was found in the function: {function_name}\n", fg="yellow"
)
click.secho(f"{warning}\n", fg="yellow")
warning_count += 1
if solution:
click.secho(f"A proposed solution to these concerns is:\n\n{solution}\n\n")
return True

return error_count, warning_count


def process_file(file_path: str):
def process_file(file_path: str) -> Tuple[int, int]:
"""
Process a single Python file and analyze its functions' docstrings.
This function processes the given Python file, extracts the functions within it,
and analyzes their docstrings for errors and warnings.
It then returns the total number of errors and warnings found in the
docstrings of the functions in the given file.
Parameters
----------
file_path : str
The path to the .py file to analyze the functions' docstrings.
Returns
-------
None
The function does not return any value. It prints the critiques and suggestions for the docstrings in the given file.
Tuple[int, int]
A tuple containing the total number of errors and warnings found in the docstrings of the functions in the given file.
"""
functions = extract_functions(file_path)

error_count = 0
warning_count = 0

for idx, function in enumerate(functions):
print(
f"Processing function {idx + 1} of {len(functions)} in file {file_path}..."
)
assert isinstance(function, str)
critique = ask_for_critique(function)
report_concerns(critique)
errors, warnings = report_concerns(critique)
error_count += errors
warning_count += warnings

return error_count, warning_count


def process_directory(directory_path: str, ignore_dirs: Optional[List[str]] = None):
def process_directory(
directory_path: str, ignore_dirs: Optional[List[str]] = None
) -> Tuple[int, int]:
"""
Recursively process all .py files in a directory and its subdirectories, ignoring specified directories.
Expand All @@ -197,19 +218,26 @@ def process_directory(directory_path: str, ignore_dirs: Optional[List[str]] = No
Returns
-------
None
The function does not return any value. It prints the critiques and suggestions for the docstrings in the .py files.
Tuple[int, int]
A tuple containing the total number of errors and warnings found in the docstrings of the .py files.
"""
if ignore_dirs is None:
ignore_dirs = ["tests"]

error_count = 0
warning_count = 0

for root, dirs, files in os.walk(directory_path):
dirs[:] = [d for d in dirs if d not in ignore_dirs]

for file in files:
if file.endswith(".py"):
file_path = os.path.join(root, file)
process_file(file_path)
errors, warnings = process_file(file_path)
error_count += errors
warning_count += warnings

return error_count, warning_count


@click.command(name="DocstringAuditor")
Expand All @@ -221,7 +249,13 @@ def process_directory(directory_path: str, ignore_dirs: Optional[List[str]] = No
default=["tests"],
help="A list of directory names to ignore while processing .py files. Separate multiple directories with a space.",
)
def docstring_auditor(path: str, ignore_dirs: List[str]):
@click.option(
"--error-on-warnings",
is_flag=True,
default=False,
help="If true, warnings will be treated as errors and included in the exit code count.",
)
def docstring_auditor(path: str, ignore_dirs: List[str], error_on_warnings: bool):
"""
Analyze Python functions' docstrings in a given file or directory and provide critiques and suggestions for improvement.
Expand All @@ -233,23 +267,34 @@ def docstring_auditor(path: str, ignore_dirs: List[str]):
----------
path : str
The path to the .py file or directory to analyze the functions' docstrings.
ignore_dirs : List[str]
A list of directory names to ignore while processing .py files.
error_on_warnings : bool
If true, warnings will be treated as errors and included in the exit code count.
Returns
-------
None
The function does not return any value. It prints the critiques and suggestions for the docstrings in the given file or directory.
"""
if os.path.isfile(path):
process_file(path)
error_count, warning_count = process_file(path)
elif os.path.isdir(path):
process_directory(path, ignore_dirs)
error_count, warning_count = process_directory(path, ignore_dirs)
else:
click.secho(
"Invalid path. Please provide a valid file or directory path.", fg="red"
error_text = "Invalid path. Please provide a valid file or directory path."
click.secho(error_text, fg="red")
sys.exit(error_text)

if error_count > 0 or (error_on_warnings and warning_count > 0):
error_text = (
f"Auditor identified {error_count} errors and {warning_count} warnings."
)
click.secho(error_text, fg="red")
sys.exit(error_count + (warning_count if error_on_warnings else 0))
else:
click.secho("No errors found.", fg="green")
sys.exit(0)


if __name__ == "__main__":
Expand Down
36 changes: 36 additions & 0 deletions tests/test_exit_codes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import pytest
import tempfile
from unittest.mock import patch
from docstring_auditor.main import docstring_auditor
from click.testing import CliRunner


# Test cases for sys exit values
test_data = [
# (description, error_count, warning_count, error_on_warnings, expected_exit)
("No errors or warnings", 0, 0, False, 0),
("One error, no warnings", 1, 0, False, 1),
("No errors, one warning (no error on warnings)", 0, 1, False, 0),
("No errors, one warning (with error on warnings)", 0, 1, True, 1),
("One error, one warning (no error on warnings)", 1, 1, False, 1),
("One error, one warning (with error on warnings)", 1, 1, True, 2),
]


@pytest.mark.parametrize(
"desc, error_count, warning_count, error_on_warnings, expected_exit", test_data
)
def test_docstring_auditor_exit_values(
desc, error_count, warning_count, error_on_warnings, expected_exit
):
with tempfile.TemporaryDirectory() as tempdir:
with patch(
"docstring_auditor.main.process_directory",
return_value=(error_count, warning_count),
):
runner = CliRunner()
args = [tempdir, "--ignore-dirs", "tests"]
if error_on_warnings:
args.append("--error-on-warnings")
result = runner.invoke(docstring_auditor, args=args)
assert result.exit_code == expected_exit, f"{desc} failed"
8 changes: 4 additions & 4 deletions tests/test_reporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def mock_secho(*args, **kwargs):
"solution": "",
}
result = report_concerns(response_dict)
assert result is False
assert result == (0, 0)
assert len(captured_output) == 1
assert "No concerns found" in captured_output[0]

Expand All @@ -38,7 +38,7 @@ def mock_secho(*args, **kwargs):
"solution": "Updated docstring.",
}
result = report_concerns(response_dict)
assert result is True
assert result == (1, 0)
assert len(captured_output) == 3
assert "An error was found" in captured_output[0]
assert "An error occurred." in captured_output[1]
Expand All @@ -60,7 +60,7 @@ def mock_secho(*args, **kwargs):
"solution": "",
}
result = report_concerns(response_dict)
assert result is True
assert result == (0, 1)
assert len(captured_output) == 2
assert "A warning was found" in captured_output[0]
assert "A warning occurred." in captured_output[1]
Expand All @@ -82,7 +82,7 @@ def mock_secho(*args, **kwargs):
}
result = report_concerns(response_dict)
print(captured_output)
assert result is True
assert result == (1, 1)
assert len(captured_output) == 5
assert "An error was found" in captured_output[0]
assert "An error occurred." in captured_output[1]
Expand Down

0 comments on commit d62c1c7

Please sign in to comment.