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: Test Lint #28

Merged
merged 6 commits into from
Dec 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
22 changes: 22 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Run tests

on:
pull_request:
branches: [master]

jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.8
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Upgrade pip
run: python -m pip install --upgrade pip setuptools
- name: Install dependencies
run: |
pip install .[dev]
- name: Test lint, types, and format
run: make test
34 changes: 34 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
.DEFAULT_GOAL := help
.PHONY: docs
SRC_DIRS = ./tutorxqueue
BLACK_OPTS = --exclude templates ${SRC_DIRS}

# Warning: These checks are not necessarily run on every PR.
test: test-lint test-types test-format # Run some static checks.

test-format: ## Run code formatting tests
black --check --diff $(BLACK_OPTS)

test-lint: ## Run code linting tests
pylint --errors-only --enable=unused-import,unused-argument --ignore=templates --ignore=docs/_ext ${SRC_DIRS}

test-types: ## Run type checks.
mypy --exclude=templates --ignore-missing-imports --implicit-reexport --strict ${SRC_DIRS}

format: ## Format code automatically
black $(BLACK_OPTS)

isort: ## Sort imports. This target is not mandatory because the output may be incompatible with black formatting. Provided for convenience purposes.
isort --skip=templates ${SRC_DIRS}

changelog-entry: ## Create a new changelog entry.
scriv create

changelog: ## Collect changelog entries in the CHANGELOG.md file.
scriv collect

