Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: optional weighted checks #161

Open
wants to merge 42 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
b821098
feat(output.py): began working on grade weights
dyga01 Nov 12, 2024
34c4cfb
fix(output.py): removed weighted shell check code
dyga01 Nov 12, 2024
a30ff5c
update: added gatorgrade.yml content as list into run_checks() in out…
TitusSmith33 Nov 14, 2024
2aa1b53
fix(checks.py): added in options to shell checks
dyga01 Nov 19, 2024
524b090
fix(main.py): removed print statements
dyga01 Nov 19, 2024
f7c5dcf
fix(checks.py): changing dict type to list
dyga01 Nov 19, 2024
52d50f3
fix(checks.py): testing if list and tuple work together
dyga01 Nov 19, 2024
c77371e
fix(checks.py): removed optional
dyga01 Nov 19, 2024
9ae63d4
fix(checks.py): switched tuples
dyga01 Nov 19, 2024
d594716
fix(checks.py): using optional dict to store options
dyga01 Nov 19, 2024
d259f53
fix(checks.py): using list of str to store options
dyga01 Nov 19, 2024
88e0bbf
fix(checks): attempting to use dict to store weight
dyga01 Nov 20, 2024
9800bba
fix(checks): attempting to pass gg_args to shell check
dyga01 Nov 20, 2024
6e6b020
fix(gg_args): working to fix invalid checks error
dyga01 Nov 20, 2024
81d9f61
fix(gg_args): make the check attribute optional
dyga01 Nov 20, 2024
01c448c
fix(gg_args): add a default check if there is none so the shell check…
dyga01 Nov 20, 2024
6ad6ea1
fix(gg_args): fix the default check
dyga01 Nov 20, 2024
a248aca
fix(gg_args): fix the description tag
dyga01 Nov 21, 2024
9fb5864
fix(run_shell_check): fixed to call the right function
dyga01 Nov 21, 2024
e8e7b81
test: displaying weight and check on 352 in run_checks()
TitusSmith33 Nov 21, 2024
ba70bf2
test: fixing weight output on 352 in run_checks()
TitusSmith33 Nov 21, 2024
afe9fbc
test: moving weight print in run_checks()
TitusSmith33 Nov 21, 2024
1fca0a2
test: trying to add % in output
TitusSmith33 Nov 21, 2024
2713058
test: added percentage for each check in output
TitusSmith33 Nov 21, 2024
c1cb403
test: percentage output
TitusSmith33 Nov 21, 2024
e4ce498
test: convert individual check to percentage in output
TitusSmith33 Nov 21, 2024
abc1692
test: percent output
TitusSmith33 Nov 21, 2024
10bc5bf
test: testing output with check
TitusSmith33 Nov 21, 2024
afcf3cb
test: adding to check lines in output
TitusSmith33 Nov 21, 2024
36feab5
test: fixing issue with same line output
TitusSmith33 Nov 21, 2024
5bd2183
test: testing re to remove format
TitusSmith33 Nov 21, 2024
1e01d42
feat: added weight extraction function for output
TitusSmith33 Nov 21, 2024
5143f37
test: rounded numbers for better output
TitusSmith33 Nov 21, 2024
f64a7cc
test: combining lines to add % to check line
TitusSmith33 Nov 21, 2024
d93bce6
fix: trying to print the result between the check and description
dyga01 Nov 22, 2024
208874b
fix: attempting to put % between strings
dyga01 Nov 22, 2024
2747a1f
fix: formatting and extra spacing
dyga01 Nov 22, 2024
e6a2654
fix: formatting and test issues
dyga01 Nov 22, 2024
ae4cbf5
fix: fixing mypy errors
dyga01 Nov 22, 2024
5826b32
fix: remove unused console testing
TitusSmith33 Dec 5, 2024
57612d0
Merge branch 'weighted_checks' of github.com:dyga01/gatorgrade into w…
TitusSmith33 Dec 5, 2024
bd25348
fix: added back in test_parse_config_check_gg_matchfilefragment() to …
dyga01 Dec 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python 3.12.0
18 changes: 15 additions & 3 deletions gatorgrade/input/checks.py
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
"""Define check classes."""

