Source code for abcclassroom.template

"""
abc-classroom.template
======================

"""
import os
import shutil
from pathlib import Path

from . import config as cf
from . import git as abcgit
from . import github as abcgithub
from . import auth
from . import utils


[docs]def new_update_template(args): """ Command line function that creates or updates an assignment template repository. Implementation of both the new_template and update_template console scripts (which perform the same basic functions but with different command line arguments and defaults). Creates an assignment entry in the config file if one does not already exist. Parameters ---------- args : command line arguments """ try: create_template( mode=args.mode, push_to_github=args.github, commit_message=args.commit_message, assignment_name=args.assignment, ) except FileNotFoundError as fnfe: # if the assignment does not exist in course_materials/release print(fnfe) except FileExistsError as fee: # if mode = fail and assignment repository already exists print(fee)
[docs]def create_template( assignment_name, mode="fail", push_to_github=False, commit_message="Initial commit", ): """ Classroom package function that creates or updates an assignment template repository. Implementation of both the new_template and update_template console scripts (which perform the same basic functions but with different command line arguments and defaults). Creates an assignment directory in template_dir if it does not exist already. Copies files from course_materials/release and from extra_files, initializes as a git repo, and commits all changes. Creates an assignment entry in the config file if one does not already exist. If push_to_github = True, then also pushes to github (creating the remote repository if it does not already exist). Parameters ---------- push_to_github : boolean True if you want to push to GH mode : merge, fail commit_message : string Sets a custom git commit message. For new templates, default is "Initial commit" and for updating templates, default is "Updating assignment" assignment_name : string name of the assignment """ print("Loading configuration from config.yml") try: config = cf.get_config() except (FileNotFoundError, RuntimeError) as err: print(err) return # Set up the path to the assignment files, which are in # course_dir/materials_dir/release/assignment_name course_dir = cf.get_config_option(config, "course_directory", True) materials_dir = cf.get_config_option(config, "course_materials", True) # I think this should be moved above where it created the directory parent_path = utils.get_abspath(materials_dir, course_dir) release_path = Path(parent_path, "release", assignment_name) # Check to see if there is an assignment with that name in the # release directory, if not, fail gracefully try: release_path.resolve(strict=True) except FileNotFoundError as e: print(e) raise FileNotFoundError( "Oops, it looks like the assignment - {} - does not exist " "in the location that I expected it: \n{}. \nDid " "you spell the assignment name correctly and is there a " "directory at this path?".format(assignment_name, release_path) ) # If the assignment exists, then create directory template_repo_path = create_template_dir(config, assignment_name, mode) # and copy files copy_files_to_template_repo( config, template_repo_path, assignment_name, release_path ) # Create the local git repository and commit changes abcgit.init_and_commit(template_repo_path, commit_message) # Create / append assignment entry in config - this should only happen if # the assignment above exists... print("Updating assignment list in config") course_dir = cf.get_config_option(config, "course_directory", True) cf.set_config_option( config, "assignments", assignment_name, append_value=True, configpath=course_dir, ) # Optional - push files to GitHub if push_to_github: organization = cf.get_config_option(config, "organization", True) repo_name = os.path.basename(template_repo_path) token = auth.get_github_auth()["access_token"] create_or_update_remote( template_repo_path, organization, repo_name, token )
[docs]def create_or_update_remote( template_repo_path, organization, repo_name, token ): """ Push template repo to GitHub. If remote does not yet exist, creates it before pushing. Parameters ---------- template_repo_path : string The path to the template repo on your local computer. organization : string The name of the organization where your GitHub Classroom lives. repo_name : string The name of the template repository to create on GitHub token : github token Used to authenticate with GitHub via the API. Created by running ``abc-init`` """ remote_exists = abcgithub.remote_repo_exists( organization, repo_name, token ) if not remote_exists: print("Creating remote repo {}".format(repo_name)) # create the remote repo on github and push the local repo # (will print error and return if repo already exists) abcgithub.create_repo(organization, repo_name, token) try: abcgit.add_remote(template_repo_path, organization, repo_name) except RuntimeError: print("Remote already added to local repository.") pass print("Pushing any changes to remote repository on GitHub.") try: abcgit.push_to_github(template_repo_path, "main") except RuntimeError as e: print( """Push to github failed. This is usually because there are changes on the remote that you do not have locally. Here is the git error:""" ) print(e)
[docs]def create_template_dir(config, assignment, mode="fail"): """ Creates a new directory in template_dir that will become the template repository for the assignment. If directory exists and mode is merge, do nothing. If directory exists and mode is delete, remove contents but leave .git directory. """ course_dir = cf.get_config_option(config, "course_directory", True) template_parent_dir = cf.get_config_option(config, "template_dir", True) parent_path = Path(utils.get_abspath(template_parent_dir, course_dir)) # check that parent directory for templates exists, and create it # if it does not if not parent_path.is_dir(): print( "Creating new directory for template repos at {}".format( parent_path.relative_to(course_dir) ) ) parent_path.mkdir() repo_name = assignment + "-template" template_path = Path(parent_path, repo_name) dir_exists = template_path.is_dir() if not dir_exists: template_path.mkdir() print( "Creating new template repo at {}".format( template_path.relative_to(course_dir) ) ) else: if mode == "fail": raise FileExistsError( "Oops! The directory specified: {} already exists " "for this course; " "re-run with --mode merge' or '--mode delete', " "or delete / move directory before re-running" ". ".format(template_path.relative_to(course_dir)) ) elif mode == "merge": print( "The directory specified: {} already exists for this" " course; will keep directory but overwrite existing files " "with same names".format(template_path.relative_to(course_dir)) ) else: # mode == delete print( """Directory {} already exists for this course; deleting existing files but keeping .git directory, if it exists.""".format( template_path.relative_to(course_dir) ) ) # Temporarily move the .git dir to the parent of the # template_path (i.e. the template_repos dir in the config) # We do this to avoid issues if the local repo has already been # pushed to github (if we re-create a new repo, will get error # about unrelated histories when pushing) gitdir = Path(template_path, ".git") if gitdir.exists(): target = Path(Path(template_path).parent, ".tempgit") gitdir.replace(target) # remove template_path and re-create with same name shutil.rmtree(template_path) Path(template_path).mkdir() # and then move the .git dir back target.replace(gitdir) else: # remove template_path and re-create with same name shutil.rmtree(template_path) Path(template_path).mkdir() return template_path
[docs]def copy_files_to_template_repo( config, template_repo_path, assignment, release_path ): """ Copies files and directories for the assignment recursively into the local template repository. Looks in release_dir and extra_files for files to copy. If extra_files contains a readme, updates the readme with the assignment name. Excludes files and directories that match patterns in files_to_ignore. """ # get config options files_to_ignore = cf.get_config_option(config, "files_to_ignore", False) course_dir = cf.get_config_option(config, "course_directory", True) # copy assignment-specific files utils.copy_files(release_path, template_repo_path, files_to_ignore) # copy extra_files extra_files_path = Path(course_dir, "extra_files") try: utils.copy_files(extra_files_path, template_repo_path, files_to_ignore) # and add the assignment name to the readme, if it exists readme_path = Path(template_repo_path, "README.md") if readme_path.exists(): add_assignment_to_readme(readme_path, assignment) except FileNotFoundError: print("No extra_files directory found") pass
[docs]def add_assignment_to_readme(path_to_readme, assignment): with open(path_to_readme) as readme: lines = readme.readlines() if len(lines) > 0: lines[0] = "# Assignment {}\n".format(assignment) utils.write_file(path_to_readme, lines)