Skip to content
Snippets Groups Projects
check 4.94 KiB
Newer Older
Raniere Silva's avatar
Raniere Silva committed
#!/usr/bin/python
#
# Software Carpentry Lesson Validator
#
# Check for errors at Software Carpentry lessons.
#
# Usage:
#
#     $ tools/check
Raniere Silva's avatar
Raniere Silva committed
import os
import re
import yaml

Greg Wilson's avatar
Greg Wilson committed
#----------------------------------------
# Error reporting.
Greg Wilson's avatar
Greg Wilson committed
def report_error(file_path, line_number, line, error_message):
    '''
    FIXME: docstring.
    '''
    ERR_MSG = "Error at line {} of {}:\n\t{}\n{}"
    print(ERR_MSG.format(line_number, file_path, line, error_message))

def report_missing(present, file_path, missing_element):
    '''
    FIXME: docstring.
    '''
    ERR_MSG = "Error on {}: missing {}"
    if not present:
        print(ERR_MSG.format(file_path, missing_element))
Raniere Silva's avatar
Raniere Silva committed

def report_missing_metadata(missing_element):
Greg Wilson's avatar
Greg Wilson committed
    '''
    FIXME: docstring.
    '''
    ERR_MSG = "Error on YAML header: missing {}"
    print(ERR_MSG.format(missing_element))
Raniere Silva's avatar
Raniere Silva committed

def report_broken_link(file_path, line_number, link):
Greg Wilson's avatar
Greg Wilson committed
    '''
    FIXME: docstring.
    '''
    ERR_MSG = "Broken link at line {} of {}:\n\tCan't find {}."
    print(ERR_MSG.format(line_number, file_path, link))
Greg Wilson's avatar
Greg Wilson committed
#----------------------------------------
# Checking.

def check_yaml(metadata):
Raniere Silva's avatar
Raniere Silva committed
    """
    Check if all metadata are present at YAML header.
    """
Greg Wilson's avatar
Greg Wilson committed
    METADATA_REQUIRED = {"layout", "title", "minutes"}
    for key in METADATA_REQUIRED - set(metadata.keys()):
        report_missing_metadata(key)
Raniere Silva's avatar
Raniere Silva committed

def check_lesson(file_path):
Greg Wilson's avatar
Greg Wilson committed
    '''
    FIXME: docstring telling people what you want them to write here.
    '''
Raniere Silva's avatar
Raniere Silva committed
    pass

def check_discussion(file_path):
Greg Wilson's avatar
Greg Wilson committed
    '''
    FIXME: docstring telling people what you want them to write here.
    '''
Raniere Silva's avatar
Raniere Silva committed
    pass

def check_index(file_path):
Greg Wilson's avatar
Greg Wilson committed
    '''
    FIXME: docstring.
    And break this up into pieces -- it's too long.
    '''
Raniere Silva's avatar
Raniere Silva committed
    # State variables
    in_yaml = False
    yaml_metadata = []
    has_prerequisites = False
    has_topics = False
    has_other_resources = False

    # Load file and process it
    with open(file_path, 'r') as lines:
        for line_number, line in enumerate(lines):
Greg Wilson's avatar
Greg Wilson committed
            if re.match('---', line): # what if there are multiple YAML blocks??
Raniere Silva's avatar
Raniere Silva committed
                in_yaml = not in_yaml
            elif in_yaml:
                yaml_metadata.append(line)
Greg Wilson's avatar
Greg Wilson committed
            elif re.match('> ## Prerequisites', line): # check this in the Markdown or in the generated HTML?
Raniere Silva's avatar
Raniere Silva committed
                has_prerequisites = True
Greg Wilson's avatar
Greg Wilson committed
            elif re.match('## Topics', line): # as above?
Raniere Silva's avatar
Raniere Silva committed
                has_topics = True
Greg Wilson's avatar
Greg Wilson committed
            elif re.match('## Other Resources', line): # as above
Raniere Silva's avatar
Raniere Silva committed
                has_other_resources = True
            else:
Greg Wilson's avatar
Greg Wilson committed
                ## Push this check into another function - this one is getting too long.
Raniere Silva's avatar
Raniere Silva committed
                # Check if local links are valid
                matches = re.search('\[.*\]\((?P<link>.*)\)', line)
                if matches and not matches.group("link").startswith("http"):
Greg Wilson's avatar
Greg Wilson committed
                    link = os.path.join(os.path.dirname(file_path), matches.group("link"))
Raniere Silva's avatar
Raniere Silva committed
                    if link.endswith(".html"):
Greg Wilson's avatar
Greg Wilson committed
                        link = link.replace("html", "md") # NO: what about '03-html-editing.html' ?
Raniere Silva's avatar
Raniere Silva committed
                    if not os.path.exists(link):
                        report_broken_link(file_path, line_number, link)

Greg Wilson's avatar
Greg Wilson committed
    ## Again, this function is too long - break it into sub-functions.
Raniere Silva's avatar
Raniere Silva committed
    # Check YAML
    yaml_metadata = yaml.load('\n'.join(yaml_metadata))
Greg Wilson's avatar
Greg Wilson committed
    check_yaml(yaml_metadata, {"minutes"})
Raniere Silva's avatar
Raniere Silva committed

    # Check sections
Greg Wilson's avatar
Greg Wilson committed
    ## Note the refactoring: replaces three conditionals with one.
    report_missing(has_prerequisites, file_path, "Prerequisites")
    report_missing(has_topics, file_path, "Topics")
    report_missing(has_other_resources, file_path, "Other Resources")
Raniere Silva's avatar
Raniere Silva committed

def check_intructors(file_path):
Greg Wilson's avatar
Greg Wilson committed
    '''
    FIXME: docstring telling people what you want them to write here.
    '''
Raniere Silva's avatar
Raniere Silva committed
    pass

def check_motivation(file_path):
Greg Wilson's avatar
Greg Wilson committed
    '''
    FIXME: docstring telling people what you want them to write here.
    '''
Raniere Silva's avatar
Raniere Silva committed
    pass

def check_reference(file_path):
Greg Wilson's avatar
Greg Wilson committed
    '''
    FIXME: docstring telling people what you want them to write here.
    '''
Raniere Silva's avatar
Raniere Silva committed
    pass

def check_file(file_path):
Greg Wilson's avatar
Greg Wilson committed
    '''
    FIXME: docstring telling people what you want them to write here.
    '''
    ## Functions are objects, and so can be put in tables like the one below.
    CONTROL = (
        ('[0-9]{2}-.*', check_lesson),
        ('discussion',  check_discussion),
        ('index',       check_index),
        ('instructors', check_intructors),
        ("motivation",  check_motivation),
        ("reference",   check_reference)
    )
    for (pattern, checker) in CONTROL:
        if re.search(pattern, file_path):
            checker(file_path)

## main doesn't take sys.argv[1:] or the like?  Will help with testing...
Raniere Silva's avatar
Raniere Silva committed
def main():
Greg Wilson's avatar
Greg Wilson committed
    '''
    FIXME: docstring telling people what you want them to write here.
    '''
Raniere Silva's avatar
Raniere Silva committed
    lessons_file = os.listdir("pages")
    for lesson in lessons_file:
        if lesson.endswith('.md'):
Greg Wilson's avatar
Greg Wilson committed
            ## Why not os.path.join('pages', lesson) ?
Raniere Silva's avatar
Raniere Silva committed
            check_file('pages/{}'.format(lesson))

if __name__ == "__main__":
    main()