from typing import List
from typing import List, Optional


class ShellCheck: # pylint: disable=too-few-public-methods
"""Represent a shell check."""

def __init__(self, command: str, description: str = None, json_info=None): # type: ignore
def __init__(
self,
command: str,
description: Optional[str] = None,
json_info=None,
gg_args: Optional[List[str]] = None,
): # type: ignore
"""Construct a ShellCheck.

Args:
command: The command to run in a shell.
description: The description to use in output.
If no description is given, the command is used as the description.
json_info: The all-encompassing check information to include in json output.
If none is given, command is used
If none is given, command is used.
options: Additional options for the shell check as a dictionary.
"""
self.command = command
self.description = description if description is not None else command
self.json_info = json_info
self.gg_args = gg_args if gg_args is not None else []

def __str__(self):
"""Return a string representation of the ShellCheck."""
return f"ShellCheck(command={self.command}, description={self.description}, json_info={self.json_info}, gg_args={self.gg_args})"


class GatorGraderCheck: # pylint: disable=too-few-public-methods
Expand Down
57 changes: 31 additions & 26 deletions gatorgrade/input/command_line_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,44 +24,49 @@ def generate_checks(
"""
checks: List[Union[ShellCheck, GatorGraderCheck]] = []
for check_data in check_data_list:
gg_args = []
# Add name of check if it exists in data, otherwise use default_check
check_name = check_data.check.get("check", "ConfirmFileExists")
gg_args.append(str(check_name))
# Add any additional options
options = check_data.check.get("options")
if options is not None:
for option in options:
# If option should be a flag (i.e. its value is the `True` boolean),
# add only the option without a value
option_value = options[option]
if isinstance(option_value, bool):
if option_value:
gg_args.append(f"--{option}")
# Otherwise, add both the option and its value
else:
gg_args.extend([f"--{option}", str(option_value)])
# Add directory and file if file context in data
if check_data.file_context is not None:
# Get the file and directory using os
dirname, filename = os.path.split(check_data.file_context)
if dirname == "":
dirname = "."
gg_args.extend(["--directory", dirname, "--file", filename])

# If the check has a `command` key, then it is a shell check
if "command" in check_data.check:
# Do not add GatorGrader-specific arguments to gg_args for shell checks
shell_gg_args = gg_args.copy()
checks.append(
ShellCheck(
command=check_data.check.get("command"),
description=check_data.check.get("description"),
json_info=check_data.check,
gg_args=shell_gg_args,
)
)
# Otherwise, it is a GatorGrader check
else:
gg_args = []
# Add description option if in data
# Add the description to gg_args for GatorGrader checks
description = check_data.check.get("description")
if description is not None:
gg_args.extend(["--description", str(description)])
# Always add name of check, which should be in data
gg_args.append(str(check_data.check.get("check")))
# Add any additional options
options = check_data.check.get("options")
if options is not None:
for option in options:
# If option should be a flag (i.e. its value is the `True` boolean),
# add only the option without a value
option_value = options[option]
if isinstance(option_value, bool):
if option_value:
gg_args.append(f"--{option}")
# Otherwise, add both the option and its value
else:
gg_args.extend([f"--{option}", str(option_value)])
# Add directory and file if file context in data
if check_data.file_context is not None:
# Get the file and directory using os
dirname, filename = os.path.split(check_data.file_context)
if dirname == "":
dirname = "."
gg_args.extend(["--directory", dirname, "--file", filename])
if description:
gg_args.extend(["--description", description])
checks.append(GatorGraderCheck(gg_args=gg_args, json_info=check_data.check))

return checks
25 changes: 19 additions & 6 deletions gatorgrade/output/check_result.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Define check result class."""

from typing import Union
from typing import Optional
import rich


Expand All @@ -12,8 +12,9 @@ def __init__(
passed: bool,
description: str,
json_info,
path: Union[str, None] = None,
path: Optional[str] = None,
diagnostic: str = "No diagnostic message available",
weight: int = 1,
):
"""Construct a CheckResult.

Expand All @@ -23,26 +24,35 @@ def __init__(
description: The description to use in output.
json_info: the overall information to be included in json output
diagnostic: The message to use in output if the check has failed.
weight: The weight of the check.
"""
self.passed = passed
self.description = description
self.json_info = json_info
self.diagnostic = diagnostic
self.path = path
self.run_command = ""
self.weight = weight

def display_result(self, show_diagnostic: bool = False) -> str:
def display_result(
self, show_diagnostic: bool = False, percentage: Optional[float] = None
) -> str:
"""Print check's passed or failed status, description, and, optionally, diagnostic message.

If no diagnostic message is available, then the output will say so.

Args:
show_diagnostic: If true, show the diagnostic message if the check has failed.
Defaults to false.
percentage: The percentage weight of the check.
"""
icon = "✓" if self.passed else "✕"
icon_color = "green" if self.passed else "red"
message = f"[{icon_color}]{icon}[/] {self.description}"
percentage_color = "green" if self.passed else "red"
message = f"[{icon_color}]{icon}[/]"
if percentage is not None:
message += f" [{percentage_color}]({percentage:.2f}%)[/]"
message += f" {self.description}"
if not self.passed and show_diagnostic:
message += f"\n[yellow] → {self.diagnostic}"
return message
Expand All @@ -62,14 +72,17 @@ def __str__(self, show_diagnostic: bool = False) -> str:
message = self.display_result(show_diagnostic)
return message

def print(self, show_diagnostic: bool = False) -> None:
def print(
self, show_diagnostic: bool = False, percentage: Optional[float] = None
) -> None:
"""Print check's passed or failed status, description, and, optionally, diagnostic message.

If no diagnostic message is available, then the output will say so.

Args:
show_diagnostic: If true, show the diagnostic message if the check has failed.
Defaults to false.
percentage: The percentage weight of the check.
"""
message = self.display_result(show_diagnostic)
message = self.display_result(show_diagnostic, percentage)
rich.print(message)
69 changes: 60 additions & 9 deletions gatorgrade/output/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,9 @@ def create_markdown_report_file(json: dict) -> str:
if "command" == i:
val = check["options"]["command"]
markdown_contents += f"\n\t- **command** {val}"
if "weight" == i:
val = check["options"]["weight"]
markdown_contents += f"\n\t- **weight:** {val}"
if "fragment" == i:
val = check["options"]["fragment"]
markdown_contents += f"\n\t- **fragment:** {val}"
Expand Down Expand Up @@ -284,6 +287,19 @@ def write_json_or_md_file(file_name, content_type, content):
) from e


