Exemplo n.º 1
0
    def __enter__(self):
        # Remember the student's directory
        internal.student_dir = Path.cwd()

        # Set up a temp dir for the checks
        self._working_area_manager = lib50.working_area(self.included_files,
                                                        name='-')
        internal.run_root_dir = self._working_area_manager.__enter__().parent

        # Change current working dir to the temp dir
        self._cd_manager = lib50.cd(internal.run_root_dir)
        self._cd_manager.__enter__()

        # TODO: Naming the module "checks" is arbitrary. Better name?
        self.checks_spec = importlib.util.spec_from_file_location(
            "checks", self.checks_path)

        # Clear check_names, import module, then save check_names. Not thread safe.
        # Ideally, there'd be a better way to extract declaration order than @check mutating global state,
        # but there are a lot of subtleties with using `inspect` or similar here
        _check_names.clear()
        check_module = importlib.util.module_from_spec(self.checks_spec)
        self.checks_spec.loader.exec_module(check_module)
        self.check_names = _check_names.copy()
        _check_names.clear()

        # Grab all checks from the module
        checks = inspect.getmembers(check_module,
                                    lambda f: hasattr(f, "_check_dependency"))

        # Map each check to tuples containing the names of the checks that depend on it
        self.dependency_graph = collections.defaultdict(set)
        for name, check in checks:
            dependency = None if check._check_dependency is None else check._check_dependency.__name__
            self.dependency_graph[dependency].add(name)

        # Map each check name to its description
        self.check_descriptions = {
            name: check.__doc__
            for name, check in checks
        }

        return self
Exemplo n.º 2
0
def main():
    parser = argparse.ArgumentParser(prog="check50")

    parser.add_argument("slug",
                        help=_("prescribed identifier of work to check"))
    parser.add_argument(
        "-d",
        "--dev",
        action="store_true",
        help=
        _("run check50 in development mode (implies --offline and --verbose).\n"
          "causes SLUG to be interpreted as a literal path to a checks package"
          ))
    parser.add_argument(
        "--offline",
        action="store_true",
        help=_("run checks completely offline (implies --local)"))
    parser.add_argument(
        "-l",
        "--local",
        action="store_true",
        help=
        _("run checks locally instead of uploading to cs50 (enabled by default in beta version)"
          ))
    parser.add_argument(
        "--log",
        action="store_true",
        help=_("display more detailed information about check results"))
    parser.add_argument("-o",
                        "--output",
                        action="store",
                        default="ansi",
                        choices=["ansi", "json"],
                        help=_("format of check results"))
    parser.add_argument(
        "-v",
        "--verbose",
        action="store_true",
        help=_(
            "display the full tracebacks of any errors (also implies --log)"))
    parser.add_argument("-V",
                        "--version",
                        action="version",
                        version=f"%(prog)s {__version__}")
    parser.add_argument("--logout", action=LogoutAction)

    args = parser.parse_args()

    # TODO: remove this when submit.cs50.io API is stabilized
    args.local = True

    if args.dev:
        args.offline = True
        args.verbose = True

    if args.offline:
        args.local = True

    if args.verbose:
        # Show lib50 commands being run in verbose mode
        logging.basicConfig(level="INFO")
        lib50.ProgressBar.DISABLED = True
        args.log = True

    excepthook.verbose = args.verbose
    excepthook.output = args.output

    if args.local:
        # If developing, assume slug is a path to check_dir
        if args.dev:
            internal.check_dir = Path(args.slug).expanduser().resolve()
            if not internal.check_dir.is_dir():
                raise Error(
                    _("{} is not a directory").format(internal.check_dir))
        else:
            # Otherwise have lib50 create a local copy of slug
            internal.check_dir = lib50.local(args.slug,
                                             "check50",
                                             offline=args.offline)

        config = internal.load_config(internal.check_dir)
        install_translations(config["translations"])

        if not args.offline:
            install_dependencies(config["dependencies"], verbose=args.verbose)

        checks_file = (internal.check_dir / config["checks"]).resolve()

        # Have lib50 decide which files to include
        included = lib50.files(config.get("files"))[0]

        if args.verbose:
            stdout = sys.stdout
            stderr = sys.stderr
        else:
            stdout = stderr = open(os.devnull, "w")

        # Create a working_area (temp dir) with all included student files named -
        with lib50.working_area(included, name='-') as working_area, \
                contextlib.redirect_stdout(stdout), \
                contextlib.redirect_stderr(stderr):
            results = CheckRunner(checks_file).run(included, working_area)
    else:
        # TODO: Remove this before we ship
        raise NotImplementedError(
            "cannot run check50 remotely, until version 3.0.0 is shipped ")
        username, commit_hash = lib50.push("check50", args.slug)
        results = await_results(
            f"https://cs50.me/check50/status/{username}/{commit_hash}")

    if args.output == "json":
        print_json(results)
    else:
        print_ansi(results, log=args.log)
