From 915acfb2b2ecb0a7b93e333a2711f84279dd928d Mon Sep 17 00:00:00 2001 From: Andrew DeOrio Date: Tue, 21 Jul 2026 08:51:26 -0400 Subject: [PATCH 1/3] Initial conversion to ruff --- .pylintrc | 13 - AGENTS.md | 11 +- CONTRIBUTING.md | 5 +- MANIFEST.in | 1 - agiocli/__main__.py | 95 +++--- agiocli/api_client.py | 26 +- agiocli/utils.py | 158 ++++------ pyproject.toml | 15 +- tests/conftest.py | 629 +++++++++++++++++++------------------- tests/test_cli.py | 2 + tests/test_courses.py | 32 +- tests/test_groups.py | 55 ++-- tests/test_matching.py | 128 +++++--- tests/test_projects.py | 50 +-- tests/test_submissions.py | 65 ++-- tests/utils.py | 4 +- tox.ini | 7 +- 17 files changed, 657 insertions(+), 639 deletions(-) delete mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 24b06a3..0000000 --- a/.pylintrc +++ /dev/null @@ -1,13 +0,0 @@ -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=10 - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=yes diff --git a/AGENTS.md b/AGENTS.md index d4a90ae..4d82d10 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,11 +23,10 @@ $ pytest --cov ./agiocli --cov-report term-missing $ pytest tests/test_courses.py::test_courses_pk ``` -Lint (all four must pass; this is what CI runs): +Lint (all three must pass; this is what CI runs): ```console -$ pycodestyle agiocli tests -$ pydocstyle agiocli tests -$ pylint agiocli tests +$ ruff check agiocli tests +$ ruff format --check agiocli tests $ check-manifest ``` @@ -36,8 +35,6 @@ Run the full lint + test suite in a clean throwaway virtualenv (mirrors CI exact $ tox -e py3 ``` -Note: pydocstyle cannot glob `tests/`, so tox invokes it as `sh -c "pydocstyle agiocli tests/*"`. When running pydocstyle manually against tests, match that pattern. - ## Architecture Three modules under `agiocli/`, layered: @@ -68,7 +65,7 @@ Tests are system tests driven through Click's `CliRunner` (`runner.invoke(main, - Errors surfaced to the user are reported via `sys.exit("Error: ...")`, not exceptions, except `TokenFileNotFound` and `UnsupportedAssignmentError` which are caught by callers. - Click docstrings use `\b` to prevent paragraph rewrapping; lines with `\b` carry a `# noqa: D301`. -- Subcommands with many params carry `# pylint: disable=too-many-arguments` because each CLI option needs a function parameter. +- Subcommands with many params carry `# noqa: PLR0913` because each CLI option needs a function parameter. - The version string lives in `pyproject.toml` (`version =`); bumping it is a manual step in the release procedure. ## Release diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 805fb55..e5e6af2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,9 +29,8 @@ $ pytest --cov ./agiocli --cov-report term-missing Test code style ```console -$ pycodestyle agiocli tests -$ pydocstyle agiocli tests -$ pylint agiocli tests +$ ruff check agiocli tests +$ ruff format --check agiocli tests $ check-manifest ``` diff --git a/MANIFEST.in b/MANIFEST.in index 304e715..b71f3ce 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,7 +1,6 @@ include LICENSE include MANIFEST.in include *.md -include .pylintrc graft tests # Avoid dev and and binary files diff --git a/agiocli/__main__.py b/agiocli/__main__.py index d7025b5..b2323f8 100644 --- a/agiocli/__main__.py +++ b/agiocli/__main__.py @@ -3,8 +3,11 @@ Andrew DeOrio """ + import sys + import click + from agiocli import APIClient, TokenFileNotFound, utils @@ -34,14 +37,13 @@ def login(ctx): @main.command() @click.argument("course_arg", required=False) -@click.option("-l", "--list", "show_list", is_flag=True, - help="List courses and exit.") +@click.option("-l", "--list", "show_list", is_flag=True, help="List courses and exit.") @click.option("-w", "--web", is_flag=True, help="Open course in browser.") @click.pass_context # The \b character in the docstring prevents Click from rewraping a paragraph. -# We need to tell pycodestyle to ignore it. +# We need to tell ruff to ignore it (D301). # https://click.palletsprojects.com/en/8.0.x/documentation/#preventing-rewrapping -def courses(ctx, course_arg, show_list, web): # noqa: D301 +def courses(ctx, course_arg, show_list, web): """Show course detail or list courses. COURSE_ARG is a primary key, name, or shorthand. @@ -54,7 +56,7 @@ def courses(ctx, course_arg, show_list, web): # noqa: D301 agio courses eecs485sp21 agio courses eecs485[cur|current] - """ + """ # noqa: D301 try: client = APIClient.make_default(debug=ctx.obj["DEBUG"]) except TokenFileNotFound as err: @@ -77,17 +79,15 @@ def courses(ctx, course_arg, show_list, web): # noqa: D301 @main.command() @click.argument("project_arg", required=False) -@click.option("-c", "--course", "course_arg", - help="Course pk, name, or shorthand.") -@click.option("-l", "--list", "show_list", is_flag=True, - help="List projects and exit.") +@click.option("-c", "--course", "course_arg", help="Course pk, name, or shorthand.") +@click.option("-l", "--list", "show_list", is_flag=True, help="List projects and exit.") @click.option("-w", "--web", is_flag=True, help="Open project in browser.") @click.option("--config", is_flag=True, help="Get test suite config.") @click.pass_context # The \b character in the docstring prevents Click from rewraping a paragraph. -# We need to tell pycodestyle to ignore it. +# We need to tell ruff to ignore it (D301). # https://click.palletsprojects.com/en/8.0.x/documentation/#preventing-rewrapping -def projects(ctx, project_arg, course_arg, show_list, web, config): # noqa: D301 +def projects(ctx, project_arg, course_arg, show_list, web, config): # noqa: PLR0913 """Show project detail or list projects. PROJECT_ARG is a primary key, name, or shorthand. @@ -102,8 +102,7 @@ def projects(ctx, project_arg, course_arg, show_list, web, config): # noqa: D30 agio projects p1 agio projects --course eecs485sp21 p1 --config - """ - # pylint: disable=too-many-arguments,too-many-positional-arguments + """ # noqa: D301 try: client = APIClient.make_default(debug=ctx.obj["DEBUG"]) except TokenFileNotFound as err: @@ -122,9 +121,7 @@ def projects(ctx, project_arg, course_arg, show_list, web, config): # noqa: D30 # Print test suite config if --config flag if config: - config_json = client.get( - f"/api/projects/{project['pk']}/ag_test_suites/" - ) + config_json = client.get(f"/api/projects/{project['pk']}/ag_test_suites/") print(utils.dict_str(config_json)) return @@ -139,20 +136,22 @@ def projects(ctx, project_arg, course_arg, show_list, web, config): # noqa: D30 @main.command() @click.argument("group_arg", required=False) -@click.option("-c", "--course", "course_arg", - help="Course pk, name, or shorthand.") -@click.option("-p", "--project", "project_arg", - help="Project pk, name, or shorthand.") -@click.option("-l", "--list", "show_list", is_flag=True, - help="List groups and exit.") -@click.option("-j", "--list-json", "list_json", is_flag=True, - help="List groups in JSON format (2D array) and exit.") +@click.option("-c", "--course", "course_arg", help="Course pk, name, or shorthand.") +@click.option("-p", "--project", "project_arg", help="Project pk, name, or shorthand.") +@click.option("-l", "--list", "show_list", is_flag=True, help="List groups and exit.") +@click.option( + "-j", + "--list-json", + "list_json", + is_flag=True, + help="List groups in JSON format (2D array) and exit.", +) @click.option("-w", "--web", is_flag=True, help="Open group in browser.") @click.pass_context # The \b character in the docstring prevents Click from rewraping a paragraph. -# We need to tell pycodestyle to ignore it. +# We need to tell ruff to ignore it (D301). # https://click.palletsprojects.com/en/8.0.x/documentation/#preventing-rewrapping -def groups(ctx, group_arg, project_arg, course_arg, show_list, list_json, web): # noqa: D301 +def groups(ctx, group_arg, project_arg, course_arg, show_list, list_json, web): # noqa: PLR0913 """Show group detail or list groups. GROUP_ARG is a primary key, name, or member uniqname. @@ -167,10 +166,7 @@ def groups(ctx, group_arg, project_arg, course_arg, show_list, list_json, web): agio groups awdeorio --project 1005 agio groups awdeorio --course eecs485sp21 --project p1 - """ - # We must have an function argument for each CLI argument or option - # pylint: disable=too-many-arguments,too-many-positional-arguments - + """ # noqa: D301 try: client = APIClient.make_default(debug=ctx.obj["DEBUG"]) except TokenFileNotFound as err: @@ -207,22 +203,24 @@ def groups(ctx, group_arg, project_arg, course_arg, show_list, list_json, web): @main.command() @click.argument("submission_arg", required=False) -@click.option("-c", "--course", "course_arg", - help="Course pk, name, or shorthand.") -@click.option("-p", "--project", "project_arg", - help="Project pk, name, or shorthand.") -@click.option("-g", "--group", "group_arg", - help="Group pk or member uniqname.") -@click.option("-l", "--list", "show_list", is_flag=True, - help="List groups and exit.") -@click.option("-d", "--download", is_flag=True, - help="Download submission files.") +@click.option("-c", "--course", "course_arg", help="Course pk, name, or shorthand.") +@click.option("-p", "--project", "project_arg", help="Project pk, name, or shorthand.") +@click.option("-g", "--group", "group_arg", help="Group pk or member uniqname.") +@click.option("-l", "--list", "show_list", is_flag=True, help="List groups and exit.") +@click.option("-d", "--download", is_flag=True, help="Download submission files.") @click.pass_context # The \b character in the docstring prevents Click from rewraping a paragraph. -# We need to tell pycodestyle to ignore it. +# We need to tell ruff to ignore it (D301). # https://click.palletsprojects.com/en/8.0.x/documentation/#preventing-rewrapping -def submissions(ctx, submission_arg, group_arg, - project_arg, course_arg, show_list, download): # noqa: D301 +def submissions( # noqa: PLR0913 + ctx, + submission_arg, + group_arg, + project_arg, + course_arg, + show_list, + download, +): """Show submission detail or list submissions. SUBMISSION_ARG is a primary key, 'best', or 'last' @@ -236,10 +234,7 @@ def submissions(ctx, submission_arg, group_arg, agio submissions [...] best agio submissions [...] last agio submissions [...] --download - """ - # We must have an function argument for each CLI argument or option - # pylint: disable=too-many-arguments,too-many-positional-arguments - + """ # noqa: D301 try: client = APIClient.make_default(debug=ctx.obj["DEBUG"]) except TokenFileNotFound as err: @@ -247,9 +242,7 @@ def submissions(ctx, submission_arg, group_arg, # Handle --list: list submissions and exit if show_list: - group = utils.get_group_smart( - group_arg, project_arg, course_arg, client - ) + group = utils.get_group_smart(group_arg, project_arg, course_arg, client) submission_list = utils.get_submission_list(group, client) for i in submission_list: print(utils.submission_str(i)) @@ -270,6 +263,4 @@ def submissions(ctx, submission_arg, group_arg, if __name__ == "__main__": - # These errors are endemic to click - # pylint: disable=no-value-for-parameter,unexpected-keyword-arg main(obj={}) diff --git a/agiocli/api_client.py b/agiocli/api_client.py index b296ed7..eb884c4 100644 --- a/agiocli/api_client.py +++ b/agiocli/api_client.py @@ -4,12 +4,14 @@ Based on HTTPClient by James Perretta https://github.com/eecs-autograder/autograder-contrib/ """ + import copy -import os import json +import os import sys -from typing import Iterator +from collections.abc import Iterator from urllib.parse import urljoin + import requests @@ -28,9 +30,7 @@ class APIClient: @staticmethod def make_default( - token_filename='.agtoken', - base_url='https://autograder.io/', - debug=False + token_filename=".agtoken", base_url="https://autograder.io/", debug=False ): """Create an APIClient instance with API token found in token_filename. @@ -69,7 +69,7 @@ def get_paginated(self, path, *args, **kwargs): assert "results" in response.json() assert "next" in response.json() yield from response.json()["results"] - page_url = response.json()['next'] + page_url = response.json()["next"] def post(self, path, *args, **kwargs): """Call requests.post with authentication headers and base URL.""" @@ -105,8 +105,8 @@ def do_request(self, method_func, path, *args, **kwargs): print(f"{method} {url}") # Call the underlying requests library function - headers = copy.deepcopy(kwargs.pop('headers', {})) - headers['Authorization'] = f'Token {self.api_token}' + headers = copy.deepcopy(kwargs.pop("headers", {})) + headers["Authorization"] = f"Token {self.api_token}" response = method_func(url, *args, headers=headers, **kwargs) # Print the response @@ -123,7 +123,7 @@ def do_request(self, method_func, path, *args, **kwargs): # Decode JSON if "Content-Type" not in response.headers: sys.exit(f"Error: no Content-Type from: {response.url}") - if 'application/json' in response.headers['Content-Type']: + if "application/json" in response.headers["Content-Type"]: try: return response.json() except json.JSONDecodeError: @@ -131,7 +131,7 @@ def do_request(self, method_func, path, *args, **kwargs): f"Error: JSON decoding failed for url {response.url}\n" f"{response.text}" ) - elif 'application/octet-stream' in response.headers['Content-Type']: + elif "application/octet-stream" in response.headers["Content-Type"]: return response.content else: sys.exit( @@ -156,7 +156,7 @@ def get_api_token(token_filename: str) -> str: # Make sure that we're starting in a subdir of the home directory curdir = os.path.abspath(os.curdir) - if os.path.expanduser('~') not in curdir: + if os.path.expanduser("~") not in curdir: raise TokenFileNotFound(f"Invalid search path: {curdir}") # Search, walking up the directory structure from PWD to home @@ -177,7 +177,7 @@ def get_api_token(token_filename: str) -> str: def walk_up_to_home_dir() -> Iterator[str]: """Iterate up the directory structure from pwd to home directory.""" current_dir = os.path.abspath(os.curdir) - home_dir = os.path.expanduser('~') + home_dir = os.path.expanduser("~") while current_dir != home_dir: yield current_dir @@ -197,5 +197,5 @@ def print_response(response): print(formatted) -class TokenFileNotFound(Exception): +class TokenFileNotFound(Exception): # noqa: N818 -- public API, don't rename """Exception type indicating failure to locate user token file.""" diff --git a/agiocli/utils.py b/agiocli/utils.py index 1636602..1e41a16 100644 --- a/agiocli/utils.py +++ b/agiocli/utils.py @@ -1,15 +1,18 @@ """Common utility functions.""" + import datetime as dt +import itertools import json import pathlib -import sys import platform +import re import subprocess +import sys import webbrowser -import itertools -import re + import dateutil.parser import pick + try: import gnureadline as readline except ImportError: @@ -21,10 +24,18 @@ # Map month number to semester number MONTH_SEMESTER_NUM = { - 1: 1, 2: 1, 3: 1, 4: 1, # Jan-Apr Winter - 5: 2, 6: 2, # May-Jun Spring - 7: 3, 8: 3, # Jul-Aug Summer - 9: 4, 10: 4, 11: 4, 12: 4, # Sep-Dec Fall + 1: 1, + 2: 1, + 3: 1, + 4: 1, # Jan-Apr Winter + 5: 2, + 6: 2, # May-Jun Spring + 7: 3, + 8: 3, # Jul-Aug Summer + 9: 4, + 10: 4, + 11: 4, + 12: 4, # Sep-Dec Fall } # Map month number to semester name @@ -43,6 +54,9 @@ 12: "Fall", } +# A year below this is assumed to be a 2-digit year, e.g. 21 -> 2021 +TWO_DIGIT_YEAR_CUTOFF = 100 + def dict_str(obj): """Format a dictionary as an indented string.""" @@ -51,25 +65,16 @@ def dict_str(obj): def course_str(course): """Format course as a string.""" - return ( - f"[{course['pk']}] {course['name']} " - f"{course['semester']} {course['year']}" - ) + return f"[{course['pk']}] {course['name']} {course['semester']} {course['year']}" def course_key(course): """Return a tuple for sorting courses by year, semester, and name.""" # Coerce year - if course["year"] is None: - year = 0 - else: - year = course["year"] + year = 0 if course["year"] is None else course["year"] # Convert semester to a number - if course["semester"] is None: - semester_num = 0 - else: - semester_num = SEMESTER_NUM[course["semester"]] + semester_num = 0 if course["semester"] is None else SEMESTER_NUM[course["semester"]] # Coerce name name = course["name"] @@ -83,34 +88,22 @@ def course_key(course): def is_current_course(course): """Return True if course is from current or future semester.""" # Coerce year - if course["year"] is None: - year = 0 - else: - year = course["year"] + year = 0 if course["year"] is None else course["year"] # Convert semester to a number - if course["semester"] is None: - semester_num = 0 - else: - semester_num = SEMESTER_NUM[course["semester"]] + semester_num = 0 if course["semester"] is None else SEMESTER_NUM[course["semester"]] # Compare course year and semester to today today = dt.date.today() - return ( - year >= today.year and - semester_num >= MONTH_SEMESTER_NUM[today.month] - ) + return year >= today.year and semester_num >= MONTH_SEMESTER_NUM[today.month] def course_match(search, courses): """Return courses matching search term.""" year, semester, name = parse_course_string(search) courses = filter( - lambda x: - x["year"] == year and - x["semester"] == semester and - name in x["name"], - courses + lambda x: x["year"] == year and x["semester"] == semester and name in x["name"], + courses, ) return list(courses) @@ -161,7 +154,7 @@ def parse_course_string(user_input): # Convert year to a number, handling 2-digit year as "20xx" year = int(year) assert year >= 0 - if year < 100: + if year < TWO_DIGIT_YEAR_CUTOFF: year = 2000 + year # Convert semester abbreviation to semester name. Make sure that the keys @@ -206,8 +199,7 @@ def get_current_course_list(client): courses = client.get(f"/api/users/{user['pk']}/courses_is_admin_for/") courses += client.get(f"/api/users/{user['pk']}/courses_is_staff_for/") courses = sorted(courses, key=course_key, reverse=True) - courses = [k for k, v in itertools.groupby(courses)] # Unique - return courses + return [k for k, v in itertools.groupby(courses)] # Unique def get_course_smart(course_arg, client): @@ -245,16 +237,10 @@ def get_course_smart(course_arg, client): matches = course_match(course_arg, courses) if not matches: courses_str = "\n".join(course_str(i) for i in courses) - sys.exit( - f"Error: no course matches '{course_arg}'\n" - f"{courses_str}" - ) + sys.exit(f"Error: no course matches '{course_arg}'\n{courses_str}") elif len(matches) > 1: matches_str = "\n".join(course_str(i) for i in matches) - sys.exit( - f"Error: multiple courses match '{course_arg}'\n" - f"{matches_str}" - ) + sys.exit(f"Error: multiple courses match '{course_arg}'\n{matches_str}") return matches[0] @@ -287,8 +273,11 @@ def parse_project_string(user_input): # HACK: If the input is just a string, e.g., "images", then it will be # mis-classified as asstype. - if (match.group("asstype") and not match.group("num") - and not match.group("subtitle")): + if ( + match.group("asstype") + and not match.group("num") + and not match.group("subtitle") + ): subtitle = match.group("asstype") return "", 0, subtitle @@ -342,31 +331,24 @@ def project_match(search, projects): asstype, num, subtitle = parse_project_string(search) # Filter for only parsable projects - projects = filter( - lambda x: parse_project_string_skipper(x["name"]), projects - ) + projects = filter(lambda x: parse_project_string_skipper(x["name"]), projects) # Remove projects with an assignment type mismatch (Lab vs. Project, etc.) if asstype: projects = filter( - lambda x: - parse_project_string(x["name"])[0].lower() == asstype.lower(), - projects + lambda x: parse_project_string(x["name"])[0].lower() == asstype.lower(), + projects, ) # Remove projects with a number mismatch if num: - projects = filter( - lambda x: parse_project_string(x["name"])[1] == num, - projects - ) + projects = filter(lambda x: parse_project_string(x["name"])[1] == num, projects) # Remove projects with a name mismatch, tolerating substring match if subtitle: projects = filter( - lambda x: - subtitle.lower() in parse_project_string(x["name"])[2].lower(), - projects + lambda x: subtitle.lower() in parse_project_string(x["name"])[2].lower(), + projects, ) return list(projects) @@ -375,8 +357,7 @@ def project_match(search, projects): def get_course_project_list(course, client): """Return a sorted list of projects for course.""" projects = client.get(f"/api/courses/{course['pk']}/projects/") - projects = sorted(projects, key=lambda x: x["name"]) - return projects + return sorted(projects, key=lambda x: x["name"]) def get_project_smart(project_arg, course_arg, client): @@ -415,16 +396,10 @@ def get_project_smart(project_arg, course_arg, client): matches = project_match(project_arg, projects) if not matches: projects_str = "\n".join(project_str(i) for i in projects) - sys.exit( - f"Error: no project matches '{project_arg}'\n" - f"{projects_str}" - ) + sys.exit(f"Error: no project matches '{project_arg}'\n{projects_str}") elif len(matches) > 1: matches_str = "\n".join(project_str(i) for i in matches) - sys.exit( - f"Error: multiple projects match '{project_arg}'\n" - f"{matches_str}" - ) + sys.exit(f"Error: multiple projects match '{project_arg}'\n{matches_str}") return matches[0] @@ -460,8 +435,7 @@ def group_match(uniqname, groups): def get_group_list(project, client): """Return a sorted list of groups for project.""" groups = client.get(f"/api/projects/{project['pk']}/groups/") - groups = sorted(groups, key=lambda x: x["pk"]) - return groups + return sorted(groups, key=lambda x: x["pk"]) def get_group_smart(group_arg, project_arg, course_arg, client): @@ -503,10 +477,11 @@ def uniqname_completer(text, state): if state < len(options): return options[state] return None + readline.set_completer(uniqname_completer) # Use the tab key for completion - readline.parse_and_bind('tab: complete') + readline.parse_and_bind("tab: complete") # Prompt the user to select a uniqname while True: @@ -522,16 +497,13 @@ def uniqname_completer(text, state): sys.exit(f"Error: uniqname not in any group: {group_arg}") elif len(matches) > 1: matches_str = "\n".join(group_str(i) for i in matches) - sys.exit( - f"Error: uniqname in more than one group: {group_arg}" - f"{matches_str}" - ) + sys.exit(f"Error: uniqname in more than one group: {group_arg}{matches_str}") return matches[0] def is_wsl(): """Check if user is running WSL.""" - return 'microsoft' in platform.uname().release + return "microsoft" in platform.uname().release def open_web(url): @@ -539,11 +511,12 @@ def open_web(url): if is_wsl(): # Need to escape & in Windows # https://stackoverflow.com/questions/1327431/how-do-i-escape-ampersands-in-batch-files - url = url.replace('&', '^&') + url = url.replace("&", "^&") subprocess.run( - ['cmd.exe', '/c', 'start', url], - stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - check=True + ["cmd.exe", "/c", "start", url], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=True, ) else: webbrowser.open(url) @@ -558,20 +531,16 @@ def submission_str(submission): """Format submission as a string.""" timestamp = dateutil.parser.parse(submission["timestamp"]) timestamp_human = timestamp.strftime("%Y-%m-%d %H:%M:%S") - return ( - f"[{submission['pk']}] {timestamp_human} " - ) + return f"[{submission['pk']}] {timestamp_human} " def get_submission_list(group, client): """Return a sorted list of submissions for a group.""" submissions = client.get(f"/api/groups/{group['pk']}/submissions/") - submissions = sorted(submissions, key=submission_key, reverse=True) - return submissions + return sorted(submissions, key=submission_key, reverse=True) -def get_submission_smart( - submission_arg, group_arg, project_arg, course_arg, client): +def get_submission_smart(submission_arg, group_arg, project_arg, course_arg, client): """Interact with the user to select a submission. 1. If submission_arg is a number, look up submission by primary key @@ -641,11 +610,10 @@ def download_submission(submission, group_arg, client): # Download file to PWD. If there are multiple files, put them in a new # directory. - filenames = submission['submitted_filenames'] + filenames = submission["submitted_filenames"] if not filenames: sys.exit( - "Error: no files to download for submission" - f"{submission_str(submission)}" + f"Error: no files to download for submission{submission_str(submission)}" ) elif len(filenames) == 1: filename = filenames[0] @@ -657,7 +625,7 @@ def download_submission(submission, group_arg, client): sys.exit(f"Error: refuse to clobber directory: {dirname}") dirname.mkdir() for filename in filenames: - download_file(filename, submission, dirname/filename, client) + download_file(filename, submission, dirname / filename, client) def download_file(filename, submission, target, client): diff --git a/pyproject.toml b/pyproject.toml index 812fc2c..51cb7a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,13 +35,11 @@ dev = [ "check-manifest", "freezegun", "pdbpp", - "pycodestyle", - "pydocstyle", - "pylint", "pytest", "pytest-cov", "pytest-mock", "requests-mock", + "ruff", "tox", "twine", ] @@ -49,3 +47,14 @@ dev = [ [tool.setuptools.packages.find] where = ["."] include = ["agiocli*"] + +[tool.ruff.lint] +select = ["E", "W", "F", "D", "I", "UP", "N", "PT", "B", "SIM", "PL", "RET", "C4", "ARG"] +ignore = ["D203", "D213", "W191", "E111", "E114", "E117", "D206", "D300"] + +[tool.ruff.lint.pydocstyle] +convention = "pep257" + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401", "F403"] +"test_*.py" = ["PLR2004", "ARG001"] diff --git a/tests/conftest.py b/tests/conftest.py index 72198e2..6fe71f0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,5 +1,7 @@ """Shared test fixtures.""" + import json + import pytest import utils @@ -16,7 +18,7 @@ def constants_setup(): "subtitle": "Web Systems", "num_late_days": 0, "allowed_guest_domain": "@umich.edu", - "last_modified": "2021-04-07T02:19:22.818992Z" + "last_modified": "2021-04-07T02:19:22.818992Z", }, "PROJECT_1005": { "pk": 1005, @@ -47,15 +49,15 @@ def constants_setup(): "project": 1005, "name": "autograder-6f82202d.tar.gz", "last_modified": "2021-04-07T02:16:02.992523Z", - "size": 11323 + "size": 11323, }, { "pk": 21909, "project": 1005, "name": "install", "last_modified": "2021-04-07T02:16:03.147228Z", - "size": 2230 - } + "size": 2230, + }, ], "expected_student_files": [ { @@ -64,7 +66,7 @@ def constants_setup(): "pattern": "submit.tar.gz", "min_num_matches": 1, "max_num_matches": 1, - "last_modified": "2021-04-07T00:31:21.379258Z" + "last_modified": "2021-04-07T00:31:21.379258Z", } ], "has_handgrading_rubric": False, @@ -74,15 +76,13 @@ def constants_setup(): "honor_pledge_text": ( "I have neither given nor received aid on this project, " "nor have I concealed any violations of the Honor Code." - ) + ), }, "GROUP_246965": { "pk": 246965, "project": 1005, "extended_due_date": None, - "member_names": [ - "awdeorio@umich.edu" - ], + "member_names": ["awdeorio@umich.edu"], "members": [ { "pk": 5, @@ -90,7 +90,7 @@ def constants_setup(): "first_name": "Andrew", "last_name": "DeOrio", "email": "", - "is_superuser": False + "is_superuser": False, } ], "bonus_submissions_remaining": 0, @@ -98,16 +98,14 @@ def constants_setup(): "num_submissions": 2, "num_submits_towards_limit": 1, "created_at": "2021-04-21T17:01:37.807261Z", - "last_modified": "2021-04-21T17:01:37.807025Z" + "last_modified": "2021-04-21T17:01:37.807025Z", }, "SUBMISSION_1128572": { "pk": 1128572, "group": 246965, "timestamp": "2021-06-29T14:55:57.886137Z", "submitter": "awdeorio@umich.edu", - "submitted_filenames": [ - "submit.tar.gz" - ], + "submitted_filenames": ["submit.tar.gz"], "discarded_files": [], "missing_files": {}, "status": "finished_grading", @@ -118,16 +116,14 @@ def constants_setup(): "position_in_queue": 0, "grading_start_time": "2021-06-29T14:56:04.154353Z", "non_deferred_grading_end_time": "2021-06-29T14:56:56.289720Z", - "last_modified": "2021-06-29T14:56:55.378590Z" + "last_modified": "2021-06-29T14:56:55.378590Z", }, "SUBMISSION_1125717": { "pk": 1125717, "group": 246965, "timestamp": "2021-06-09T12:49:16.047791Z", "submitter": "awdeorio@umich.edu", - "submitted_filenames": [ - "submit.tar.gz" - ], + "submitted_filenames": ["submit.tar.gz"], "discarded_files": [], "missing_files": {}, "status": "finished_grading", @@ -138,7 +134,7 @@ def constants_setup(): "position_in_queue": 0, "grading_start_time": "2021-06-09T12:49:21.340598Z", "non_deferred_grading_end_time": "2021-06-09T12:50:26.978440Z", - "last_modified": "2021-06-09T12:50:25.899462Z" + "last_modified": "2021-06-09T12:50:25.899462Z", }, } @@ -153,61 +149,67 @@ def api_requests_mock(requests_mock, mocker, constants): requests_mock.get( "https://autograder.io/api/users/current/", headers={"Content-Type": "application/json"}, - text=json.dumps({ - "pk": 5, - "username": "awdeorio@umich.edu", - "first_name": "Andrew", - "last_name": "DeOrio", - "email": "", - "is_superuser": False - }) + text=json.dumps( + { + "pk": 5, + "username": "awdeorio@umich.edu", + "first_name": "Andrew", + "last_name": "DeOrio", + "email": "", + "is_superuser": False, + } + ), ) # Course list / admin requests_mock.get( "https://autograder.io/api/users/5/courses_is_admin_for/", headers={"Content-Type": "application/json"}, - text=json.dumps([ - { - "pk": 111, - "name": "EECS 280", - "semester": "Spring", - "year": 2021, - "subtitle": "", - "num_late_days": 0, - "allowed_guest_domain": "@umich.edu", - "last_modified": "2021-05-03T01:12:28.049482Z" - }, - constants["COURSE_109"], - { - "pk": 129, - "name": "EECS 485", - "semester": "Fall", - "year": 2021, - "subtitle": "Web Systems", - "num_late_days": 0, - "allowed_guest_domain": "@umich.edu", - "last_modified": "2021-06-23T13:54:07.942973Z" - } - ]) + text=json.dumps( + [ + { + "pk": 111, + "name": "EECS 280", + "semester": "Spring", + "year": 2021, + "subtitle": "", + "num_late_days": 0, + "allowed_guest_domain": "@umich.edu", + "last_modified": "2021-05-03T01:12:28.049482Z", + }, + constants["COURSE_109"], + { + "pk": 129, + "name": "EECS 485", + "semester": "Fall", + "year": 2021, + "subtitle": "Web Systems", + "num_late_days": 0, + "allowed_guest_domain": "@umich.edu", + "last_modified": "2021-06-23T13:54:07.942973Z", + }, + ] + ), ) # Course list / staff requests_mock.get( "https://autograder.io/api/users/5/courses_is_staff_for/", headers={"Content-Type": "application/json"}, - text=json.dumps([ - { - "pk": 1, - "name": "EECS 280", - "semester": "Fall", - "year": 2016, - "subtitle": "", - "num_late_days": 0, - "allowed_guest_domain": "@umich.edu", - "last_modified": "2019-02-07T21:06:01.779838Z" - }, - ]) + text=json.dumps( + [ + { + "pk": 1, + "name": "EECS 280", + "semester": "Fall", + "year": 2016, + "subtitle": "", + "num_late_days": 0, + "allowed_guest_domain": "@umich.edu", + "last_modified": "2019-02-07T21:06:01.779838Z", + }, + ] + ), ) # Course detail @@ -221,241 +223,243 @@ def api_requests_mock(requests_mock, mocker, constants): requests_mock.get( "https://autograder.io/api/courses/109/projects/", headers={"Content-Type": "application/json"}, - text=json.dumps([ - { - "pk": 1007, - "name": "Project 5 - Search Engine", - "last_modified": "2021-06-21T16:42:49.476960Z", - "course": 109, - "visible_to_students": True, - "closing_time": "2021-06-21T04:30:00Z", - "soft_closing_time": "2021-06-21T03:59:00Z", - "disallow_student_submissions": False, - "disallow_group_registration": False, - "guests_can_submit": False, - "min_group_size": 1, - "max_group_size": 3, - "submission_limit_per_day": 3, - "allow_submissions_past_limit": False, - "groups_combine_daily_submissions": False, - "submission_limit_reset_time": "00:00:00", - "submission_limit_reset_timezone": "US/Eastern", - "num_bonus_submissions": 0, - "total_submission_limit": None, - "allow_late_days": False, - "ultimate_submission_policy": "best", - "hide_ultimate_submission_fdbk": False, - "instructor_files": [ - { - "pk": 24749, - "project": 1007, - "name": "autograder-4d215e29.tar.xz", - "last_modified": "2021-06-14T03:39:17.838619Z", - "size": 4306452 - }, - { - "pk": 24750, - "project": 1007, - "name": "install", - "last_modified": "2021-06-14T03:39:18.125266Z", - "size": 2906 - } - ], - "expected_student_files": [ - { - "pk": 2547, - "project": 1007, - "pattern": "submit.tar.xz", - "min_num_matches": 1, - "max_num_matches": 1, - "last_modified": "2021-04-07T00:31:28.029083Z" - } - ], - "has_handgrading_rubric": False, - "send_email_on_submission_received": True, - "send_email_on_non_deferred_tests_finished": True, - "use_honor_pledge": True, - "honor_pledge_text": ( - "I have neither given nor received aid on this project, " - "nor have I concealed any violations of the Honor Code." - ) - }, - { - "pk": 1008, - "name": "Project 3 - Client-side Dynamic Pages", - "last_modified": "2021-05-27T22:01:15.359868Z", - "course": 109, - "visible_to_students": True, - "closing_time": "2021-05-26T04:30:00Z", - "soft_closing_time": "2021-05-26T03:59:00Z", - "disallow_student_submissions": False, - "disallow_group_registration": False, - "guests_can_submit": False, - "min_group_size": 1, - "max_group_size": 3, - "submission_limit_per_day": 3, - "allow_submissions_past_limit": False, - "groups_combine_daily_submissions": False, - "submission_limit_reset_time": "00:00:00", - "submission_limit_reset_timezone": "US/Eastern", - "num_bonus_submissions": 0, - "total_submission_limit": None, - "allow_late_days": False, - "ultimate_submission_policy": "best", - "hide_ultimate_submission_fdbk": False, - "instructor_files": [ - { - "pk": 22508, - "project": 1008, - "name": "autograder-bcc78f56-dirty.tar.gz", - "last_modified": "2021-05-18T22:00:36.641145Z", - "size": 168836 - }, - { - "pk": 21949, - "project": 1008, - "name": "install", - "last_modified": "2021-05-18T22:00:36.805560Z", - "size": 3057 - } - ], - "expected_student_files": [ - { - "pk": 2548, - "project": 1008, - "pattern": "submit.tar.gz", - "min_num_matches": 1, - "max_num_matches": 1, - "last_modified": "2021-04-07T00:31:30.640369Z" - } - ], - "has_handgrading_rubric": False, - "send_email_on_submission_received": True, - "send_email_on_non_deferred_tests_finished": True, - "use_honor_pledge": True, - "honor_pledge_text": ( - "I have neither given nor received aid on this project, " - "nor have I concealed any violations of the Honor Code." - ) - }, - constants["PROJECT_1005"], - { - "pk": 1009, - "name": "Project 2 - Server-side Dynamic Pages", - "last_modified": "2021-05-19T13:03:53.221672Z", - "course": 109, - "visible_to_students": True, - "closing_time": "2021-05-19T04:30:00Z", - "soft_closing_time": "2021-05-19T03:59:00Z", - "disallow_student_submissions": False, - "disallow_group_registration": False, - "guests_can_submit": False, - "min_group_size": 1, - "max_group_size": 3, - "submission_limit_per_day": 3, - "allow_submissions_past_limit": False, - "groups_combine_daily_submissions": False, - "submission_limit_reset_time": "00:00:00", - "submission_limit_reset_timezone": "US/Eastern", - "num_bonus_submissions": 0, - "total_submission_limit": None, - "allow_late_days": False, - "ultimate_submission_policy": "best", - "hide_ultimate_submission_fdbk": False, - "instructor_files": [ - { - "pk": 22506, - "project": 1009, - "name": "autograder-788e5076.tar.gz", - "last_modified": "2021-05-11T19:21:55.373807Z", - "size": 282326 - }, - { - "pk": 22507, - "project": 1009, - "name": "install", - "last_modified": "2021-05-11T19:21:55.596257Z", - "size": 2095 - } - ], - "expected_student_files": [ - { - "pk": 2549, - "project": 1009, - "pattern": "submit.tar.gz", - "min_num_matches": 1, - "max_num_matches": 1, - "last_modified": "2021-04-07T00:31:33.584510Z" - } - ], - "has_handgrading_rubric": False, - "send_email_on_submission_received": True, - "send_email_on_non_deferred_tests_finished": True, - "use_honor_pledge": True, - "honor_pledge_text": ( - "I have neither given nor received aid on this project, " - "nor have I concealed any violations of the Honor Code." - ) - }, - { - "pk": 1006, - "name": "Project 4 - MapReduce", - "last_modified": "2021-06-15T03:59:11.942724Z", - "course": 109, - "visible_to_students": True, - "closing_time": "2021-06-14T04:30:00Z", - "soft_closing_time": "2021-06-14T03:59:00Z", - "disallow_student_submissions": False, - "disallow_group_registration": False, - "guests_can_submit": False, - "min_group_size": 1, - "max_group_size": 3, - "submission_limit_per_day": 3, - "allow_submissions_past_limit": False, - "groups_combine_daily_submissions": False, - "submission_limit_reset_time": "00:00:00", - "submission_limit_reset_timezone": "US/Eastern", - "num_bonus_submissions": 0, - "total_submission_limit": None, - "allow_late_days": False, - "ultimate_submission_policy": "best", - "hide_ultimate_submission_fdbk": False, - "instructor_files": [ - { - "pk": 21900, - "project": 1006, - "name": "autograder-8b2ec48d.tar.gz", - "last_modified": "2021-04-07T00:31:25.318107Z", - "size": 3077619 - }, - { - "pk": 21901, - "project": 1006, - "name": "install", - "last_modified": "2021-04-07T00:31:25.441090Z", - "size": 2098 - } - ], - "expected_student_files": [ - { - "pk": 2546, - "project": 1006, - "pattern": "submit.tar.gz", - "min_num_matches": 1, - "max_num_matches": 1, - "last_modified": "2021-04-07T00:31:25.503082Z" - } - ], - "has_handgrading_rubric": False, - "send_email_on_submission_received": True, - "send_email_on_non_deferred_tests_finished": True, - "use_honor_pledge": True, - "honor_pledge_text": ( - "I have neither given nor received aid on this project, " - "nor have I concealed any violations of the Honor Code." - ) - } - ]) + text=json.dumps( + [ + { + "pk": 1007, + "name": "Project 5 - Search Engine", + "last_modified": "2021-06-21T16:42:49.476960Z", + "course": 109, + "visible_to_students": True, + "closing_time": "2021-06-21T04:30:00Z", + "soft_closing_time": "2021-06-21T03:59:00Z", + "disallow_student_submissions": False, + "disallow_group_registration": False, + "guests_can_submit": False, + "min_group_size": 1, + "max_group_size": 3, + "submission_limit_per_day": 3, + "allow_submissions_past_limit": False, + "groups_combine_daily_submissions": False, + "submission_limit_reset_time": "00:00:00", + "submission_limit_reset_timezone": "US/Eastern", + "num_bonus_submissions": 0, + "total_submission_limit": None, + "allow_late_days": False, + "ultimate_submission_policy": "best", + "hide_ultimate_submission_fdbk": False, + "instructor_files": [ + { + "pk": 24749, + "project": 1007, + "name": "autograder-4d215e29.tar.xz", + "last_modified": "2021-06-14T03:39:17.838619Z", + "size": 4306452, + }, + { + "pk": 24750, + "project": 1007, + "name": "install", + "last_modified": "2021-06-14T03:39:18.125266Z", + "size": 2906, + }, + ], + "expected_student_files": [ + { + "pk": 2547, + "project": 1007, + "pattern": "submit.tar.xz", + "min_num_matches": 1, + "max_num_matches": 1, + "last_modified": "2021-04-07T00:31:28.029083Z", + } + ], + "has_handgrading_rubric": False, + "send_email_on_submission_received": True, + "send_email_on_non_deferred_tests_finished": True, + "use_honor_pledge": True, + "honor_pledge_text": ( + "I have neither given nor received aid on this project, " + "nor have I concealed any violations of the Honor Code." + ), + }, + { + "pk": 1008, + "name": "Project 3 - Client-side Dynamic Pages", + "last_modified": "2021-05-27T22:01:15.359868Z", + "course": 109, + "visible_to_students": True, + "closing_time": "2021-05-26T04:30:00Z", + "soft_closing_time": "2021-05-26T03:59:00Z", + "disallow_student_submissions": False, + "disallow_group_registration": False, + "guests_can_submit": False, + "min_group_size": 1, + "max_group_size": 3, + "submission_limit_per_day": 3, + "allow_submissions_past_limit": False, + "groups_combine_daily_submissions": False, + "submission_limit_reset_time": "00:00:00", + "submission_limit_reset_timezone": "US/Eastern", + "num_bonus_submissions": 0, + "total_submission_limit": None, + "allow_late_days": False, + "ultimate_submission_policy": "best", + "hide_ultimate_submission_fdbk": False, + "instructor_files": [ + { + "pk": 22508, + "project": 1008, + "name": "autograder-bcc78f56-dirty.tar.gz", + "last_modified": "2021-05-18T22:00:36.641145Z", + "size": 168836, + }, + { + "pk": 21949, + "project": 1008, + "name": "install", + "last_modified": "2021-05-18T22:00:36.805560Z", + "size": 3057, + }, + ], + "expected_student_files": [ + { + "pk": 2548, + "project": 1008, + "pattern": "submit.tar.gz", + "min_num_matches": 1, + "max_num_matches": 1, + "last_modified": "2021-04-07T00:31:30.640369Z", + } + ], + "has_handgrading_rubric": False, + "send_email_on_submission_received": True, + "send_email_on_non_deferred_tests_finished": True, + "use_honor_pledge": True, + "honor_pledge_text": ( + "I have neither given nor received aid on this project, " + "nor have I concealed any violations of the Honor Code." + ), + }, + constants["PROJECT_1005"], + { + "pk": 1009, + "name": "Project 2 - Server-side Dynamic Pages", + "last_modified": "2021-05-19T13:03:53.221672Z", + "course": 109, + "visible_to_students": True, + "closing_time": "2021-05-19T04:30:00Z", + "soft_closing_time": "2021-05-19T03:59:00Z", + "disallow_student_submissions": False, + "disallow_group_registration": False, + "guests_can_submit": False, + "min_group_size": 1, + "max_group_size": 3, + "submission_limit_per_day": 3, + "allow_submissions_past_limit": False, + "groups_combine_daily_submissions": False, + "submission_limit_reset_time": "00:00:00", + "submission_limit_reset_timezone": "US/Eastern", + "num_bonus_submissions": 0, + "total_submission_limit": None, + "allow_late_days": False, + "ultimate_submission_policy": "best", + "hide_ultimate_submission_fdbk": False, + "instructor_files": [ + { + "pk": 22506, + "project": 1009, + "name": "autograder-788e5076.tar.gz", + "last_modified": "2021-05-11T19:21:55.373807Z", + "size": 282326, + }, + { + "pk": 22507, + "project": 1009, + "name": "install", + "last_modified": "2021-05-11T19:21:55.596257Z", + "size": 2095, + }, + ], + "expected_student_files": [ + { + "pk": 2549, + "project": 1009, + "pattern": "submit.tar.gz", + "min_num_matches": 1, + "max_num_matches": 1, + "last_modified": "2021-04-07T00:31:33.584510Z", + } + ], + "has_handgrading_rubric": False, + "send_email_on_submission_received": True, + "send_email_on_non_deferred_tests_finished": True, + "use_honor_pledge": True, + "honor_pledge_text": ( + "I have neither given nor received aid on this project, " + "nor have I concealed any violations of the Honor Code." + ), + }, + { + "pk": 1006, + "name": "Project 4 - MapReduce", + "last_modified": "2021-06-15T03:59:11.942724Z", + "course": 109, + "visible_to_students": True, + "closing_time": "2021-06-14T04:30:00Z", + "soft_closing_time": "2021-06-14T03:59:00Z", + "disallow_student_submissions": False, + "disallow_group_registration": False, + "guests_can_submit": False, + "min_group_size": 1, + "max_group_size": 3, + "submission_limit_per_day": 3, + "allow_submissions_past_limit": False, + "groups_combine_daily_submissions": False, + "submission_limit_reset_time": "00:00:00", + "submission_limit_reset_timezone": "US/Eastern", + "num_bonus_submissions": 0, + "total_submission_limit": None, + "allow_late_days": False, + "ultimate_submission_policy": "best", + "hide_ultimate_submission_fdbk": False, + "instructor_files": [ + { + "pk": 21900, + "project": 1006, + "name": "autograder-8b2ec48d.tar.gz", + "last_modified": "2021-04-07T00:31:25.318107Z", + "size": 3077619, + }, + { + "pk": 21901, + "project": 1006, + "name": "install", + "last_modified": "2021-04-07T00:31:25.441090Z", + "size": 2098, + }, + ], + "expected_student_files": [ + { + "pk": 2546, + "project": 1006, + "pattern": "submit.tar.gz", + "min_num_matches": 1, + "max_num_matches": 1, + "last_modified": "2021-04-07T00:31:25.503082Z", + } + ], + "has_handgrading_rubric": False, + "send_email_on_submission_received": True, + "send_email_on_non_deferred_tests_finished": True, + "use_honor_pledge": True, + "honor_pledge_text": ( + "I have neither given nor received aid on this project, " + "nor have I concealed any violations of the Honor Code." + ), + }, + ] + ), ) # Project detail @@ -475,9 +479,7 @@ def api_requests_mock(requests_mock, mocker, constants): "pk": 243636, "project": 1005, "extended_due_date": None, - "member_names": [ - "achitta@umich.edu" - ], + "member_names": ["achitta@umich.edu"], "members": [ { "pk": 11854, @@ -485,7 +487,7 @@ def api_requests_mock(requests_mock, mocker, constants): "first_name": "Aditya", "last_name": "Chitta", "email": "", - "is_superuser": False + "is_superuser": False, } ], "bonus_submissions_remaining": 0, @@ -493,15 +495,13 @@ def api_requests_mock(requests_mock, mocker, constants): "num_submissions": 1, "num_submits_towards_limit": 0, "created_at": "2021-04-07T02:17:16.975983Z", - "last_modified": "2021-04-07T02:17:16.975700Z" + "last_modified": "2021-04-07T02:17:16.975700Z", }, { "pk": 246965, "project": 1005, "extended_due_date": None, - "member_names": [ - "awdeorio@umich.edu" - ], + "member_names": ["awdeorio@umich.edu"], "members": [ { "pk": 5, @@ -509,7 +509,7 @@ def api_requests_mock(requests_mock, mocker, constants): "first_name": "Andrew", "last_name": "DeOrio", "email": "", - "is_superuser": False + "is_superuser": False, } ], "bonus_submissions_remaining": 0, @@ -517,9 +517,10 @@ def api_requests_mock(requests_mock, mocker, constants): "num_submissions": 2, "num_submits_towards_limit": 1, "created_at": "2021-04-21T17:01:37.807261Z", - "last_modified": "2021-04-21T17:01:37.807025Z" + "last_modified": "2021-04-21T17:01:37.807025Z", }, - ]), + ] + ), ) # Group detail @@ -533,10 +534,12 @@ def api_requests_mock(requests_mock, mocker, constants): requests_mock.get( "https://autograder.io/api/groups/246965/submissions/", headers={"Content-Type": "application/json"}, - text=json.dumps([ - constants["SUBMISSION_1128572"], - constants["SUBMISSION_1125717"], - ]) + text=json.dumps( + [ + constants["SUBMISSION_1128572"], + constants["SUBMISSION_1125717"], + ] + ), ) # Submission detail @@ -554,7 +557,7 @@ def api_requests_mock(requests_mock, mocker, constants): ) # Project autograder configuration - config_path = utils.TESTDATA_DIR/"eecs485sp21_p1_config.json" + config_path = utils.TESTDATA_DIR / "eecs485sp21_p1_config.json" config_text = config_path.read_text() requests_mock.get( "https://autograder.io/api/projects/1005/ag_test_suites/", diff --git a/tests/test_cli.py b/tests/test_cli.py index 704ed38..2bf4d6c 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -4,8 +4,10 @@ These tests use the Click testing interface. https://click.palletsprojects.com/en/8.0.x/testing/ """ + import click import click.testing + from agiocli.__main__ import main diff --git a/tests/test_courses.py b/tests/test_courses.py index 3809a11..ddd012e 100644 --- a/tests/test_courses.py +++ b/tests/test_courses.py @@ -3,17 +3,16 @@ These tests use the Click testing interface. https://click.palletsprojects.com/en/8.0.x/testing/ """ + import json import textwrap + import click import click.testing import freezegun from pick import Option -from agiocli.__main__ import main - -# Unused arguments due to fixtures are endemic to pytest -# pylint: disable=unused-argument +from agiocli.__main__ import main def test_courses_list(api_mock): @@ -47,14 +46,14 @@ def test_courses_empty(api_mock, mocker): """ # Mock user-selection menu, users selects course pk 109 course_109 = { - 'pk': 109, - 'name': 'EECS 485', - 'semester': 'Spring', - 'year': 2021, - 'subtitle': 'Web Systems', - 'num_late_days': 0, - 'allowed_guest_domain': '@umich.edu', - 'last_modified': '2021-04-07T02:19:22.818992Z' + "pk": 109, + "name": "EECS 485", + "semester": "Spring", + "year": 2021, + "subtitle": "Web Systems", + "num_late_days": 0, + "allowed_guest_domain": "@umich.edu", + "last_modified": "2021-04-07T02:19:22.818992Z", } mocker.patch("pick.pick", return_value=(Option(course_109, course_109), 1)) @@ -95,7 +94,8 @@ def test_courses_name(api_mock): """ runner = click.testing.CliRunner() result = runner.invoke( - main, ["courses", "EECS 485 Spring 2021"], + main, + ["courses", "EECS 485 Spring 2021"], catch_exceptions=False, ) assert result.exit_code == 0, result.output @@ -114,7 +114,8 @@ def test_courses_shortcut(api_mock): """ runner = click.testing.CliRunner() result = runner.invoke( - main, ["courses", "eecs485sp21"], + main, + ["courses", "eecs485sp21"], catch_exceptions=False, ) assert result.exit_code == 0, result.output @@ -136,7 +137,8 @@ def test_courses_default_semester(api_mock): runner = click.testing.CliRunner() with freezegun.freeze_time("2021-06-15"): result = runner.invoke( - main, ["courses", "eecs485"], + main, + ["courses", "eecs485"], catch_exceptions=False, ) diff --git a/tests/test_groups.py b/tests/test_groups.py index ffc0217..b844e6f 100644 --- a/tests/test_groups.py +++ b/tests/test_groups.py @@ -3,15 +3,14 @@ These tests use the Click testing interface. https://click.palletsprojects.com/en/8.0.x/testing/ """ + import json + import click import click.testing from pick import Option -from agiocli.__main__ import main - -# Unused arguments due to fixtures are endemic to pytest -# pylint: disable=unused-argument +from agiocli.__main__ import main def test_groups_list(api_mock): @@ -25,10 +24,12 @@ def test_groups_list(api_mock): """ runner = click.testing.CliRunner() result = runner.invoke( - main, [ + main, + [ "groups", "--list", - "--project", "1005", + "--project", + "1005", ], catch_exceptions=False, ) @@ -48,10 +49,12 @@ def test_groups_list_json(api_mock): """ runner = click.testing.CliRunner() result = runner.invoke( - main, [ + main, + [ "groups", "--list-json", - "--project", "1005", + "--project", + "1005", ], catch_exceptions=False, ) @@ -77,12 +80,12 @@ def test_groups_pk(api_mock): assert output_obj["pk"] == 246965 assert output_obj["members"] == [ { - 'email': '', - 'first_name': 'Andrew', - 'is_superuser': False, - 'last_name': 'DeOrio', - 'pk': 5, - 'username': 'awdeorio@umich.edu', + "email": "", + "first_name": "Andrew", + "is_superuser": False, + "last_name": "DeOrio", + "pk": 5, + "username": "awdeorio@umich.edu", }, ] @@ -101,10 +104,13 @@ def test_groups_uniqname(api_mock): """ runner = click.testing.CliRunner() result = runner.invoke( - main, [ + main, + [ "groups", - "--course", "eecs485sp21", - "--project", "p1", + "--course", + "eecs485sp21", + "--project", + "p1", "awdeorio", ], catch_exceptions=False, @@ -126,12 +132,15 @@ def test_groups_empty(api_mock, mocker, constants): # Mock user-selection menu, users selects course 109, then project 1005. # These are constants in conftest.py. Mock input "awdeorio", which selects # a group. - mocker.patch("pick.pick", side_effect=[ - # First call to pick() selects course - (Option(constants["COURSE_109"], constants["COURSE_109"]), 1), - # Second call selects project - (Option(constants["PROJECT_1005"], constants["PROJECT_1005"]), 0), - ]) + mocker.patch( + "pick.pick", + side_effect=[ + # First call to pick() selects course + (Option(constants["COURSE_109"], constants["COURSE_109"]), 1), + # Second call selects project + (Option(constants["PROJECT_1005"], constants["PROJECT_1005"]), 0), + ], + ) mocker.patch("builtins.input", return_value="awdeorio") # Run agio diff --git a/tests/test_matching.py b/tests/test_matching.py index 3958671..a7d671d 100644 --- a/tests/test_matching.py +++ b/tests/test_matching.py @@ -1,11 +1,13 @@ """Unit tests for smart user input string matching.""" + import freezegun import pytest + from agiocli import utils @pytest.mark.parametrize( - "search, expected_course_pk", + ("search", "expected_course_pk"), [ ("EECS 280 Spring 2021", 111), ("eecs 280 spring 2021", 111), @@ -20,7 +22,7 @@ ("eecs485sp21", 109), ("eecs485-sp-21", 109), ("eecs485-sp_21", 109), - ] + ], ) def test_course_match_input_patterns(search, expected_course_pk): """Many supported input patterns.""" @@ -31,7 +33,7 @@ def test_course_match_input_patterns(search, expected_course_pk): @pytest.mark.parametrize( - "search, expected_course_pk", + ("search", "expected_course_pk"), [ ("EECS 598 Winter 2019", 38), ("EECS 398 Fall 2019", 53), @@ -61,7 +63,7 @@ def test_course_match_input_patterns(search, expected_course_pk): # Above won't match because there is a duplicate ("498f20", 79), # This won't match on EECS 598 ("398w21", 90), - ] + ], ) def test_course_match_pattern(search, expected_course_pk): """Many supported input patterns.""" @@ -84,7 +86,7 @@ def test_course_match_pattern(search, expected_course_pk): ("485 sp 16"), ("eecs280sp16"), ("eecs485sp16"), - ] + ], ) def test_course_match_bad_year(search): """Bad year in pattern.""" @@ -93,7 +95,7 @@ def test_course_match_bad_year(search): @pytest.mark.parametrize( - "search, expected_course_pk", + ("search", "expected_course_pk"), [ ("EECS 280", 111), ("EECS 280 cur", 111), @@ -111,8 +113,7 @@ def test_course_match_bad_year(search): ("eecs485current", 109), ("eecs485-cur", 109), ("eecs485-current", 109), - - ] + ], ) def test_course_match_current(search, expected_course_pk): """Auto select current semester.""" @@ -126,7 +127,7 @@ def test_course_match_current(search, expected_course_pk): @pytest.mark.parametrize( - "search, expected_project_pk", + ("search", "expected_project_pk"), [ ("Lab 01 - Getting Started", 435), ("Lab 1 - Getting Started", 435), @@ -157,7 +158,7 @@ def test_course_match_current(search, expected_course_pk): ("P_02", 423), ("P_02_images", 423), ("Polymorphism", 424), - ] + ], ) def test_project_match_pattern(search, expected_project_pk): """Many supported input patterns.""" @@ -168,14 +169,14 @@ def test_project_match_pattern(search, expected_project_pk): @pytest.mark.parametrize( - "search, expected_project_pk", + ("search", "expected_project_pk"), [ ("p1", 1527), ("p2", 1525), ("p3", 1524), ("p4", 1526), ("p5", 1523), - ] + ], ) def test_project_match_pattern_include_invalid(search, expected_project_pk): """Many supported input patterns.""" @@ -196,7 +197,7 @@ def test_project_match_pattern_include_invalid(search, expected_project_pk): ("Project 3 - Images"), ("Lab 2 - Images"), ("L14"), - ] + ], ) def test_project_match_bad_num(search): """Bad project number in pattern.""" @@ -217,32 +218,37 @@ def test_project_match_bad_num(search): {"pk": 12, "name": "EECS 280", "semester": "Winter", "year": 2018}, {"pk": 14, "name": "ENGR 101", "semester": "Winter", "year": 2018}, {"pk": 15, "name": "EECS 485", "semester": "Winter", "year": 2018}, - {"pk": 16, "name": "EECS 398: System Design of a Search Engine", - "semester": "Winter", "year": 2019}, + { + "pk": 16, + "name": "EECS 398: System Design of a Search Engine", + "semester": "Winter", + "year": 2019, + }, {"pk": 17, "name": "EECS 280", "semester": "Spring", "year": 2018}, {"pk": 18, "name": "EECS 370", "semester": "Spring", "year": 2018}, {"pk": 19, "name": "Demo EECS 280 F17", "semester": None, "year": None}, {"pk": 20, "name": "Demo Course", "semester": None, "year": None}, {"pk": 21, "name": "EECS 280 Diagnostic", "semester": None, "year": None}, {"pk": 22, "name": "EECS 183", "semester": "Fall", "year": 2018}, - {"pk": 23, "name": "Copy of EECS 280 SP 2018", - "semester": None, "year": None}, + {"pk": 23, "name": "Copy of EECS 280 SP 2018", "semester": None, "year": None}, {"pk": 25, "name": "EECS 490", "semester": "Fall", "year": 2018}, {"pk": 26, "name": "EECS 280", "semester": "Fall", "year": 2018}, {"pk": 27, "name": "EECS 285", "semester": "Fall", "year": 2018}, {"pk": 29, "name": "EECS 485", "semester": "Fall", "year": 2018}, {"pk": 30, "name": "ENGR 101", "semester": "Fall", "year": 2018}, - {"pk": 31, "name": "EECS 498-001 (Data Mining)", - "semester": "Fall", "year": 2018}, + {"pk": 31, "name": "EECS 498-001 (Data Mining)", "semester": "Fall", "year": 2018}, {"pk": 32, "name": "EECS 280", "semester": "Winter", "year": 2019}, - {"pk": 33, "name": "EECS 183 All Projects", - "semester": None, "year": None}, + {"pk": 33, "name": "EECS 183 All Projects", "semester": None, "year": None}, {"pk": 34, "name": "EECS 183", "semester": "Winter", "year": 2019}, {"pk": 35, "name": "EECS 485", "semester": "Winter", "year": 2019}, {"pk": 36, "name": "ENGR 101", "semester": "Winter", "year": 2019}, {"pk": 37, "name": "EECS 481", "semester": "Winter", "year": 2019}, - {"pk": 38, "name": "EECS 598-008 Advanced Data Mining", - "semester": "Winter", "year": 2019}, + { + "pk": 38, + "name": "EECS 598-008 Advanced Data Mining", + "semester": "Winter", + "year": 2019, + }, {"pk": 40, "name": "EECS 483", "semester": "Winter", "year": 2019}, {"pk": 41, "name": "EECS 493", "semester": "Winter", "year": 2019}, {"pk": 42, "name": "EECS 370", "semester": "Fall", "year": 2019}, @@ -252,13 +258,16 @@ def test_project_match_bad_num(search): {"pk": 46, "name": "EECS 485", "semester": "Fall", "year": 2019}, {"pk": 47, "name": "EECS 183", "semester": "Fall", "year": 2019}, {"pk": 48, "name": "ENGR 151", "semester": "Fall", "year": 2019}, - {"pk": 49, "name": "MSci Computer Science", - "semester": "Summer", "year": 2020}, + {"pk": 49, "name": "MSci Computer Science", "semester": "Summer", "year": 2020}, {"pk": 50, "name": "EECS 280", "semester": "Fall", "year": 2019}, {"pk": 51, "name": "EECS 481", "semester": "Fall", "year": 2019}, {"pk": 52, "name": "ENGR 101", "semester": "Fall", "year": 2019}, - {"pk": 53, "name": "EECS 398: System Design of a Search Engine", - "semester": "Fall", "year": 2019}, + { + "pk": 53, + "name": "EECS 398: System Design of a Search Engine", + "semester": "Fall", + "year": 2019, + }, {"pk": 54, "name": "EECS 484", "semester": "Fall", "year": 2019}, {"pk": 55, "name": "EECS 183", "semester": "Winter", "year": 2020}, {"pk": 56, "name": "EECS 280", "semester": "Winter", "year": 2020}, @@ -276,15 +285,21 @@ def test_project_match_bad_num(search): {"pk": 70, "name": "EECS 280", "semester": "Spring", "year": 2020}, {"pk": 71, "name": "EECS 484", "semester": "Spring", "year": 2020}, {"pk": 74, "name": "EECS 485", "semester": "Summer", "year": 2020}, - {"pk": 75, "name": "EECS 280 - INCOMPLETES", - "semester": "Winter", "year": 2020}, - {"pk": 76, "name": "EECS 484 (copied from F19)", - "semester": "Fall", "year": 2020}, - {"pk": 77, "name": "EECS 484 Fall 2020 (official)", - "semester": "Fall", "year": 2020}, + {"pk": 75, "name": "EECS 280 - INCOMPLETES", "semester": "Winter", "year": 2020}, + {"pk": 76, "name": "EECS 484 (copied from F19)", "semester": "Fall", "year": 2020}, + { + "pk": 77, + "name": "EECS 484 Fall 2020 (official)", + "semester": "Fall", + "year": 2020, + }, {"pk": 78, "name": "EECS 183", "semester": "Fall", "year": 2020}, - {"pk": 79, "name": "EECS 498/598 Deep Learning for Computer Vision", - "semester": "Fall", "year": 2020}, + { + "pk": 79, + "name": "EECS 498/598 Deep Learning for Computer Vision", + "semester": "Fall", + "year": 2020, + }, {"pk": 80, "name": "EECS 280", "semester": "Fall", "year": 2020}, {"pk": 81, "name": "CIS 350", "semester": "Fall", "year": 2020}, {"pk": 82, "name": "EECS 481", "semester": "Fall", "year": 2020}, @@ -292,36 +307,49 @@ def test_project_match_bad_num(search): {"pk": 85, "name": "EECS 485", "semester": "Fall", "year": 2020}, {"pk": 86, "name": "EECS 285", "semester": "Fall", "year": 2020}, {"pk": 87, "name": "ENGR 101", "semester": "Fall", "year": 2020}, - {"pk": 88, "name": "Research and Development Autograder", - "semester": "Fall", "year": 2020}, + { + "pk": 88, + "name": "Research and Development Autograder", + "semester": "Fall", + "year": 2020, + }, {"pk": 89, "name": "ENGR 151", "semester": "Fall", "year": 2020}, {"pk": 90, "name": "EECS 398-001", "semester": "Winter", "year": 2021}, - {"pk": 91, "name": "EECS 183 - Elevators Development OUTDATED", - "semester": "Fall", "year": 2020}, + { + "pk": 91, + "name": "EECS 183 - Elevators Development OUTDATED", + "semester": "Fall", + "year": 2020, + }, {"pk": 93, "name": "ENGR 101", "semester": "Winter", "year": 2021}, {"pk": 94, "name": "EECS 485", "semester": "Winter", "year": 2021}, - {"pk": 95, "name": "EECS 440: System Design of a Search Engine", - "semester": "Winter", "year": 2021}, + { + "pk": 95, + "name": "EECS 440: System Design of a Search Engine", + "semester": "Winter", + "year": 2021, + }, {"pk": 96, "name": "EECS 481", "semester": "Winter", "year": 2021}, - {"pk": 97, "name": "EECS 484 Winter 2021", - "semester": "Winter", "year": 2021}, + {"pk": 97, "name": "EECS 484 Winter 2021", "semester": "Winter", "year": 2021}, {"pk": 98, "name": "EECS 476", "semester": "Winter", "year": 2021}, {"pk": 99, "name": "EECS 183", "semester": "Winter", "year": 2021}, {"pk": 100, "name": "EECS 280", "semester": "Winter", "year": 2021}, {"pk": 101, "name": "EECS 370", "semester": "Winter", "year": 2021}, {"pk": 102, "name": "EECS 590", "semester": "Winter", "year": 2021}, - {"pk": 106, "name": "EECS 442 Computer Vision Winter 2021", - "semester": "Winter", "year": 2021}, + { + "pk": 106, + "name": "EECS 442 Computer Vision Winter 2021", + "semester": "Winter", + "year": 2021, + }, {"pk": 107, "name": "EECS 483", "semester": "Winter", "year": 2021}, {"pk": 108, "name": "CIS 350", "semester": "Winter", "year": 2021}, {"pk": 109, "name": "EECS 485", "semester": "Spring", "year": 2021}, - {"pk": 110, "name": "EECS 484 Spring 2021", - "semester": "Spring", "year": 2021}, + {"pk": 110, "name": "EECS 484 Spring 2021", "semester": "Spring", "year": 2021}, {"pk": 111, "name": "EECS 280", "semester": "Spring", "year": 2021}, {"pk": 112, "name": "EECS 370", "semester": "Spring", "year": 2021}, - {"pk": 113, "name": "EECS 370 - Make up", - "semester": "Spring", "year": 2021}, - {"pk": 126, "name": "ENGR 101", "semester": "Fall", "year": 2021} + {"pk": 113, "name": "EECS 370 - Make up", "semester": "Spring", "year": 2021}, + {"pk": 126, "name": "ENGR 101", "semester": "Fall", "year": 2021}, ] diff --git a/tests/test_projects.py b/tests/test_projects.py index d8e35af..00baf22 100644 --- a/tests/test_projects.py +++ b/tests/test_projects.py @@ -3,18 +3,17 @@ These tests use the Click testing interface. https://click.palletsprojects.com/en/8.0.x/testing/ """ + import json -import textwrap import shlex +import textwrap + import click import click.testing import utils from pick import Option -from agiocli.__main__ import main - -# Unused arguments due to fixtures are endemic to pytest -# pylint: disable=unused-argument +from agiocli.__main__ import main def test_projects_list_course_pk(api_mock): @@ -28,7 +27,8 @@ def test_projects_list_course_pk(api_mock): """ runner = click.testing.CliRunner() result = runner.invoke( - main, ["projects", "--list", "--course", "109"], + main, + ["projects", "--list", "--course", "109"], catch_exceptions=False, ) assert result.exit_code == 0, result.output @@ -70,9 +70,11 @@ def test_projects_name(api_mock): """ runner = click.testing.CliRunner() result = runner.invoke( - main, [ + main, + [ "projects", - "--course", "eecs485sp21", + "--course", + "eecs485sp21", "Project 1 - Templated Static Site Generator", ], catch_exceptions=False, @@ -94,9 +96,11 @@ def test_projects_shortcut(api_mock): """ runner = click.testing.CliRunner() result = runner.invoke( - main, [ + main, + [ "projects", - "-c", "eecs485sp21", + "-c", + "eecs485sp21", "p1", ], catch_exceptions=False, @@ -117,8 +121,10 @@ def test_projects_no_course(api_mock, mocker, constants): """ # Mock user-selection menu, users selects course 109. This constant is # defined in conftest.py - mocker.patch("pick.pick", return_value=( - Option(constants["COURSE_109"], constants["COURSE_109"]), 1)) + mocker.patch( + "pick.pick", + return_value=(Option(constants["COURSE_109"], constants["COURSE_109"]), 1), + ) # Run agio runner = click.testing.CliRunner() @@ -141,12 +147,15 @@ def test_projects_empty(api_mock, mocker, constants): """ # Mock user-selection menu, users selects course 109, then project 1005. # These constants are defined in conftest.py - mocker.patch("pick.pick", side_effect=[ - # First call to pick() selects course - (Option(constants["COURSE_109"], constants["COURSE_109"]), 1), - # Second call selects project - (Option(constants["PROJECT_1005"], constants["PROJECT_1005"]), 0), - ]) + mocker.patch( + "pick.pick", + side_effect=[ + # First call to pick() selects course + (Option(constants["COURSE_109"], constants["COURSE_109"]), 1), + # Second call selects project + (Option(constants["PROJECT_1005"], constants["PROJECT_1005"]), 0), + ], + ) # Run agio runner = click.testing.CliRunner() @@ -169,12 +178,13 @@ def test_projects_config(api_mock, mocker, constants): """ runner = click.testing.CliRunner() result = runner.invoke( - main, shlex.split("projects -c eecs485sp21 p1 --config"), + main, + shlex.split("projects -c eecs485sp21 p1 --config"), catch_exceptions=False, ) assert result.exit_code == 0, result.output output = json.loads(result.output) - expected_path = utils.TESTDATA_DIR/"eecs485sp21_p1_config.json" + expected_path = utils.TESTDATA_DIR / "eecs485sp21_p1_config.json" with expected_path.open(encoding="utf-8") as infile: expected = json.load(infile) assert output == expected diff --git a/tests/test_submissions.py b/tests/test_submissions.py index 5b8a764..c73ce18 100644 --- a/tests/test_submissions.py +++ b/tests/test_submissions.py @@ -3,15 +3,14 @@ These tests use the Click testing interface. https://click.palletsprojects.com/en/8.0.x/testing/ """ + import json + import click import click.testing from pick import Option -from agiocli.__main__ import main - -# Unused arguments due to fixtures are endemic to pytest -# pylint: disable=unused-argument +from agiocli.__main__ import main def test_submissions_list(api_mock): @@ -25,10 +24,12 @@ def test_submissions_list(api_mock): """ runner = click.testing.CliRunner() result = runner.invoke( - main, [ + main, + [ "submissions", "--list", - "--group", "246965", + "--group", + "246965", ], catch_exceptions=False, ) @@ -48,7 +49,8 @@ def test_submissions_pk(api_mock): """ runner = click.testing.CliRunner() result = runner.invoke( - main, ["submissions", "1128572"], + main, + ["submissions", "1128572"], catch_exceptions=False, ) assert result.exit_code == 0, result.output @@ -71,11 +73,15 @@ def test_submissions_last(api_mock): """ runner = click.testing.CliRunner() result = runner.invoke( - main, [ + main, + [ "submissions", - "--course", "eecs485sp21", - "--project", "p1", - "--group", "awdeorio", + "--course", + "eecs485sp21", + "--project", + "p1", + "--group", + "awdeorio", "last", ], catch_exceptions=False, @@ -100,11 +106,15 @@ def test_submissions_best(api_mock): """ runner = click.testing.CliRunner() result = runner.invoke( - main, [ + main, + [ "submissions", - "--course", "eecs485sp21", - "--project", "p1", - "--group", "awdeorio", + "--course", + "eecs485sp21", + "--project", + "p1", + "--group", + "awdeorio", "best", ], catch_exceptions=False, @@ -126,15 +136,22 @@ def test_submissions_empty(api_mock, mocker, constants): # Mock user-selection menu, users selects course 109, then project 1005, # then submission 1128572. These are constants in conftest.py. Mock input # "awdeorio", which selects a group. - mocker.patch("pick.pick", side_effect=[ - # First call to pick() selects course - (Option(constants["COURSE_109"], constants["COURSE_109"]), 1), - # Second call selects project - (Option(constants["PROJECT_1005"], constants["PROJECT_1005"]), 0), - # Third call selects submission - (Option(constants["SUBMISSION_1128572"], - constants["SUBMISSION_1128572"]), 0), - ]) + mocker.patch( + "pick.pick", + side_effect=[ + # First call to pick() selects course + (Option(constants["COURSE_109"], constants["COURSE_109"]), 1), + # Second call selects project + (Option(constants["PROJECT_1005"], constants["PROJECT_1005"]), 0), + # Third call selects submission + ( + Option( + constants["SUBMISSION_1128572"], constants["SUBMISSION_1128572"] + ), + 0, + ), + ], + ) mocker.patch("builtins.input", return_value="awdeorio") # Run agio diff --git a/tests/utils.py b/tests/utils.py index 8847545..214c6f4 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,7 @@ """Common test utilities.""" -import pathlib +import pathlib # Path to testdata directory TEST_DIR = pathlib.Path(__file__).parent -TESTDATA_DIR = TEST_DIR/"testdata" +TESTDATA_DIR = TEST_DIR / "testdata" diff --git a/tox.ini b/tox.ini index 8044e5a..644127f 100644 --- a/tox.ini +++ b/tox.ini @@ -4,15 +4,12 @@ envlist = py3 # Run unit tests -# HACK: Pydocstyle fails to find tests. Invoke a shell to use a glob. [testenv] setenv = PYTHONPATH = {toxinidir} -allowlist_externals = sh extras = dev commands = - pycodestyle agiocli tests - sh -c "pydocstyle agiocli tests/*" - pylint agiocli tests + ruff check agiocli tests + ruff format --check agiocli tests check-manifest pytest -vvs --cov agiocli From ce4f602171072dfa41802ca40714e11a684bcea4 Mon Sep 17 00:00:00 2001 From: Andrew DeOrio Date: Tue, 21 Jul 2026 08:55:01 -0400 Subject: [PATCH 2/3] Clean up exceptions --- agiocli/__init__.py | 4 ++-- pyproject.toml | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/agiocli/__init__.py b/agiocli/__init__.py index 762cfbe..3d9f49e 100644 --- a/agiocli/__init__.py +++ b/agiocli/__init__.py @@ -1,4 +1,4 @@ """Autograder.io CLI API.""" -from .api_client import APIClient, TokenFileNotFound -from .utils import * +from .api_client import APIClient as APIClient +from .api_client import TokenFileNotFound as TokenFileNotFound diff --git a/pyproject.toml b/pyproject.toml index 51cb7a6..a413f11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,5 +56,4 @@ ignore = ["D203", "D213", "W191", "E111", "E114", "E117", "D206", "D300"] convention = "pep257" [tool.ruff.lint.per-file-ignores] -"__init__.py" = ["F401", "F403"] "test_*.py" = ["PLR2004", "ARG001"] From d3a044167da455faa7cd668dd6dbe44b58d3d087 Mon Sep 17 00:00:00 2001 From: Andrew DeOrio Date: Tue, 21 Jul 2026 08:56:15 -0400 Subject: [PATCH 3/3] Document why test files ignore PLR2004 and ARG001 --- pyproject.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index a413f11..6ed342d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,4 +56,6 @@ ignore = ["D203", "D213", "W191", "E111", "E114", "E117", "D206", "D300"] convention = "pep257" [tool.ruff.lint.per-file-ignores] +# PLR2004: test assertions compare against bare literals, e.g. assert pk == 109 +# ARG001: test functions take pytest fixtures as unused arguments, e.g. api_mock "test_*.py" = ["PLR2004", "ARG001"]