From c5ecef952e67ea7c3c1d48514f70565948c2926e Mon Sep 17 00:00:00 2001 From: Billy Rhoades Date: Thu, 18 Jan 2018 17:29:11 -0600 Subject: [PATCH 01/12] Added travis config file. --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f078f18 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +language: python +python: + - "3.6" +install: + - pip install -r requirements.txt +script: + - pyflakes assigner/**/*.py assigner/*.py From b68adcb15ee488e77d037f3ca6d17fdc13703866 Mon Sep 17 00:00:00 2001 From: Billy Rhoades Date: Thu, 18 Jan 2018 17:38:13 -0600 Subject: [PATCH 02/12] Added pylint to requirements. Fixed travis call. --- .travis.yml | 3 ++- requirements.txt | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index f078f18..4537e27 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,5 @@ python: install: - pip install -r requirements.txt script: - - pyflakes assigner/**/*.py assigner/*.py + - pyflakes assigner + - pylint assigner diff --git a/requirements.txt b/requirements.txt index 2308a03..4ab98b7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ GitPython==1.0.1 PyYAML==3.11 colorlog==2.6.0 -flake8==2.5.1 gitdb==0.6.4 jsonschema==2.5.1 mccabe==0.3.1 @@ -14,3 +13,6 @@ smmap==0.9.0 wheel==0.24.0 PTable==0.9.2 progressbar2==3.10.1 +pylint==1.8.1 +wrapt==1.10.11 +python-utils==2.2.0 From 27dfa2f153df0e41f901a716ca8ccfc3d5a57bc0 Mon Sep 17 00:00:00 2001 From: Billy Rhoades Date: Thu, 18 Jan 2018 18:29:33 -0600 Subject: [PATCH 03/12] Resolved most pylint errors. --- assigner/__init__.py | 4 +- assigner/baserepo.py | 114 +++++++++++++++++++++--------------- assigner/canvas.py | 6 +- assigner/commands/assign.py | 40 +++++++------ assigner/commands/canvas.py | 40 ++++++++----- assigner/commands/get.py | 23 +++++--- assigner/commands/import.py | 5 +- assigner/commands/init.py | 18 ++++-- assigner/commands/new.py | 9 +-- assigner/commands/open.py | 12 ++-- assigner/commands/roster.py | 16 +++-- assigner/commands/status.py | 12 ++-- assigner/config.py | 2 +- assigner/progress.py | 1 + assigner/roster_util.py | 12 ++-- requirements.txt | 1 + 16 files changed, 188 insertions(+), 127 deletions(-) diff --git a/assigner/__init__.py b/assigner/__init__.py index 2b60c9b..88e9044 100755 --- a/assigner/__init__.py +++ b/assigner/__init__.py @@ -56,7 +56,7 @@ def manage_repos(conf, args, action): student_section = student["section"] if "id" not in student: logging.warning( - "Student {} does not have a gitlab account.".format(username) + "Student %s does not have a gitlab account.", username ) continue full_name = StudentRepo.name(semester, student_section, @@ -164,7 +164,7 @@ def main(): if args.tracebacks: raise e if isinstance(e, KeyError): - logger.error(str(e) + " is missing") + logger.error("%s is missing", e) else: logger.error(str(e)) raise SystemExit(1) from e diff --git a/assigner/baserepo.py b/assigner/baserepo.py index 019d0a5..f22cf01 100644 --- a/assigner/baserepo.py +++ b/assigner/baserepo.py @@ -4,7 +4,6 @@ import os import re import requests -import tempfile from enum import Enum from requests.exceptions import HTTPError @@ -67,13 +66,16 @@ def from_url(cls, url, token): namespace, name, token, url) logging.debug(json.dumps(self.info)) - logging.debug("{} is valid.".format(self.name_with_namespace)) + logging.debug("%s is valid.", self.name_with_namespace) return self @classmethod - def _cls_gl_get(cls, url_base, path, token, params={}): + def _cls_gl_get(cls, url_base, path, token, params=None): """Make a Gitlab GET request""" + if not params: + params = {} + params.update({"private_token": token}) url = urljoin(url_base, "/api/v4" + path) r = requests.get(url, params=params) @@ -81,8 +83,13 @@ def _cls_gl_get(cls, url_base, path, token, params={}): return r.json() @classmethod - def _cls_gl_post(cls, url_base, path, token, payload={}, params={}): + def _cls_gl_post(cls, url_base, path, token, payload=None, params=None): """Make a Gitlab POST request""" + if not params: + params = {} + if not payload: + payload = {} + params.update({"private_token": token}) url = urljoin(url_base, "/api/v4" + path) r = requests.post(url, params=params, data=payload) @@ -90,8 +97,13 @@ def _cls_gl_post(cls, url_base, path, token, payload={}, params={}): return r.json() @classmethod - def _cls_gl_put(cls, url_base, path, token, payload={}, params={}): + def _cls_gl_put(cls, url_base, path, token, payload=None, params=None): """Make a Gitlab PUT request""" + if not params: + params = {} + if not payload: + payload = {} + params.update({"private_token": token}) url = urljoin(url_base, "/api/v4" + path) r = requests.put(url, params=params, data=payload) @@ -99,8 +111,11 @@ def _cls_gl_put(cls, url_base, path, token, payload={}, params={}): return r.json() @classmethod - def _cls_gl_delete(cls, url_base, path, token, params={}): + def _cls_gl_delete(cls, url_base, path, token, params=None): """Make a Gitlab DELETE request""" + if not params: + params = {} + params.update({"private_token": token}) url = urljoin(url_base, "/api/v4" + path) r = requests.delete(url, params=params) @@ -145,7 +160,8 @@ def info(self): self._info = self._gl_get(url) except HTTPError as e: if e.response.status_code == 404: - logging.debug("Could not find repo with url {}/api/v4{}.".format(self.url_base,url)) + logging.debug("Could not find repo with url %s/api/v4%s.", + self.url_base, url) self._info = None else: raise @@ -178,7 +194,7 @@ def get_head(self, branch): if head.name == branch: return head - return self.repo.create_head(b, "origin/{}".format(b)) + return self.repo.create_head(branch, "origin/{}".format(branch)) def checkout(self, branch): return self.get_head(branch).checkout() @@ -190,18 +206,18 @@ def pull(self, branch): self.repo.remote().pull(branch) def clone_to(self, dir_name, branch=None): - logging.debug("Cloning {}...".format(self.ssh_url)) + logging.debug("Cloning %s...", self.ssh_url) try: if branch: self._repo = git.Repo.clone_from(self.ssh_url, dir_name, - branch=branch) + branch=branch) for b in branch: self._repo.create_head(b, "origin/{}".format(b)) logging.debug(self._repo.heads) else: self._repo = git.Repo.clone_from(self.ssh_url, dir_name) - logging.debug("Cloned {}.".format(self.name)) + logging.debug("Cloned %s.", self.name) except git.exc.GitCommandError as e: # GitPython may delete this directory # and the caller may have opinions about that, @@ -213,15 +229,15 @@ def clone_to(self, dir_name, branch=None): def add_local_copy(self, dir_name): if self.repo is not None: - logging.warn("You already have a local copy associated with this repo") + logging.warning("You already have a local copy associated with this repo") return - logging.debug("Using {} for the local repo...".format(dir_name)) + logging.debug("Using %s for the local repo...", dir_name) self._repo = git.Repo(dir_name) def delete(self): self._gl_delete("/projects/{}".format(self.id)) - logging.debug("Deleted {}.".format(self.name)) + logging.debug("Deleted %s.", self.name) # TODO: this should really go elsewhere @classmethod @@ -229,24 +245,24 @@ def get_user_id(cls, username, url_base, token): data = cls._cls_gl_get(url_base, "/users", token, params={"search": username}) - if len(data) == 0: - logging.warn( - "Did not find any users matching {}.".format(username) + if not data: + logging.warning( + "Did not find any users matching %s.", username ) raise RepoError("No user {}.".format(username)) for result in data: if result["username"] == username: logging.info( - "Got id {} for user {}.".format(data[0]["id"], username) + "Got id %s for user %s.", data[0]["id"], username ) return result["id"] # Fall back to first result if all else fails - logging.warn("Got {} users for {}.".format(len(data), username)) - logging.warn("Failed to find an exact match for {}.".format(username)) + logging.warning("Got %s users for %s.", len(data), username) + logging.warning("Failed to find an exact match for %s.", username) logging.info( - "Got id {} for user {}.".format(data[0]["id"], data[0]["username"]) + "Got id %s for user %s.", data[0]["id"], data[0]["username"] ) return data[0]["id"] @@ -332,32 +348,51 @@ def archive(self): def unarchive(self): return self._gl_post("/projects/{}/unarchive".format(self.id)) - def protect(self, branch="master", developer_push=True, developer_merge=True): # NOTE: these are not the same defaults that Gitlab uses + # NOTE: these are not the same defaults that Gitlab uses + def protect(self, branch="master", developer_push=True, developer_merge=True): params = { "developers_can_push": developer_push, "developers_can_merge": developer_merge, - } - return self._gl_put("/projects/{}/repository/branches/{}/protect".format(self.id, branch), params) + } + return self._gl_put("/projects/{}/repository/branches/{}/protect" + .format(self.id, branch), params) def unprotect(self, branch="master"): - return self._gl_put("/projects/{}/repository/branches/{}/unprotect".format(self.id, branch)) + return self._gl_put("/projects/{}/repository/branches/{}/unprotect" + .format(self.id, branch)) + + def _gl_get(self, path, params=None): + if not params: + params = {} - def _gl_get(self, path, params={}): return self.__class__._cls_gl_get( self.url_base, path, self.token, params ) - def _gl_post(self, path, payload={}, params={}): + def _gl_post(self, path, payload=None, params=None): + if not payload: + payload = {} + if not params: + params = {} + return self.__class__._cls_gl_post( self.url_base, path, self.token, payload ) - def _gl_put(self, path, payload={}, params={}): + def _gl_put(self, path, payload=None, params=None): + if not payload: + payload = {} + if not params: + params = {} + return self.__class__._cls_gl_put( self.url_base, path, self.token, payload ) - def _gl_delete(self, path, params={}): + def _gl_delete(self, path, params=None): + if not params: + params = {} + return self.__class__._cls_gl_delete( self.url_base, path, self.token, params ) @@ -370,11 +405,11 @@ def new(cls, name, namespace, url_base, token): namespaces = cls._cls_gl_get(url_base, "/namespaces", token, {"search": namespace}) logging.debug( - "Got {} namespaces matching {}.".format(len(namespaces), namespace) + "Got %s namespaces matching %s.", len(namespaces), namespace ) logging.debug( "Using namespace " + - "{} with ID {}.".format(namespaces[0]["path"], namespaces[0]["id"]) + "%s with ID %s.", namespaces[0]["path"], namespaces[0]["id"] ) payload = { @@ -396,7 +431,7 @@ def push_to(self, student_repo, branch="master"): r = git.Remote.add(self.repo, student_repo.name, student_repo.ssh_url) r.push(branch) - logging.debug("Pushed {} to {}.".format(self.name, student_repo.name)) + logging.debug("Pushed %s to %s.", self.name, student_repo.name) class StudentRepo(Repo): @@ -436,18 +471,3 @@ def name(cls, semester, section, assignment, user): def push(self, base_repo, branch="master"): """Push base_repo code to this repo""" base_repo.push_to(self, branch) - - -if __name__ == "__main__": - # Need to make this module if'n you're going to run this - from secret import token - - logging.basicConfig(level=logging.INFO) - - b = BaseRepo("https://git.mst.edu/2016-Spring-CS-2001/hw01.git", token) - print(json.dumps(b.info, indent=8)) - with tempfile.TemporaryDirectory() as tmpdirname: - b.clone_to(tmpdirname) - print(os.listdir(tmpdirname)) - - StudentRepo.new(b, "2016SP", "A", "mwwcp2", token) diff --git a/assigner/canvas.py b/assigner/canvas.py index 1b0b236..0ea0d33 100644 --- a/assigner/canvas.py +++ b/assigner/canvas.py @@ -25,7 +25,7 @@ def _request(self, method: str, url: str, params: dict = None, retries: int = 3) header = self.REQUEST_HEADER connection.request(method, url, (json.dumps(params) if params is not None else None), header) return connection.getresponse() - except Exception as ex: + except http.client.HTTPException as ex: tries += 1 if tries > retries: raise ex @@ -59,7 +59,7 @@ def _get_all_pages(self, url: str, params: dict = None) -> list: count = len(result) page = 1 while 'next' in links: - logging.debug("Got links:\n" + json.dumps(links, sort_keys=True, indent=2)) + logging.debug("Got links:\n%s", json.dumps(links, sort_keys=True, indent=2)) page += 1 next_url = url.split('?')[0] + '?page=%s&per_page=%s' % (page, count) logging.info("Getting next page for " + url + " via " + next_url) @@ -71,7 +71,7 @@ def _get_all_pages(self, url: str, params: dict = None) -> list: def get_instructor_courses(self): get = lambda x: self._get_all_pages('/api/v1/courses', - {'enrollment_type': x, 'state': ['available']}) + {'enrollment_type': x, 'state': ['available']}) result = get('teacher') result.extend(get('ta')) result.extend(get('grader')) diff --git a/assigner/commands/assign.py b/assigner/commands/assign.py index abf44ae..6b6e1dd 100644 --- a/assigner/commands/assign.py +++ b/assigner/commands/assign.py @@ -9,7 +9,7 @@ from requests.exceptions import HTTPError -help="Assign a base repo to students" +help = "Assign a base repo to students" logger = logging.getLogger(__name__) @@ -40,14 +40,16 @@ def assign(conf, args): print("Assigning '{}' to {} student{} in {}.".format( hw_name, student_count, "s" if student_count != 1 else "", - "section " + args.section if args.section else "all sections") - ) + "section " + args.section if args.section else "all sections" + )) base = BaseRepo(host, namespace, hw_name, token) if not dry_run: try: base.clone_to(tmpdirname, branch) except RepoError as e: - logging.error("Could not clone base repo (have you pushed at least one commit to it?)") + logging.error( + "Could not clone base repo (have you pushed at least one commit to it?)" + ) logging.debug(e) return @@ -72,18 +74,20 @@ def assign(conf, args): actual_count += 1 logging.debug("Assigned.") elif force: - logging.info("{}: Already exists, deleting...".format(full_name)) + logging.info("%s: Already exists, deleting...", full_name) if not dry_run: repo.delete() - # Gitlab will throw a 400 if you delete and immediately recreate a repo. - # We retry w/ exponential backoff up to 5 times + # Gitlab will throw a 400 if you delete and immediately + # recreate a repo. We retry w/ exponential backoff up + # to 5 times wait = 0.1 retries = 0 while True: try: - repo = StudentRepo.new(base, semester, student_section, - username, token) + repo = StudentRepo.new( + base, semester, student_section, username, token + ) logger.debug("Success!") break except HTTPError as e: @@ -102,7 +106,7 @@ def assign(conf, args): actual_count += 1 logging.debug("Assigned.") elif args.branch: - logging.info("{}: Already exists.".format(full_name)) + logging.info("%s: Already exists.", full_name) # If we have an explicit branch, push anyways if not dry_run: repo.push(base, branch) @@ -111,7 +115,7 @@ def assign(conf, args): actual_count += 1 logging.debug("Assigned.") else: - logging.info("{}: Already exists, skipping...".format(full_name)) + logging.info("%s: Already exists, skipping...", full_name) i += 1 progress.finish() @@ -129,16 +133,16 @@ def assign(conf, args): def setup_parser(parser): parser.add_argument("name", - help="Name of the assignment to assign.") + help="Name of the assignment to assign.") parser.add_argument("--branch", "--branches", nargs="+", - help="Branch or branches to push") + help="Branch or branches to push") parser.add_argument("--section", nargs="?", - help="Section to assign homework to") + help="Section to assign homework to") parser.add_argument("--student", metavar="id", - help="ID of the student to assign to.") + help="ID of the student to assign to.") parser.add_argument("--dry-run", action="store_true", - help="Don't actually do it.") + help="Don't actually do it.") parser.add_argument("-f", "--force", action="store_true", dest="force", - help="Delete and recreate already existing " + - "student repos.") + help="Delete and recreate already existing " + + "student repos.") parser.set_defaults(run=assign) diff --git a/assigner/commands/canvas.py b/assigner/commands/canvas.py index 24f00a3..cbbf126 100644 --- a/assigner/commands/canvas.py +++ b/assigner/commands/canvas.py @@ -1,24 +1,26 @@ import logging -from assigner.canvas import CanvasAPI -from assigner.config import config_context,DuplicateUserError - from prettytable import PrettyTable -from assigner.roster_util import add_to_roster from assigner import make_help_parser +from assigner.canvas import CanvasAPI +from assigner.config import config_context, DuplicateUserError +from assigner.roster_util import add_to_roster help = "Get Canvas course information" logger = logging.getLogger(__name__) + @config_context def import_from_canvas(conf, args): """Imports students from a Canvas course to the roster. """ if 'canvas-token' not in conf: - logger.error("canvas-token configuration is missing! Please set the Canvas API access " - "token before attempting to import users from Canvas") + logger.error( + "canvas-token configuration is missing! Please set the Canvas API access " + "token before attempting to import users from Canvas" + ) print("Import from canvas failed: missing Canvas API access token.") return @@ -35,22 +37,23 @@ def import_from_canvas(conf, args): for s in students: if 'sis_user_id' not in s: - logger.error("Could not get username for {}".format(s['sortable_name'])) + logger.error("Could not get username for %s", s['sortable_name']) try: add_to_roster(conf, conf.roster, s['sortable_name'], s['sis_user_id'], section, force) except DuplicateUserError: - logger.warning("User {} is already in the roster, skipping".format(s['sis_user_id'])) + logger.warning("User %s is already in the roster, skipping", s['sis_user_id']) print("Imported {} students.".format(len(students))) + @config_context -def print_canvas_courses(conf, args): +def print_canvas_courses(conf, _): """Show a list of current teacher's courses from Canvas via the API. """ if 'canvas-token' not in conf: logger.error("canvas-token configuration is missing! Please set the Canvas API access " - "token before attempting to use Canvas API functionality") + "token before attempting to use Canvas API functionality") print("Canvas course listing failed: missing Canvas API access token.") return @@ -70,16 +73,25 @@ def print_canvas_courses(conf, args): print(output) + def setup_parser(parser): subparsers = parser.add_subparsers(title='Canvas commands') - list_parser = subparsers.add_parser('list', help='List published Canvas courses where you are a teacher, TA, or grader') + list_parser = subparsers.add_parser( + "list", help="List published Canvas courses where you are a teacher, TA, or grader" + ) list_parser.set_defaults(run=print_canvas_courses) - import_parser = subparsers.add_parser('import', help='Import the roster from a specific Canvas course') + import_parser = subparsers.add_parser( + "import", help="Import the roster from a specific Canvas course" + ) import_parser.add_argument("id", help="Canvas ID for course to import from") import_parser.add_argument("section", help="Section being imported") - import_parser.add_argument("--force", action="store_true", help="Import duplicate students anyway") + import_parser.add_argument( + "--force", action="store_true", help="Import duplicate students anyway" + ) import_parser.set_defaults(run=import_from_canvas) - make_help_parser(parser, subparsers, "Show help for roster or one of its commands") + make_help_parser( + parser, subparsers, "Show help for roster or one of its commands" + ) diff --git a/assigner/commands/get.py b/assigner/commands/get.py index 0309a1b..034e5da 100644 --- a/assigner/commands/get.py +++ b/assigner/commands/get.py @@ -70,17 +70,23 @@ def get(conf, args): result.note ) - # see http://gitpython.readthedocs.io/en/stable/reference.html#git.remote.FetchInfo + # see: + # http://gitpython.readthedocs.io/en/stable/reference.html#git.remote.FetchInfo if result.flags & result.NEW_HEAD: - output.add_row([row, sec, sid, name, "{}: new branch at {}".format( - result.ref.name, str(result.ref.commit)[:8] - )]) + output.add_row([ + row, sec, sid, name, "{}: new branch at {}".format( + result.ref.name, str(result.ref.commit)[:8] + ) + ]) row = sec = sid = name = "" # don't print user info more than once elif result.old_commit is not None: - output.add_row([row, sec, sid, name, "{}: {} -> {}".format( - result.ref.name, str(result.old_commit)[:8], str(result.ref.commit)[:8] - )]) + output.add_row([ + row, sec, sid, name, "{}: {} -> {}".format( + result.ref.name, str(result.old_commit)[:8], + str(result.ref.commit)[:8] + ) + ]) row = sec = sid = name = "" logging.debug("Pulling specified branches...") @@ -90,7 +96,8 @@ def get(conf, args): repo.pull(b) except GitCommandError as e: logging.debug(e) - logging.warning("Local changes to %s/%s would be overwritten by pull", username, b) + logging.warning("Local changes to %s/%s would be overwritten by pull", + username, b) logging.warning(" (use --force to overwrite)") # Check out first branch specified; this is probably what people expect diff --git a/assigner/commands/import.py b/assigner/commands/import.py index 260cb5f..61332a6 100644 --- a/assigner/commands/import.py +++ b/assigner/commands/import.py @@ -5,7 +5,7 @@ from assigner.config import config_context, DuplicateUserError from assigner.roster_util import add_to_roster -help="Import users from a csv" +help = "Import users from a csv" logger = logging.getLogger(__name__) @@ -35,10 +35,11 @@ def import_students(conf, args): try: add_to_roster(conf, conf.roster, row[3], match.group("user"), section, args.force) except DuplicateUserError: - logger.warning("User {} is already in the roster, skipping".format(match.group("user"))) + logger.warning("User %s is already in the roster, skipping", match.group("user")) print("Imported {} students.".format(count)) + def setup_parser(parser): parser.add_argument("file", help="CSV file to import from") parser.add_argument("section", help="Section being imported") diff --git a/assigner/commands/init.py b/assigner/commands/init.py index 5af44fd..0cb18c5 100644 --- a/assigner/commands/init.py +++ b/assigner/commands/init.py @@ -3,10 +3,11 @@ from assigner.config import config_context -help="Interactively initialize a new configuration" +help = "Interactively initialize a new configuration" logger = logging.getLogger(__name__) + def prompt(explanation, default=None): prompt_string = '' if default is not None: @@ -23,6 +24,7 @@ def prompt(explanation, default=None): return value + def guess_semester(): now = datetime.datetime.now() if now.month < 5: @@ -36,17 +38,21 @@ def guess_semester(): @config_context -def init(conf, args): +def init(conf, _): conf['gitlab-host'] = "https://{}".format(prompt("Gitlab server to use", "gitlab.com")) - conf['token'] = prompt("Gitlab access token (from {}/profile/personal_access_tokens)".format(conf['gitlab-host'])) + conf['token'] = prompt("Gitlab access token (from {}/profile/personal_access_tokens)" + .format(conf['gitlab-host'])) conf['semester'] = prompt("Year and semester, in the format YYYY-(FS|SP|SS)", guess_semester()) - conf['namespace'] = prompt("Gitlab group to create repositories under", "{}-CS1001".format(conf['semester'])) + conf['namespace'] = prompt("Gitlab group to create repositories under", "{}-CS1001" + .format(conf['semester'])) do_canvas = input("Do you want to configure Canvas integration? [y/N]: ") if do_canvas.lower() == 'y': conf['canvas-host'] = prompt("Canvas server to use (???.instructure.com)") - conf['canvas-token'] = prompt("Canvas access token (from {}/profile/settings)".format(conf['canvas-host'])) + conf['canvas-token'] = prompt("Canvas access token (from {}/profile/settings)" + .format(conf['canvas-host'])) print("Congratulations, you're ready to go!") + def setup_parser(parser): - parser.set_defaults(run=init) \ No newline at end of file + parser.set_defaults(run=init) diff --git a/assigner/commands/new.py b/assigner/commands/new.py index aa9e046..72a27eb 100644 --- a/assigner/commands/new.py +++ b/assigner/commands/new.py @@ -23,21 +23,22 @@ def new(conf, args): if dry_run: url = Repo.build_url(host, namespace, hw_name) - print("Created repo for {}:\n\t{}\n\t{}".format(hw_name, url, "(ssh url not available)")) + print( + "Created repo for {}:\n\t{}\n\t{}".format(hw_name, url, "(ssh url not available)")) else: try: repo = BaseRepo.new(hw_name, namespace, host, token) print("Created repo for {}:\n\t{}\n\t{}".format(hw_name, repo.url, repo.ssh_url)) except HTTPError as e: if e.response.status_code == 400: - logger.warning("Repository {} already exists!".format(hw_name)) + logger.warning("Repository %s already exists!", hw_name) else: raise def setup_parser(parser): parser.add_argument("name", - help="Name of the assignment.") + help="Name of the assignment.") parser.add_argument("--dry-run", action="store_true", - help="Don't actually do it.") + help="Don't actually do it.") parser.set_defaults(run=new) diff --git a/assigner/commands/open.py b/assigner/commands/open.py index 64d6613..fd0ff2b 100644 --- a/assigner/commands/open.py +++ b/assigner/commands/open.py @@ -7,7 +7,7 @@ from assigner.config import config_context from assigner.progress import Progress -help="Grants students access to their repos" +help = "Grants students access to their repos" logger = logging.getLogger(__name__) @@ -41,7 +41,7 @@ def open_assignment(conf, args): repo.add_member(student["id"], Access.developer) count += 1 except RepoError: - logging.warn("Could not add {} to {}.".format(username, full_name)) + logging.warning("Could not add %s to %s.", username, full_name) except HTTPError: raise @@ -51,10 +51,10 @@ def open_assignment(conf, args): def setup_parser(parser): - parser.add_argument("name", help="Name of the assignment to grant " + - "access to") + parser.add_argument("name", + help="Name of the assignment to grant access to") parser.add_argument("--section", nargs="?", - help="Section to grant access to") + help="Section to grant access to") parser.add_argument("--student", metavar="id", - help="ID of the student to assign to.") + help="ID of the student to assign to.") parser.set_defaults(run=open_assignment) diff --git a/assigner/commands/roster.py b/assigner/commands/roster.py index ee8b813..a03fb89 100644 --- a/assigner/commands/roster.py +++ b/assigner/commands/roster.py @@ -1,24 +1,28 @@ import logging -from assigner.config import config_context, DuplicateUserError from prettytable import PrettyTable -from assigner.roster_util import get_filtered_roster, add_to_roster + from assigner import make_help_parser +from assigner.config import config_context, DuplicateUserError +from assigner.roster_util import get_filtered_roster, add_to_roster + help = "Manage class roster" logger = logging.getLogger(__name__) + @config_context def list_students(conf, args): """List students in the roster """ output = PrettyTable(["#", "Name", "Username", "Section"]) - for idx,student in enumerate(get_filtered_roster(conf.roster, args.section, None)): + for idx, student in enumerate(get_filtered_roster(conf.roster, args.section, None)): output.add_row((idx+1, student["name"], student["username"], student["section"])) print(output) + @config_context def add_student(conf, args): """Add a student to the roster @@ -28,6 +32,7 @@ def add_student(conf, args): except DuplicateUserError: logger.error("Student already exists in roster!") + @config_context def remove_student(conf, args): """Remove a student from the roster @@ -44,7 +49,8 @@ def remove_student(conf, args): del conf.roster[idx - offset] offset += 1 - logger.info("Removed {} entries from the roster".format(previous_len - len(conf.roster))) + logger.info("Removed %d entries from the roster", previous_len - len(conf.roster)) + def setup_parser(parser): subparsers = parser.add_subparsers(title='Roster commands') @@ -65,4 +71,4 @@ def setup_parser(parser): remove_parser.add_argument("username", help="Username of student to remove") remove_parser.set_defaults(run=remove_student) - make_help_parser(parser, subparsers, "Show help for roster or one of its commands") \ No newline at end of file + make_help_parser(parser, subparsers, "Show help for roster or one of its commands") diff --git a/assigner/commands/status.py b/assigner/commands/status.py index b4173a1..8a6d338 100644 --- a/assigner/commands/status.py +++ b/assigner/commands/status.py @@ -9,7 +9,7 @@ from assigner.config import config_context from assigner.baserepo import Access, Repo, StudentRepo, RepoError -help="Retrieve status of repos" +help = "Retrieve status of repos" logger = logging.getLogger(__name__) @@ -106,12 +106,12 @@ def status(conf, args): def setup_parser(parser): parser.add_argument("--section", nargs="?", - help="Section to get status of") + help="Section to get status of") parser.add_argument("--student", metavar="id", - help="ID of student.") + help="ID of student.") parser.add_argument("--sort", nargs="?", default="name", - choices=["name", "username"], - help="Key to sort users by.") + choices=["name", "username"], + help="Key to sort users by.") parser.add_argument("name", nargs="?", - help="Name of the assignment to look up.") + help="Name of the assignment to look up.") parser.set_defaults(run=status) diff --git a/assigner/config.py b/assigner/config.py index b551fd8..11bdc12 100644 --- a/assigner/config.py +++ b/assigner/config.py @@ -118,7 +118,7 @@ def __init__(self, filename): except FileNotFoundError: pass # Just make an empty config; create on __exit__() except jsonschema.ValidationError as e: - logging.warning("Your configuration is not valid: " + e.message) + logging.warning("Your configuration is not valid: %s", e.message) def __enter__(self): return self diff --git a/assigner/progress.py b/assigner/progress.py index 597ff96..137c303 100644 --- a/assigner/progress.py +++ b/assigner/progress.py @@ -2,6 +2,7 @@ import sys from tqdm import tqdm + # Some of the following is borrowed straight from # https://github.com/tqdm/tqdm/blob/master/examples/redirect_print.py class DummyTqdmFile(object): diff --git a/assigner/roster_util.py b/assigner/roster_util.py index 22df62d..5c401d8 100644 --- a/assigner/roster_util.py +++ b/assigner/roster_util.py @@ -2,8 +2,10 @@ from assigner.baserepo import Repo, RepoError import logging + logger = logging.getLogger(__name__) + def get_filtered_roster(roster, section, target): if target: roster = [s for s in roster if s["username"] == target] @@ -13,6 +15,7 @@ def get_filtered_roster(roster, section, target): raise ValueError("No matching students found in roster.") return roster + def add_to_roster(conf, roster, name, username, section, force=False): student = { "name": name, @@ -20,10 +23,10 @@ def add_to_roster(conf, roster, name, username, section, force=False): "section": section } - logger.debug("{}".format(roster)) + logger.debug("%s", roster) - if not force and any([s['username'] == username for s in roster]): - raise DuplicateUserError("Student already exists in roster!") + if not force and any(filter(lambda s: s['username'] == username, roster)): + raise DuplicateUserError("Student already exists in roster!") try: student["id"] = Repo.get_user_id( @@ -31,8 +34,7 @@ def add_to_roster(conf, roster, name, username, section, force=False): ) except RepoError: logger.warning( - "Student {} does not have a Gitlab account.".format(name) + "Student %s does not have a Gitlab account.", name ) roster.append(student) - diff --git a/requirements.txt b/requirements.txt index 4ab98b7..4cf7468 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,3 +16,4 @@ progressbar2==3.10.1 pylint==1.8.1 wrapt==1.10.11 python-utils==2.2.0 +tqdm==4.19.5 From 47c32aca05fb9064e479115bfe280f4746915e37 Mon Sep 17 00:00:00 2001 From: Billy Rhoades Date: Thu, 18 Jan 2018 18:40:32 -0600 Subject: [PATCH 04/12] StudentRepo.name -> StudentRepo.build_name name was being shadowed by an instance var. --- assigner/__init__.py | 4 ++-- assigner/baserepo.py | 2 +- assigner/commands/assign.py | 4 ++-- assigner/commands/get.py | 4 ++-- assigner/commands/open.py | 4 ++-- assigner/commands/status.py | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/assigner/__init__.py b/assigner/__init__.py index 88e9044..d4b0416 100755 --- a/assigner/__init__.py +++ b/assigner/__init__.py @@ -59,8 +59,8 @@ def manage_repos(conf, args, action): "Student %s does not have a gitlab account.", username ) continue - full_name = StudentRepo.name(semester, student_section, - hw_name, username) + full_name = StudentRepo.build_name(semester, student_section, + hw_name, username) try: repo = StudentRepo(host, namespace, full_name, token) diff --git a/assigner/baserepo.py b/assigner/baserepo.py index f22cf01..ba2d90b 100644 --- a/assigner/baserepo.py +++ b/assigner/baserepo.py @@ -457,7 +457,7 @@ def new(cls, base_repo, semester, section, username, token): return cls.from_url(result["http_url_to_repo"], token) @classmethod - def name(cls, semester, section, assignment, user): + def build_name(cls, semester, section, assignment, user): fmt = { "semester": semester, "section": section, diff --git a/assigner/commands/assign.py b/assigner/commands/assign.py index 6b6e1dd..2317bde 100644 --- a/assigner/commands/assign.py +++ b/assigner/commands/assign.py @@ -60,8 +60,8 @@ def assign(conf, args): for i, student in progress.enumerate(roster): username = student["username"] student_section = student["section"] - full_name = StudentRepo.name(semester, student_section, - hw_name, username) + full_name = StudentRepo.build_name(semester, student_section, + hw_name, username) repo = StudentRepo(host, namespace, full_name, token) if not repo.already_exists(): diff --git a/assigner/commands/get.py b/assigner/commands/get.py index 034e5da..7f9aeb4 100644 --- a/assigner/commands/get.py +++ b/assigner/commands/get.py @@ -44,8 +44,8 @@ def get(conf, args): for i, student in progress.enumerate(roster): username = student["username"] student_section = student["section"] - full_name = StudentRepo.name(semester, student_section, - hw_name, username) + full_name = StudentRepo.build_name(semester, student_section, + hw_name, username) try: repo = StudentRepo(host, namespace, full_name, token) diff --git a/assigner/commands/open.py b/assigner/commands/open.py index fd0ff2b..932c465 100644 --- a/assigner/commands/open.py +++ b/assigner/commands/open.py @@ -30,8 +30,8 @@ def open_assignment(conf, args): for student in progress.iterate(roster): username = student["username"] student_section = student["section"] - full_name = StudentRepo.name(semester, student_section, - hw_name, username) + full_name = StudentRepo.build_name(semester, student_section, + hw_name, username) try: repo = StudentRepo(host, namespace, full_name, token) diff --git a/assigner/commands/status.py b/assigner/commands/status.py index 8a6d338..57523a4 100644 --- a/assigner/commands/status.py +++ b/assigner/commands/status.py @@ -45,8 +45,8 @@ def status(conf, args): name = student["name"] username = student["username"] student_section = student["section"] - full_name = StudentRepo.name(semester, student_section, - hw_name, username) + full_name = StudentRepo.build_name(semester, student_section, + hw_name, username) row = [i+1, student_section, username, name, "", "", "", "", ""] From 1d58edba0e2fda683dad46040a1bf162657ca7ff Mon Sep 17 00:00:00 2001 From: Billy Rhoades Date: Thu, 18 Jan 2018 18:49:09 -0600 Subject: [PATCH 05/12] Removed FIXME for something that was fixed, created #95 for the other. --- assigner/baserepo.py | 3 +-- assigner/commands/import.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/assigner/baserepo.py b/assigner/baserepo.py index ba2d90b..c473bc2 100644 --- a/assigner/baserepo.py +++ b/assigner/baserepo.py @@ -239,7 +239,6 @@ def delete(self): self._gl_delete("/projects/{}".format(self.id)) logging.debug("Deleted %s.", self.name) - # TODO: this should really go elsewhere @classmethod def get_user_id(cls, username, url_base, token): data = cls._cls_gl_get(url_base, "/users", token, @@ -441,7 +440,7 @@ class StudentRepo(Repo): def new(cls, base_repo, semester, section, username, token): """Create a new repository on GitLab""" payload = { - "name": cls.name(semester, section, base_repo.name, username), + "name": cls.build_name(semester, section, base_repo.name, username), "namespace_id": base_repo.namespace_id, "issues_enabled": False, "merge_requests_enabled": False, diff --git a/assigner/commands/import.py b/assigner/commands/import.py index 61332a6..e04a5e4 100644 --- a/assigner/commands/import.py +++ b/assigner/commands/import.py @@ -16,7 +16,6 @@ def import_students(conf, args): """ section = args.section - # TODO: This should probably move to another file email_re = re.compile(r"^(?P[^@]+)") with open(args.file) as fh: reader = csv.reader(fh) From 2936b44a120d77f04fec7f2870671125b8f9bf79 Mon Sep 17 00:00:00 2001 From: Billy Rhoades Date: Thu, 18 Jan 2018 18:58:38 -0600 Subject: [PATCH 06/12] Small changes after a second pass. --- assigner/canvas.py | 4 ++-- assigner/commands/assign.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assigner/canvas.py b/assigner/canvas.py index 0ea0d33..dfbcdf0 100644 --- a/assigner/canvas.py +++ b/assigner/canvas.py @@ -25,10 +25,10 @@ def _request(self, method: str, url: str, params: dict = None, retries: int = 3) header = self.REQUEST_HEADER connection.request(method, url, (json.dumps(params) if params is not None else None), header) return connection.getresponse() - except http.client.HTTPException as ex: + except http.client.HTTPException: tries += 1 if tries > retries: - raise ex + raise logging.warning("Caught exception in request after %d tries. Will retry %d more times.", tries, retries - tries, exc_info=True) time.sleep(1) diff --git a/assigner/commands/assign.py b/assigner/commands/assign.py index 2317bde..d097a0b 100644 --- a/assigner/commands/assign.py +++ b/assigner/commands/assign.py @@ -143,6 +143,6 @@ def setup_parser(parser): parser.add_argument("--dry-run", action="store_true", help="Don't actually do it.") parser.add_argument("-f", "--force", action="store_true", dest="force", - help="Delete and recreate already existing " + + help="Delete and recreate already existing " "student repos.") parser.set_defaults(run=assign) From 167912a2134856633639346db6a7d6137137dd27 Mon Sep 17 00:00:00 2001 From: Billy Rhoades Date: Thu, 18 Jan 2018 19:00:16 -0600 Subject: [PATCH 07/12] Updated changelog. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 21b9d93..fb9309b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ ## Devel +- Removed lint and added travis config. + ## 1.0.0 - Rename `token` to `gitlab-token` in the configuration file From 5c3fd318979e28867f2cab5e15112a1295bacf0e Mon Sep 17 00:00:00 2001 From: Billy Rhoades Date: Thu, 18 Jan 2018 19:11:32 -0600 Subject: [PATCH 08/12] Updated changelog (again). --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb9309b..b3700ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ ## Devel -- Removed lint and added travis config. +- Removed lint, removed old baserepo standalone code, and added travis config. ## 1.0.0 From 51f43518a60bb2f3b26a9801c46b96bf3f5bef30 Mon Sep 17 00:00:00 2001 From: Billy Rhoades Date: Fri, 19 Jan 2018 16:47:32 -0600 Subject: [PATCH 09/12] Restored ={} default params, split changelog. --- CHANGELOG.md | 4 +++- assigner/baserepo.py | 48 ++++++++------------------------------------ 2 files changed, 11 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b3700ea..0f8b25f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ ## Devel -- Removed lint, removed old baserepo standalone code, and added travis config. +- Removed remaining lint as specified by pylint. +- Removed old baserepo standalone code. +- Added Travis CI config for pylint and pyflakes. ## 1.0.0 diff --git a/assigner/baserepo.py b/assigner/baserepo.py index c473bc2..6b71bcf 100644 --- a/assigner/baserepo.py +++ b/assigner/baserepo.py @@ -71,11 +71,8 @@ def from_url(cls, url, token): return self @classmethod - def _cls_gl_get(cls, url_base, path, token, params=None): + def _cls_gl_get(cls, url_base, path, token, params={}): """Make a Gitlab GET request""" - if not params: - params = {} - params.update({"private_token": token}) url = urljoin(url_base, "/api/v4" + path) r = requests.get(url, params=params) @@ -83,13 +80,8 @@ def _cls_gl_get(cls, url_base, path, token, params=None): return r.json() @classmethod - def _cls_gl_post(cls, url_base, path, token, payload=None, params=None): + def _cls_gl_post(cls, url_base, path, token, payload={}, params={}): """Make a Gitlab POST request""" - if not params: - params = {} - if not payload: - payload = {} - params.update({"private_token": token}) url = urljoin(url_base, "/api/v4" + path) r = requests.post(url, params=params, data=payload) @@ -97,13 +89,8 @@ def _cls_gl_post(cls, url_base, path, token, payload=None, params=None): return r.json() @classmethod - def _cls_gl_put(cls, url_base, path, token, payload=None, params=None): + def _cls_gl_put(cls, url_base, path, token, payload={}, params={}): """Make a Gitlab PUT request""" - if not params: - params = {} - if not payload: - payload = {} - params.update({"private_token": token}) url = urljoin(url_base, "/api/v4" + path) r = requests.put(url, params=params, data=payload) @@ -111,11 +98,8 @@ def _cls_gl_put(cls, url_base, path, token, payload=None, params=None): return r.json() @classmethod - def _cls_gl_delete(cls, url_base, path, token, params=None): + def _cls_gl_delete(cls, url_base, path, token, params={}): """Make a Gitlab DELETE request""" - if not params: - params = {} - params.update({"private_token": token}) url = urljoin(url_base, "/api/v4" + path) r = requests.delete(url, params=params) @@ -360,38 +344,22 @@ def unprotect(self, branch="master"): return self._gl_put("/projects/{}/repository/branches/{}/unprotect" .format(self.id, branch)) - def _gl_get(self, path, params=None): - if not params: - params = {} - + def _gl_get(self, path, params={}): return self.__class__._cls_gl_get( self.url_base, path, self.token, params ) - def _gl_post(self, path, payload=None, params=None): - if not payload: - payload = {} - if not params: - params = {} - + def _gl_post(self, path, payload={}, params={}): return self.__class__._cls_gl_post( self.url_base, path, self.token, payload ) - def _gl_put(self, path, payload=None, params=None): - if not payload: - payload = {} - if not params: - params = {} - + def _gl_put(self, path, payload={}, params={}): return self.__class__._cls_gl_put( self.url_base, path, self.token, payload ) - def _gl_delete(self, path, params=None): - if not params: - params = {} - + def _gl_delete(self, path, params={}): return self.__class__._cls_gl_delete( self.url_base, path, self.token, params ) From ff4477f2aca78e39ba42a6d3dc8a29a9ab1beab9 Mon Sep 17 00:00:00 2001 From: Billy Rhoades Date: Fri, 19 Jan 2018 16:50:23 -0600 Subject: [PATCH 10/12] Added pylint ignore for ={}, cleaned up unused params. --- assigner/baserepo.py | 4 ++-- pylintrc | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/assigner/baserepo.py b/assigner/baserepo.py index 6b71bcf..26b3112 100644 --- a/assigner/baserepo.py +++ b/assigner/baserepo.py @@ -349,12 +349,12 @@ def _gl_get(self, path, params={}): self.url_base, path, self.token, params ) - def _gl_post(self, path, payload={}, params={}): + def _gl_post(self, path, payload={}, _=None): return self.__class__._cls_gl_post( self.url_base, path, self.token, payload ) - def _gl_put(self, path, payload={}, params={}): + def _gl_put(self, path, payload={}, _=None): return self.__class__._cls_gl_put( self.url_base, path, self.token, payload ) diff --git a/pylintrc b/pylintrc index 0928e1f..2e265ae 100644 --- a/pylintrc +++ b/pylintrc @@ -143,6 +143,7 @@ disable=print-statement, too-many-statements, attribute-defined-outside-init, redefined-builtin, + dangerous-default-value, From 4bd307154ae3c5c5089b4b1b8d73acf8fbd86d67 Mon Sep 17 00:00:00 2001 From: Billy Rhoades Date: Fri, 19 Jan 2018 16:53:45 -0600 Subject: [PATCH 11/12] Added requirements.txt information to CONTRIBUTING. --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 72236dc..b269f2d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,6 +21,7 @@ Before merging: - Describe the change in `CHANGELOG.md` - Make sure `pyflakes assigner` passes with no errors/warnings - Check `pylint assigner` and make sure it's not too egregious. (Eventually we'll get the code to a point where there are no messages from `pylint`...) +- Update requirements.txt with any new depedencies. ## Installation for Developing From 6f07c75008f2f059c4c71df2c6484d5d451db216 Mon Sep 17 00:00:00 2001 From: Billy Rhoades Date: Fri, 19 Jan 2018 21:34:48 -0600 Subject: [PATCH 12/12] Passing params that were unused through in baserepo, updated changelog. Made dangerous-default-args a warning again. --- CONTRIBUTING.md | 4 ++-- assigner/baserepo.py | 9 +++++---- pylintrc | 1 - 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b269f2d..169dd1b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,8 +20,8 @@ Before merging: - Describe the change in `CHANGELOG.md` - Make sure `pyflakes assigner` passes with no errors/warnings -- Check `pylint assigner` and make sure it's not too egregious. (Eventually we'll get the code to a point where there are no messages from `pylint`...) -- Update requirements.txt with any new depedencies. +- Check `pylint assigner` and make sure there are no new errors/warnings. +- Update requirements.txt with any new dependencies. ## Installation for Developing diff --git a/assigner/baserepo.py b/assigner/baserepo.py index 26b3112..3d73dc3 100644 --- a/assigner/baserepo.py +++ b/assigner/baserepo.py @@ -1,3 +1,4 @@ +#pylint: disable=dangerous-default-value import git import json import logging @@ -349,14 +350,14 @@ def _gl_get(self, path, params={}): self.url_base, path, self.token, params ) - def _gl_post(self, path, payload={}, _=None): + def _gl_post(self, path, payload={}, params={}): return self.__class__._cls_gl_post( - self.url_base, path, self.token, payload + self.url_base, path, self.token, payload, params ) - def _gl_put(self, path, payload={}, _=None): + def _gl_put(self, path, payload={}, params={}): return self.__class__._cls_gl_put( - self.url_base, path, self.token, payload + self.url_base, path, self.token, payload, params ) def _gl_delete(self, path, params={}): diff --git a/pylintrc b/pylintrc index 2e265ae..0928e1f 100644 --- a/pylintrc +++ b/pylintrc @@ -143,7 +143,6 @@ disable=print-statement, too-many-statements, attribute-defined-outside-init, redefined-builtin, - dangerous-default-value,