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
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)
def test_include_missing_file(self): with self.assertRaises(FileNotFoundError): with lib50.working_area(["i_do_not_exist"]) as working_area: pass
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"})
def test_one_file(self): with lib50.working_area(["foo.py"]) as working_area: contents = os.listdir(working_area) self.assertEqual(contents, ["foo.py"])
def test_empty(self): with lib50.working_area([]) as working_area: contents = os.listdir(working_area) self.assertEqual(contents, [])
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"])
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"])