diff --git a/gatorgrade/input/in_file_path.py b/gatorgrade/input/in_file_path.py index deec75ad..3a622c6f 100644 --- a/gatorgrade/input/in_file_path.py +++ b/gatorgrade/input/in_file_path.py @@ -1,6 +1,8 @@ """Generates a list of commands to be run through gatorgrader.""" from collections import namedtuple +from pathlib import Path +from typing import Any from typing import List import yaml @@ -12,12 +14,26 @@ # which is a file path associated with the check to be used when running the check. CheckData = namedtuple("CheckData", ["file_context", "check"]) +# define the default encoding +DEFAULT_ENCODING = "utf8" -def parse_yaml_file(file_path): + +def parse_yaml_file(file_path: Path) -> List[Any]: """Parse a YAML file and return its contents as a list of dictionaries.""" - with open(file_path, encoding="utf8") as file: - data = yaml.load_all(file, Loader=yaml.FullLoader) - return list(data) + # confirm that the file exists before attempting to read from it + if file_path.exists(): + # read the contents of the specified file using the default + # encoding and then parse that file using the yaml package + with open(file_path, encoding=DEFAULT_ENCODING) as file: + # after parsing with the yaml module, return a list + # of all of the contents specified in the file + data = yaml.load_all(file, Loader=yaml.FullLoader) + return list(data) + # some aspect of the file does not exist + # (i.e., wrong file or wrong directory) + # and thus parsing with YAML is not possible; + # return a blank list that calling function handles + return [] def reformat_yaml_data(data): diff --git a/gatorgrade/input/parse_config.py b/gatorgrade/input/parse_config.py index 4ed2d6a5..d6ac12c7 100644 --- a/gatorgrade/input/parse_config.py +++ b/gatorgrade/input/parse_config.py @@ -1,11 +1,13 @@ """Returns the list of commands to be run through gatorgrader.""" +from pathlib import Path + from gatorgrade.input.command_line_generator import generate_checks from gatorgrade.input.in_file_path import parse_yaml_file from gatorgrade.input.in_file_path import reformat_yaml_data -def parse_config(file): +def parse_config(file: Path): """Parse the input yaml file and generate specified checks. Args: @@ -13,7 +15,18 @@ def parse_config(file): Returns: Returns a dictionary that specifies shell commands and gatorgrade commands """ - parse_con = generate_checks( - reformat_yaml_data(parse_yaml_file(file)) - ) # Call previously generated function to modify file - return parse_con + # parse the YAML file using parse_yaml_file provided by gatorgrade + parsed_yaml_file = parse_yaml_file(file) + # the parsed YAML file contains some contents in a list and thus + # the tool should generate a GatorGrader check for each element in list + if len(parsed_yaml_file) > 0: + # after reformatting the parse YAML file, + # use it to generate all of the checks; + # these will be valid checks that are now + # ready for execution with this tool + parse_con = generate_checks(reformat_yaml_data(parsed_yaml_file)) + return parse_con + # return an empty list because of the fact that the + # parsing process did not return a list with content; + # allow the calling function to handle the empty list + return [] diff --git a/gatorgrade/main.py b/gatorgrade/main.py index 917c3583..6aff2b06 100644 --- a/gatorgrade/main.py +++ b/gatorgrade/main.py @@ -6,13 +6,19 @@ from typing import List import typer +from rich.console import Console from gatorgrade.generate.generate import generate_config from gatorgrade.input.parse_config import parse_config from gatorgrade.output.output import run_checks +# create an app for the Typer-based CLI app = typer.Typer(add_completion=False) +# create a default console for printing with rich +console = Console() + +# define constants used in this module FILE = "gatorgrade.yml" FAILURE = 1 @@ -23,10 +29,28 @@ def gatorgrade( filename: Path = typer.Option(FILE, "--config", "-c", help="Name of the yml file."), ): """Run the GatorGrader checks in the gatorgrade.yml file.""" - # check if ctx.subcommand is none + # if ctx.subcommand is None then this means + # that, by default, gatorgrade should run in checking mode if ctx.invoked_subcommand is None: + # parse the provided configuration file checks = parse_config(filename) - checks_status = run_checks(checks) + # there are valid checks and thus the + # tool should run them with run_checks + if len(checks) > 0: + checks_status = run_checks(checks) + # no checks were created and this means + # that, most likely, the file was not + # valid and thus the tool cannot run checks + else: + checks_status = False + console.print() + console.print(f"The file {filename} either does not exist or is not valid.") + console.print("Exiting now!") + console.print() + # at least one of the checks did not pass or + # the provided file was not valid and thus + # the tool should return a non-zero exit + # code to designate some type of failure if checks_status is not True: sys.exit(FAILURE) diff --git a/poetry.lock b/poetry.lock index b3e502bd..21f37bdd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -800,6 +800,14 @@ typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.9 [package.extras] jupyter = ["ipywidgets (>=7.5.1,<8.0.0)"] +[[package]] +name = "shellingham" +version = "1.5.0" +description = "Tool to Detect Surrounding Shell" +category = "main" +optional = false +python-versions = ">=3.4" + [[package]] name = "six" version = "1.16.0" @@ -872,7 +880,7 @@ python-versions = ">=3.6" [[package]] name = "typer" -version = "0.4.1" +version = "0.6.1" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." category = "main" optional = false @@ -880,12 +888,15 @@ python-versions = ">=3.6" [package.dependencies] click = ">=7.1.1,<9.0.0" +colorama = {version = ">=0.4.3,<0.5.0", optional = true, markers = "extra == \"all\""} +rich = {version = ">=10.11.0,<13.0.0", optional = true, markers = "extra == \"all\""} +shellingham = {version = ">=1.3.0,<2.0.0", optional = true, markers = "extra == \"all\""} [package.extras] -test = ["isort (>=5.0.6,<6.0.0)", "black (>=22.3.0,<23.0.0)", "mypy (==0.910)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "coverage (>=5.2,<6.0)", "pytest-cov (>=2.10.0,<3.0.0)", "pytest (>=4.4.0,<5.4.0)", "shellingham (>=1.3.0,<2.0.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mkdocs (>=1.1.2,<2.0.0)"] -dev = ["flake8 (>=3.8.3,<4.0.0)", "autoflake (>=1.3.1,<2.0.0)"] -all = ["shellingham (>=1.3.0,<2.0.0)", "colorama (>=0.4.3,<0.5.0)"] +all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)", "rich (>=10.11.0,<13.0.0)"] +dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] +doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)"] +test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.910)", "black (>=22.3.0,<23.0.0)", "isort (>=5.0.6,<6.0.0)", "rich (>=10.11.0,<13.0.0)"] [[package]] name = "typing-extensions" @@ -942,7 +953,7 @@ testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest- [metadata] lock-version = "1.1" python-versions = ">=3.7,<4.0" -content-hash = "099453941dc23764e6144c459fe0c1a6901b87b8dffaeafd954d5dbcf87f116c" +content-hash = "b00f295850266d2f98df98eb8f876970d1bc15272f8f844f01abd771a7f7b330" [metadata.files] astroid = [ @@ -1367,6 +1378,10 @@ requests = [ {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"}, ] rich = [] +shellingham = [ + {file = "shellingham-1.5.0-py2.py3-none-any.whl", hash = "sha256:a8f02ba61b69baaa13facdba62908ca8690a94b8119b69f5ec5873ea85f7391b"}, + {file = "shellingham-1.5.0.tar.gz", hash = "sha256:72fb7f5c63103ca2cb91b23dee0c71fe8ad6fbfd46418ef17dbe40db51592dad"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1415,10 +1430,7 @@ typed-ast = [ {file = "typed_ast-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:20d5118e494478ef2d3a2702d964dae830aedd7b4d3b626d003eea526be18718"}, {file = "typed_ast-1.5.3.tar.gz", hash = "sha256:27f25232e2dd0edfe1f019d6bfaaf11e86e657d9bdb7b0956db95f560cceb2b3"}, ] -typer = [ - {file = "typer-0.4.1-py3-none-any.whl", hash = "sha256:e8467f0ebac0c81366c2168d6ad9f888efdfb6d4e1d3d5b4a004f46fa444b5c3"}, - {file = "typer-0.4.1.tar.gz", hash = "sha256:5646aef0d936b2c761a10393f0384ee6b5c7fe0bb3e5cd710b17134ca1d99cff"}, -] +typer = [] typing-extensions = [ {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, diff --git a/pyproject.toml b/pyproject.toml index 9c509f48..b8e88595 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,15 +1,15 @@ [tool.poetry] name = "gatorgrade" -version = "0.2.1" +version = "0.2.2" description = "Python tool to execute GatorGrader" authors = ["Michael Abraham", "Jacob Allebach", "Liam Black", "Katherine Burgess", "Yanqiao Chen", "Ochirsaikhan Davaajambal", "Tuguldurnemekh Gantulga", "Anthony Grant-Cook", "Dylan Holland", "Gregory M. Kapfhammer", "Peyton Kelly", "Luke Lacaria", "Lauren Nevill", "Jack Turner", "Daniel Ullrich", "Garrison Vanzin", "Rian Watson"] [tool.poetry.dependencies] python = ">=3.7,<4.0" -typer = "^0.4.1" PyYAML = "^6.0" gatorgrader = "^1.1.0" rich = "^12.5.1" +typer = {extras = ["all"], version = "^0.6.1"} [tool.poetry.dev-dependencies] taskipy = "^1.10.1" diff --git a/tests/input/test_input_gg_checks.py b/tests/input/test_input_gg_checks.py index baaa02ef..6633fb8a 100644 --- a/tests/input/test_input_gg_checks.py +++ b/tests/input/test_input_gg_checks.py @@ -1,5 +1,7 @@ """Test suite for parse_config function.""" +from pathlib import Path + from gatorgrade.input.checks import GatorGraderCheck from gatorgrade.input.checks import ShellCheck from gatorgrade.input.parse_config import parse_config @@ -8,7 +10,7 @@ def test_parse_config_gg_check_in_file_context_contains_file(): """Test to make sure that the file context is included in the GatorGrader arguments.""" # Given a configuration file with a GatorGrader check within a file context - config = "tests/input/yml_test_files/gatorgrade_one_gg_check_in_file.yml" + config = Path("tests/input/yml_test_files/gatorgrade_one_gg_check_in_file.yml") # When parse_config is run output = parse_config(config) # Then the file path should be in the GatorGrader arguments @@ -18,7 +20,7 @@ def test_parse_config_gg_check_in_file_context_contains_file(): def test_parse_config_check_gg_matchfilefragment(): """Test to make sure the description, check name, and options appear in the GatorGrader arguments.""" # Given a configuration file with a GatorGrader check - config = "tests/input/yml_test_files/gatorgrade_matchfilefragment.yml" + config = Path("tests/input/yml_test_files/gatorgrade_matchfilefragment.yml") # When parse_config is run output = parse_config(config) # Then the description, check name, and options appear in the GatorGrader arguments @@ -41,7 +43,9 @@ def test_parse_config_check_gg_matchfilefragment(): def test_parse_config_gg_check_no_file_context_contains_no_file(): """Test to make sure checks without a file context do not have a file path in GatorGrader arguments.""" # Given a configuration file with a GatorGrader check without a file context - config = "tests/input/yml_test_files/gatorgrade_one_gg_check_no_file_context.yml" + config = Path( + "tests/input/yml_test_files/gatorgrade_one_gg_check_no_file_context.yml" + ) # When parse_config is run output = parse_config(config) # Then the GatorGrader arguments do not contain a file path @@ -57,7 +61,7 @@ def test_parse_config_gg_check_no_file_context_contains_no_file(): def test_parse_config_parses_both_shell_and_gg_checks(): """Test to make sure that both shell and GatorGrader checks are parsed.""" # Given a configuration file that contains a shell check and GatorGrader check - config = "tests/input/yml_test_files/gatorgrader_both_checks.yml" + config = Path("tests/input/yml_test_files/gatorgrader_both_checks.yml") # When parse_config is run output = parse_config(config) # Then the output should contain a shell check and GatorGrader check @@ -68,7 +72,7 @@ def test_parse_config_parses_both_shell_and_gg_checks(): def test_parse_config_yml_file_runs_setup_shell_checks(): """Test to make sure that a configuration file without setup commands can be parsed.""" # Given a configuration file without setup commands - config = "tests/input/yml_test_files/gatorgrade_no_shell_setup_check.yml" + config = Path("tests/input/yml_test_files/gatorgrade_no_shell_setup_check.yml") # When parse_config run output = parse_config(config) # Then the output should contain the GatorGrader check @@ -84,7 +88,7 @@ def test_parse_config_yml_file_runs_setup_shell_checks(): def test_parse_config_shell_check_contains_command(): """Test to make sure that the command for a shell check is stored.""" # Given a configuration file with a shell check - config = "tests/input/yml_test_files/gatorgrade_one_shell_command_check.yml" + config = Path("tests/input/yml_test_files/gatorgrade_one_shell_command_check.yml") # When the parse_config is run output = parse_config(config) # Then the command should be stored in the shell check