Exemplo n.º 3
0
 def test_include_missing_file(self):
     with self.assertRaises(FileNotFoundError):
         with lib50.working_area(["i_do_not_exist"]) as working_area:
             pass
Exemplo n.º 4
0
    def test_multiple_files(self):
        with lib50.working_area(["foo.py", "bar.c"]) as working_area:
            contents = os.listdir(working_area)

        self.assertEqual(set(contents), {"foo.py", "bar.c"})
Exemplo n.º 5
0
    def test_one_file(self):
        with lib50.working_area(["foo.py"]) as working_area:
            contents = os.listdir(working_area)

        self.assertEqual(contents, ["foo.py"])
Exemplo n.º 6
0
    def test_empty(self):
        with lib50.working_area([]) as working_area:
            contents = os.listdir(working_area)

        self.assertEqual(contents, [])
Exemplo n.º 7
0
def main():
    parser = argparse.ArgumentParser(prog="check50")

    parser.add_argument("slug", help=_("prescribed identifier of work to check"))
    parser.add_argument("-d", "--dev",
                        action="store_true",
                        help=_("run check50 in development mode (implies --offline and --verbose).\n"
                               "causes SLUG to be interpreted as a literal path to a checks package"))
    parser.add_argument("--offline",
                        action="store_true",
                        help=_("run checks completely offline (implies --local)"))
    parser.add_argument("-l", "--local",
                        action="store_true",
                        help=_("run checks locally instead of uploading to cs50"))
    parser.add_argument("--log",
                        action="store_true",
                        help=_("display more detailed information about check results"))
    parser.add_argument("-o", "--output",
                        action="store",
                        nargs="+",
                        default=["ansi", "html"],
                        choices=["ansi", "json", "html"],
                        help=_("format of check results"))
    parser.add_argument("--target",
                        action="store",
                        nargs="+",
                        help=_("target specific checks to run"))
    parser.add_argument("--output-file",
                        action="store",
                        metavar="FILE",
                        help=_("file to write output to"))
    parser.add_argument("-v", "--verbose",
                        action="store_true",
                        help=_("display the full tracebacks of any errors (also implies --log)"))
    parser.add_argument("-V", "--version",
                        action="version",
                        version=f"%(prog)s {__version__}")
    parser.add_argument("--logout", action=LogoutAction)

    args = parser.parse_args()

    global SLUG
    SLUG = args.slug


    if args.dev:
        args.offline = True
        args.verbose = True

    if args.offline:
        args.local = True

    if args.verbose:
        # Show lib50 commands being run in verbose mode
        logging.basicConfig(level=os.environ.get("CHECK50_LOGLEVEL", "INFO"))
        lib50.ProgressBar.DISABLED = True
        args.log = True

    # Filter out any duplicates from args.output
    seen_output = set()
    args.output = [output for output in args.output if not (output in seen_output or seen_output.add(output))]

    # Set excepthook
    excepthook.verbose = args.verbose
    excepthook.outputs = args.output
    excepthook.output_file = args.output_file

    if not args.local:
        commit_hash = lib50.push("check50", SLUG, internal.CONFIG_LOADER, data={"check50": True})[1]
        with lib50.ProgressBar("Waiting for results") if "ansi" in args.output else nullcontext():
            tag_hash, results = await_results(commit_hash, SLUG)
    else:
        with lib50.ProgressBar("Checking") if not args.verbose and "ansi" in args.output else nullcontext():
            # If developing, assume slug is a path to check_dir
            if args.dev:
                internal.check_dir = Path(SLUG).expanduser().resolve()
                if not internal.check_dir.is_dir():
                    raise internal.Error(_("{} is not a directory").format(internal.check_dir))
            else:
                # Otherwise have lib50 create a local copy of slug
                try:
                    internal.check_dir = lib50.local(SLUG, offline=args.offline)
                except lib50.ConnectionError:
                    raise internal.Error(_("check50 could not retrieve checks from GitHub. Try running check50 again with --offline.").format(SLUG))
                except lib50.InvalidSlugError:
                    raise_invalid_slug(SLUG, offline=args.offline)

            # Load config
            config = internal.load_config(internal.check_dir)
            # Compile local checks if necessary
            if isinstance(config["checks"], dict):
                config["checks"] = internal.compile_checks(config["checks"], prompt=args.dev)

            install_translations(config["translations"])

            if not args.offline:
                install_dependencies(config["dependencies"], verbose=args.verbose)

            checks_file = (internal.check_dir / config["checks"]).resolve()

            # Have lib50 decide which files to include
            included = lib50.files(config.get("files"))[0]

            # Only open devnull conditionally
            ctxmanager = open(os.devnull, "w") if not args.verbose else nullcontext()
            with ctxmanager as devnull:
                if args.verbose:
                    stdout = sys.stdout
                    stderr = sys.stderr
                else:
                    stdout = stderr = devnull

                # Create a working_area (temp dir) with all included student files named -
                with lib50.working_area(included, name='-') as working_area, \
                        contextlib.redirect_stdout(stdout), \
                        contextlib.redirect_stderr(stderr):

                    runner = CheckRunner(checks_file)

                    # Run checks
                    if args.target:
                        check_results = runner.run(args.target, included, working_area)
                    else:
                        check_results = runner.run_all(included, working_area)

                    results = {
                        "slug": SLUG,
                        "results": [attr.asdict(result) for result in check_results],
                        "version": __version__
                    }


    # Render output
    file_manager = open(args.output_file, "w") if args.output_file else nullcontext(sys.stdout)
    with file_manager as output_file:
        for output in args.output:
            if output == "json":
                output_file.write(renderer.to_json(**results))
                output_file.write("\n")
            elif output == "ansi":
                output_file.write(renderer.to_ansi(**results, log=args.log))
                output_file.write("\n")
            elif output == "html":
                if os.environ.get("CS50_IDE_TYPE") and args.local:
                    html = renderer.to_html(**results)
                    subprocess.check_call(["c9", "exec", "renderresults", "check50", html])
                else:
                    if args.local:
                        html = renderer.to_html(**results)
                        with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".html") as html_file:
                            html_file.write(html)
                        url = f"file://{html_file.name}"
                    else:
                        url = f"https://submit.cs50.io/check50/{tag_hash}"

                    termcolor.cprint(_("To see the results in your browser go to {}").format(url), "white", attrs=["bold"])