def calculate_total_weight(checks: List[Union[ShellCheck, GatorGraderCheck]]) -> int:
"""Calculate the total weight of all the checks."""
total_weight = 0
for check in checks:
weight = 1 # Default weight
if isinstance(check, (ShellCheck, GatorGraderCheck)):
if "--weight" in check.gg_args:
index_of_weight = check.gg_args.index("--weight")
weight = int(check.gg_args[index_of_weight + 1])
total_weight += weight
return total_weight


def run_checks(
checks: List[Union[ShellCheck, GatorGraderCheck]], report: Tuple[str, str, str]
) -> bool:
Expand All @@ -295,11 +311,11 @@ def run_checks(
Args:
checks: The list of shell and GatorGrader checks to run.
"""
total_weight = calculate_total_weight(checks)
results = []
# run each of the checks
for check in checks:
result = None
command_ran = None
weight = 1
# run a shell check; this means
# that it is going to run a command
# in the shell as a part of a check;
Expand All @@ -308,12 +324,30 @@ def run_checks(
# inside of a CheckResult object but
# not initialized in the constructor
if isinstance(check, ShellCheck):
# Weighted Checks
if "--weight" in check.gg_args:
index_of_weight = check.gg_args.index("--weight")
weight = int(check.gg_args[index_of_weight + 1]) # Updated line
# Remove the hint from gg_args before passing to GatorGrader
check.gg_args = (
check.gg_args[:index_of_weight]
+ check.gg_args[index_of_weight + 2 :]
)
result = _run_shell_check(check)
command_ran = check.command
result.run_command = command_ran
result.weight = weight
# run a check that GatorGrader implements
elif isinstance(check, GatorGraderCheck):
# Weighted Checks
if "--weight" in check.gg_args:
index_of_weight = check.gg_args.index("--weight")
weight = int(check.gg_args[index_of_weight + 1]) # Updated line
# Remove the hint from gg_args before passing to GatorGrader
check.gg_args = (
check.gg_args[:index_of_weight]
+ check.gg_args[index_of_weight + 2 :]
)
result = _run_gg_check(check)
result.weight = weight
# check to see if there was a command in the
# GatorGraderCheck. This code finds the index of the
# word "--command" in the check.gg_args list if it
Expand All @@ -330,17 +364,23 @@ def run_checks(
# there were results from running checks
# and thus they must be displayed
if result is not None:
result.print()
check_weight = int(weight) / total_weight
check_percent = round(check_weight * 100, 2)
result.print(percentage=check_percent)
results.append(result)
# testing printed weights
# print(f"Weight = {weight}")
# total_weight = sum(getattr(result, 'weight', 1) for result in results)
# check_weight = (int(weight) / total_weight)
# print(check_weight * 100)

# determine if there are failures and then display them
failed_results = list(filter(lambda result: not result.passed, results))
# print failures list if there are failures to print
# and print what ShellCheck command that Gatorgrade ran
if len(failed_results) > 0:
print("\n-~- FAILURES -~-\n")
for result in failed_results:
# main.console.print("This is a result")
# main.console.print(result)
result.print(show_diagnostic=True)
# this result is an instance of CheckResult
# that has a run_command field that is some
Expand All @@ -356,11 +396,22 @@ def run_checks(
# determine how many of the checks passed and then
# compute the total percentage of checks passed
passed_count = len(results) - len(failed_results)
# prevent division by zero if no results
# Math to calculate the % score
if len(results) == 0:
total_weight = 0
passed_weight = 0
percent = 0
else:
percent = round(passed_count / len(results) * 100)
total_weight = sum(getattr(result, "weight", 1) for result in results)
passed_weight = sum(
getattr(result, "weight", 1) for result in results if result.passed
)
# prevent division by zero if no results
if total_weight == 0:
percent = 0
else:
percent = round(passed_weight / total_weight * 100)

# if the report is wanted, create output in line with their specifications
if all(report):
report_output_data = create_report_json(passed_count, results, percent)
Expand Down
12 changes: 6 additions & 6 deletions tests/input/test_input_gg_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def test_parse_config_check_gg_matchfilefragment():
# When parse_config is run
output = parse_config(config)
# Then the description, check name, and options appear in the GatorGrader arguments
assert output[0].gg_args == [
assert set(output[0].gg_args) == {
"--description",
"Complete all TODOs",
"MatchFileFragment",
Expand All @@ -37,7 +37,7 @@ def test_parse_config_check_gg_matchfilefragment():
"path/to",
"--file",
"file.py",
]
}


def test_parse_config_gg_check_no_file_context_contains_no_file():
Expand All @@ -49,13 +49,13 @@ def test_parse_config_gg_check_no_file_context_contains_no_file():
# When parse_config is run
output = parse_config(config)
# Then the GatorGrader arguments do not contain a file path
assert output[0].gg_args == [
assert set(output[0].gg_args) == {
"--description",
"Have 8 commits",
"CountCommits",
"--count",
"8",
]
}


def test_parse_config_parses_both_shell_and_gg_checks():
Expand All @@ -76,13 +76,13 @@ def test_parse_config_yml_file_runs_setup_shell_checks():
# When parse_config run
output = parse_config(config)
# Then the output should contain the GatorGrader check
assert output[0].gg_args == [
assert set(output[0].gg_args) == {
"--description",
"Have 8 commits",
"CountCommits",
"--count",
"8",
]
}


def test_parse_config_shell_check_contains_command():
Expand Down
Loading