ESCAPE = 
help: ## Print this help
@grep -E '^([a-zA-Z_-]+:.*?## .*|######* .+)$$' Makefile \
| sed 's/######* \(.*\)/@ $(ESCAPE)[1;31m\1$(ESCAPE)[0m/g' | tr '@' '\n' \
| awk 'BEGIN {FS = ":.*?## "}; {printf "\033[33m%-30s\033[0m %s\n", $$1, $$2}'
1 change: 1 addition & 0 deletions changelog.d/20231118_161232_codewithemad.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- [Improvement] Added Typing to code, Makefile and test action to the repository and formatted code with Black and isort. (by @CodeWithEmad)
3 changes: 3 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
include_package_data=True,
python_requires=">=3.8",
install_requires=["tutor>=16.0.0,<17.0.0", "requests"],
extras_require={
"dev": ["tutor[dev]>=16.0.0,<17.0.0"],
},
entry_points={"tutor.plugin.v1": ["xqueue = tutorxqueue.plugin"]},
classifiers=[
"Development Status :: 5 - Production/Stable",
Expand Down
1 change: 0 additions & 1 deletion tutorxqueue/__about__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
__version__ = "16.0.2"

142 changes: 87 additions & 55 deletions tutorxqueue/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,24 @@

import json
import os
import typing as t
from glob import glob
from typing import Any, Literal, Optional, Union

import click
import pkg_resources
import requests

import requests # type: ignore
from tutor import config as tutor_config
from tutor.__about__ import __version_suffix__
from tutor import exceptions
from tutor import hooks as tutor_hooks
from tutor.__about__ import __version_suffix__

from .__about__ import __version__

# Handle version suffix in nightly mode, just like tutor core
if __version_suffix__:
__version__ += "-" + __version_suffix__

config = {
"unique": {
"AUTH_PASSWORD": "{{ 8|random_string }}",
"MYSQL_PASSWORD": "{{ 8|random_string }}",
"SECRET_KEY": "{{ 24|random_string }}",
},
config: dict[str, dict[str, Any]] = {
"defaults": {
"VERSION": __version__,
"AUTH_USERNAME": "lms",
Expand All @@ -36,6 +30,11 @@
"REPOSITORY": "https://github.com/openedx/xqueue",
"REPOSITORY_VERSION": "{{ OPENEDX_COMMON_VERSION }}",
},
"unique": {
"AUTH_PASSWORD": "{{ 8|random_string }}",
"MYSQL_PASSWORD": "{{ 8|random_string }}",
"SECRET_KEY": "{{ 24|random_string }}",
},
}

# Initialization hooks
Expand All @@ -61,25 +60,31 @@
tutor_hooks.Filters.CLI_DO_INIT_TASKS.add_item((service, init_task))

# Image management
tutor_hooks.Filters.IMAGES_BUILD.add_item((
"xqueue",
("plugins", "xqueue", "build", "xqueue"),
"{{ XQUEUE_DOCKER_IMAGE }}",
(),
))

tutor_hooks.Filters.IMAGES_PULL.add_item((
"xqueue",
"{{ XQUEUE_DOCKER_IMAGE }}",
))
tutor_hooks.Filters.IMAGES_PUSH.add_item((
"xqueue",
"{{ XQUEUE_DOCKER_IMAGE }}",
))
tutor_hooks.Filters.IMAGES_BUILD.add_item(
(
"xqueue",
("plugins", "xqueue", "build", "xqueue"),
"{{ XQUEUE_DOCKER_IMAGE }}",
(),
)
)

tutor_hooks.Filters.IMAGES_PULL.add_item(
(
"xqueue",
"{{ XQUEUE_DOCKER_IMAGE }}",
)
)
tutor_hooks.Filters.IMAGES_PUSH.add_item(
(
"xqueue",
"{{ XQUEUE_DOCKER_IMAGE }}",
)
)


@tutor_hooks.Filters.COMPOSE_MOUNTS.add()
def _mount_xqueue(volumes, name):
def _mount_xqueue(volumes: list[tuple[str, str]], name: str) -> list[tuple[str, str]]:
"""
When mounting xqueue with `--mount=/path/to/xqueue`,
bind-mount the host repo in the xqueue container.
Expand All @@ -94,11 +99,11 @@ def _mount_xqueue(volumes, name):


@click.group(help="Interact with the Xqueue server", name="xqueue")
def command():
def command() -> None:
pass


@click.group(help="List and grade submissions")
@click.group(help="list and grade submissions")
@click.pass_obj
@click.option("-q", "--queue", default="openedx", show_default=True, help="Queue name")
@click.option(
Expand All @@ -111,21 +116,21 @@ def command():
"from the TUTOR_XQUEUE_URL environment variable."
),
)
def submissions(context, queue, url):
context.queue = queue
context.url = url
def submissions(context: click.Context, queue: str, url: str) -> None:
context.queue = queue # type: ignore
context.url = url # type: ignore


@click.command(name="count", help="Count submissions in queue")
@click.pass_obj
def count_submissions(context):
print_result(context, "count_submissions", context.queue)
def count_submissions(context: click.Context) -> None:
print_result(context, "count_submissions", context.queue) # type: ignore


@click.command(name="show", help="Show last submission")
@click.pass_obj
def show_submission(context):
print_result(context, "show_submission", context.queue)
def show_submission(context: click.Context) -> None:
print_result(context, "show_submission", context.queue) # type: ignore


@click.command(name="grade", help="Grade a specific submission")
Expand All @@ -135,29 +140,43 @@ def show_submission(context):
@click.argument("correct", type=click.BOOL)
@click.argument("message")
@click.pass_obj
def grade_submission(context, submission_id, submission_key, grade, correct, message):
def grade_submission(
context: click.Context,
submission_id: str,
submission_key: str,
grade: str,
correct: str,
message: str,
) -> None:
print_result(
context,
"grade_submission",
submission_id,
submission_key,
grade,
correct,
message,
(
submission_id,
submission_key,
grade,
correct,
message,
),
Comment on lines +154 to +160
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CodeWithEmad what is the purpose of grouping these 5 variables in the print_result method?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was trying to fix the typing issue where *args: tuple[Any, ...]. It may not be the optimal solution, but I couldn't find any other alternative.

)


def print_result(context, client_func_name, *args, **kwargs):
user_config = tutor_config.load(context.root)
client = Client(user_config, url=context.url)
def print_result(
context: click.Context,
client_func_name: str,
*args: tuple[Any, ...],
**kwargs: dict[str, Any],
) -> None:
user_config = tutor_config.load(context.root) # type: ignore
client = Client(user_config, url=context.url) # type: ignore
func = getattr(client, client_func_name)
result = func(*args, **kwargs)
print(json.dumps(result, indent=2))


class Client:
def __init__(self, user_config, url=None):
self._session = None
def __init__(self, user_config: dict[str, Any], url: str = "") -> None:
self._session: Optional[requests.Session] = None
self.username = user_config["XQUEUE_AUTH_USERNAME"]
self.password = user_config["XQUEUE_AUTH_PASSWORD"]

Expand All @@ -169,17 +188,17 @@ def __init__(self, user_config, url=None):
self.login()

@property
def session(self):
def session(self) -> requests.Session:
if self._session is None:
self._session = requests.Session()
return self._session

def url(self, endpoint):
def url(self, endpoint: str) -> str:
# Don't forget to add a trailing slash to all endpoints: this is how xqueue
# works...
return self.base_url + endpoint

def login(self):
def login(self) -> None:
response = self.request(
"/xqueue/login/",
method="POST",
Expand All @@ -193,7 +212,7 @@ def login(self):
)
)

def show_submission(self, queue):
def show_submission(self, queue: str) -> Union[dict[str, Any], Any]:
response = self.request("/xqueue/get_submission/", params={"queue_name": queue})
if response["return_code"] != 0:
return response
Expand All @@ -216,10 +235,17 @@ def show_submission(self, queue):
"return_code": response["return_code"],
}

def count_submissions(self, queue):
def count_submissions(self, queue: str) -> Any:
regisb marked this conversation as resolved.
Show resolved Hide resolved
return self.request("/xqueue/get_queuelen/", params={"queue_name": queue})

def grade_submission(self, submission_id, submission_key, grade, correct, msg):
def grade_submission(
self,
submission_id: str,
submission_key: str,
grade: str,
correct: bool,
msg: str,
) -> Any:
return self.request(
"/xqueue/put_result/",
method="POST",
Expand All @@ -233,7 +259,13 @@ def grade_submission(self, submission_id, submission_key, grade, correct, msg):
},
)

def request(self, endpoint, method="GET", data=None, params=None):
def request(
self,
endpoint: str,
method: str = "GET",
data: Optional[dict[str, Any]] = None,
params: Optional[dict[str, Any]] = None,
) -> Any:
func = getattr(self.session, method.lower())
response = func(self.url(endpoint), data=data, params=params)
# TODO handle errors >= 400 and non-parsable json responses
Expand All @@ -245,7 +277,6 @@ def request(self, endpoint, method="GET", data=None, params=None):
submissions.add_command(grade_submission)
command.add_command(submissions)

####### Boilerplate code
# Add the "templates" folder as a template root
tutor_hooks.Filters.ENV_TEMPLATE_ROOTS.add_item(
pkg_resources.resource_filename("tutorxqueue", "templates")
Expand Down Expand Up @@ -288,9 +319,10 @@ def request(self, endpoint, method="GET", data=None, params=None):
# Xqueue Public Host
########################################


@tutor_hooks.Filters.APP_PUBLIC_HOSTS.add()
def _xqueue_public_hosts(
hosts: list[str], context_name: t.Literal["local", "dev"]
hosts: list[str], context_name: Literal["local", "dev"]
) -> list[str]:
if context_name == "dev":
hosts += ["{{ XQUEUE_HOST }}:8000"]
Expand Down
Loading