Exemplo n.º 8
0
def main():
    parser = argparse.ArgumentParser(prog="check50")

    parser.add_argument("slug",
                        help=_("prescribed identifier of work to check"))
    parser.add_argument(
        "-d",
        "--dev",
        action="store_true",
        help=
        _("run check50 in development mode (implies --offline and --verbose info).\n"
          "causes SLUG to be interpreted as a literal path to a checks package"
          ))
    parser.add_argument(
        "--offline",
        action="store_true",
        help=
        _("run checks completely offline (implies --local, --no-download-checks and --no-install-dependencies)"
          ))
    parser.add_argument(
        "-l",
        "--local",
        action="store_true",
        help=_("run checks locally instead of uploading to cs50"))
    parser.add_argument(
        "--log",
        action="store_true",
        help=_("display more detailed information about check results"))
    parser.add_argument("-o",
                        "--output",
                        action="store",
                        nargs="+",
                        default=["ansi", "html"],
                        choices=["ansi", "json", "html"],
                        help=_("format of check results"))
    parser.add_argument("--target",
                        action="store",
                        nargs="+",
                        help=_("target specific checks to run"))
    parser.add_argument("--output-file",
                        action="store",
                        metavar="FILE",
                        help=_("file to write output to"))
    parser.add_argument(
        "-v",
        "--verbose",
        action="store",
        nargs="?",
        default="",
        const="info",
        choices=[
            attr for attr in dir(logging)
            if attr.isupper() and isinstance(getattr(logging, attr), int)
        ],
        type=str.upper,
        help=_(
            "sets the verbosity level."
            ' "INFO" displays the full tracebacks of errors and shows all commands run.'
            ' "DEBUG" adds the output of all command run.'))
    parser.add_argument(
        "--no-download-checks",
        action="store_true",
        help=
        _("do not download checks, but use previously downloaded checks instead (only works with --local)"
          ))
    parser.add_argument(
        "--no-install-dependencies",
        action="store_true",
        help=_("do not install dependencies (only works with --local)"))
    parser.add_argument("-V",
                        "--version",
                        action="version",
                        version=f"%(prog)s {__version__}")
    parser.add_argument("--logout", action=LogoutAction)

    args = parser.parse_args()

    global SLUG
    SLUG = args.slug

    # dev implies offline and verbose "info" if not overwritten
    if args.dev:
        args.offline = True
        if not args.verbose:
            args.verbose = "info"

    # offline implies local
    if args.offline:
        args.no_install_dependencies = True
        args.no_download_checks = True
        args.local = True

    # Setup logging for lib50 depending on verbosity level
    setup_logging(args.verbose)

    # Warning in case of running remotely with no_download_checks or no_install_dependencies set
    if not args.local:
        useless_args = []
        if args.no_download_checks:
            useless_args.append("--no-downloads-checks")
        if args.no_install_dependencies:
            useless_args.append("--no-install-dependencies")

        if useless_args:
            termcolor.cprint(_(
                "Warning: you should always use --local when using: {}".format(
                    ", ".join(useless_args))),
                             "yellow",
                             attrs=["bold"])

    # Filter out any duplicates from args.output
    seen_output = set()
    args.output = [
        output for output in args.output
        if not (output in seen_output or seen_output.add(output))
    ]

    # Set excepthook
    excepthook.verbose = bool(args.verbose)
    excepthook.outputs = args.output
    excepthook.output_file = args.output_file

    # If remote, push files to GitHub and await results
    if not args.local:
        commit_hash = lib50.push("check50",
                                 SLUG,
                                 internal.CONFIG_LOADER,
                                 data={"check50": True})[1]
        with lib50.ProgressBar("Waiting for results"
                               ) if "ansi" in args.output else nullcontext():
            tag_hash, results = await_results(commit_hash, SLUG)
    # Otherwise run checks locally
    else:
        with lib50.ProgressBar(
                "Checking") if "ansi" in args.output else nullcontext():
            # If developing, assume slug is a path to check_dir
            if args.dev:
                internal.check_dir = Path(SLUG).expanduser().resolve()
                if not internal.check_dir.is_dir():
                    raise internal.Error(
                        _("{} is not a directory").format(internal.check_dir))
            # Otherwise have lib50 create a local copy of slug
            else:
                try:
                    internal.check_dir = lib50.local(
                        SLUG, offline=args.no_download_checks)
                except lib50.ConnectionError:
                    raise internal.Error(
                        _("check50 could not retrieve checks from GitHub. Try running check50 again with --offline."
                          ).format(SLUG))
                except lib50.InvalidSlugError:
                    raise_invalid_slug(SLUG, offline=args.no_download_checks)

            # Load config
            config = internal.load_config(internal.check_dir)

            # Compile local checks if necessary
            if isinstance(config["checks"], dict):
                config["checks"] = internal.compile_checks(config["checks"],
                                                           prompt=args.dev)

            install_translations(config["translations"])

            if not args.no_install_dependencies:
                install_dependencies(config["dependencies"],
                                     verbose=args.verbose)

            checks_file = (internal.check_dir / config["checks"]).resolve()

            # Have lib50 decide which files to include
            included = lib50.files(config.get("files"))[0]

            with open(os.devnull,
                      "w") if args.verbose else nullcontext() as devnull:
                # Redirect stdout to devnull if some verbosity level is set
                if args.verbose:
                    stdout = stderr = devnull
                else:
                    stdout = sys.stdout
                    stderr = sys.stderr

                # Create a working_area (temp dir) named - with all included student files
                with lib50.working_area(included, name='-') as working_area, \
                        contextlib.redirect_stdout(stdout), \
                        contextlib.redirect_stderr(stderr):

                    check_results = CheckRunner(checks_file).run(
                        included, working_area, args.target)
                    results = {
                        "slug":
                        SLUG,
                        "results":
                        [attr.asdict(result) for result in check_results],
                        "version":
                        __version__
                    }

    # Render output
    file_manager = open(args.output_file,
                        "w") if args.output_file else nullcontext(sys.stdout)
    with file_manager as output_file:
        for output in args.output:
            if output == "json":
                output_file.write(renderer.to_json(**results))
                output_file.write("\n")
            elif output == "ansi":
                output_file.write(renderer.to_ansi(**results, log=args.log))
                output_file.write("\n")
            elif output == "html":
                if os.environ.get("CS50_IDE_TYPE") and args.local:
                    html = renderer.to_html(**results)
                    subprocess.check_call(
                        ["c9", "exec", "renderresults", "check50", html])
                else:
                    if args.local:
                        html = renderer.to_html(**results)
                        with tempfile.NamedTemporaryFile(
                                mode="w", delete=False,
                                suffix=".html") as html_file:
                            html_file.write(html)
                        url = f"file://{html_file.name}"
                    else:
                        url = f"https://submit.cs50.io/check50/{tag_hash}"

                    termcolor.cprint(
                        _("To see the results in your browser go to {}"
                          ).format(url),
                        "white",
                        attrs=["bold"])