-
Notifications
You must be signed in to change notification settings - Fork 64
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #277 from cs50/develop
v3.0
- Loading branch information
Showing
5 changed files
with
236 additions
and
129 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
|
||
from pkg_resources import get_distribution, DistributionNotFound | ||
import os | ||
|
||
# https://stackoverflow.com/questions/17583443/what-is-the-correct-way-to-share-package-version-with-setup-py-and-the-package | ||
try: | ||
_dist = get_distribution("help50") | ||
# Normalize path for cross-OS compatibility. | ||
_dist_loc = os.path.normcase(_dist.location) | ||
_here = os.path.normcase(__file__) | ||
if not _here.startswith(os.path.join(_dist_loc, "help50")): | ||
# This version is not installed, but another version is. | ||
raise DistributionNotFound | ||
except DistributionNotFound: | ||
__version__ = "locally installed, no version information available" | ||
else: | ||
__version__ = _dist.version | ||
|
||
|
||
import collections | ||
|
||
HELPERS = collections.defaultdict(list) | ||
PREPROCESSORS = collections.defaultdict(list) | ||
|
||
|
||
def helper(*domains): | ||
""" Decorator that indicateas that a given function is a helper. """ | ||
def decorator(func): | ||
for domain in domains: | ||
HELPERS[domain].append(func) | ||
return func | ||
return decorator | ||
|
||
|
||
def preprocessor(*domains): | ||
""" Decorator that indicateas that a given function is a preprocessor. """ | ||
def decorator(func): | ||
for domain in domains: | ||
PREPROCESSORS[domain].append(func) | ||
return func | ||
return decorator | ||
|
||
|
||
class Error(Exception): | ||
pass |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
#!/usr/bin/env python | ||
import io | ||
import os | ||
import re | ||
import shlex | ||
import sys | ||
import tempfile | ||
import traceback | ||
|
||
from argparse import ArgumentParser, REMAINDER | ||
import lib50 | ||
import termcolor | ||
import pexpect | ||
|
||
from . import __version__, internal, Error | ||
|
||
|
||
def excepthook(cls, exc, tb): | ||
if (issubclass(cls, lib50.Error) or issubclass(cls, Error)) and exc.args: | ||
termcolor.cprint(str(exc), "red", file=sys.stderr) | ||
elif issubclass(cls, FileNotFoundError): | ||
termcolor.cprint(f"{exc.filename} not found", "red", file=sys.stderr) | ||
elif issubclass(cls, KeyboardInterrupt): | ||
print() | ||
elif not issubclass(cls, Exception): | ||
return | ||
else: | ||
termcolor.cprint("Sorry, something's wrong! Let [email protected] know!", "red", file=sys.stderr) | ||
|
||
if excepthook.verbose: | ||
traceback.print_exception(cls, exc, tb) | ||
|
||
sys.exit(1) | ||
|
||
|
||
excepthook.verbose = True | ||
sys.excepthook = excepthook | ||
|
||
|
||
def render_help(help): | ||
""" | ||
Display help message to student. | ||
`help` should be a 2-tuple whose first element is the part of the error | ||
message that is being translated and whose second element is the | ||
translation itself. | ||
""" | ||
if help is None: | ||
termcolor.cprint("Sorry, help50 does not yet know how to help with this!", "yellow") | ||
else: | ||
for line in help[0]: | ||
termcolor.cprint(line, "grey", "on_yellow") | ||
print() | ||
termcolor.cprint(re.sub(r"`([^`]+)`", r"\033[1m\1\033[22m", " " .join(help[1])), "yellow") | ||
|
||
|
||
def main(): | ||
parser = ArgumentParser(prog="help50", | ||
description="A command-line tool that helps " | ||
"students understand error messages.") | ||
parser.add_argument("-s", "--slug", help="identifier indicating from where to download helpers", default="cs50/helpers/master") | ||
parser.add_argument("-d", "--dev", help="slug will be treated as a local path, useful for developing helpers (implies --verbose)", action="store_true") | ||
parser.add_argument("-i", "--interactive", help="read command output from stdin instead of running a command", action="store_true") | ||
parser.add_argument("-v", "--verbose", help="display the full tracebacks of any errors", action="store_true") | ||
parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}") | ||
parser.add_argument("command", nargs=REMAINDER, | ||
default=[], help="command to be run") | ||
args = parser.parse_args() | ||
|
||
if args.dev: | ||
args.verbose = True | ||
|
||
excepthook.verbose = args.verbose | ||
|
||
|
||
if args.interactive: | ||
script = sys.stdin.read() | ||
elif args.command: | ||
# Capture stdout and stderr from process, and print it out | ||
with tempfile.TemporaryFile(mode="r+b") as temp: | ||
env = os.environ.copy() | ||
# Hack to prevent some programs from wrapping their error messages | ||
env["COLUMNS"] = "5050" | ||
proc = pexpect.spawn(f"bash -lc \"{' '.join(shlex.quote(word) for word in args.command)}\"", env=env) | ||
proc.logfile_read = temp | ||
proc.interact() | ||
proc.close() | ||
|
||
temp.seek(0) | ||
script = temp.read().decode().replace("\r\n", "\n") | ||
else: | ||
raise Error("Careful, you forgot to tell me with which command you " | ||
"need help!") | ||
termcolor.cprint("\nAsking for help...\n", "yellow") | ||
|
||
try: | ||
helpers_dir = args.slug if args.dev else lib50.local(args.slug) | ||
except lib50.Error: | ||
raise Error("Failed to fetch helpers, please ensure that you are connected to the internet!") | ||
|
||
internal.load_helpers(helpers_dir) | ||
render_help(internal.get_help(script)) | ||
|
||
|
||
if __name__ == "__main__": | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import contextlib | ||
import os | ||
import re | ||
import sys | ||
|
||
import lib50 | ||
|
||
from . import HELPERS, PREPROCESSORS | ||
|
||
lib50.set_local_path(os.environ.get("HELP50_PATH", "~/.local/share/help50")) | ||
CONFIG_LOADER = lib50.config.Loader("help50") | ||
|
||
|
||
@contextlib.contextmanager | ||
def _syspath(newpath): | ||
""" Useful contextmanager that temporarily replaces sys.path. """ | ||
oldpath = sys.path | ||
sys.path = newpath | ||
try: | ||
yield | ||
finally: | ||
sys.path = oldpath | ||
|
||
|
||
def load_config(dir): | ||
""" Read cs50 YAML file and apply default configuration to unspecified fields. """ | ||
options = { "helpers": ["helpers"] } | ||
try: | ||
config_file = lib50.config.get_config_filepath(dir) | ||
|
||
with open(config_file) as f: | ||
config = CONFIG_LOADER.load(f.read()) | ||
except lib50.InvalidConfigError: | ||
raise Error("Failed to parse help50 config, please let [email protected] know!") | ||
|
||
|
||
if isinstance(config, dict): | ||
options.update(config) | ||
|
||
if isinstance(options["helpers"], str): | ||
options["helpers"] = [options["helpers"]] | ||
|
||
return options | ||
|
||
|
||
def load_helpers(helpers_dir): | ||
""" Download helpers to a local directory via lib50. """ | ||
|
||
config = load_config(helpers_dir) | ||
for helper in config["helpers"]: | ||
with _syspath([str(helpers_dir)]): | ||
try: | ||
__import__(helper) | ||
except ImportError: | ||
raise Error("Failed to load helpers, please let [email protected] know!") | ||
|
||
|
||
def get_help(output): | ||
""" | ||
Given an error message, try every helper registered with help50 and return the output of the | ||
first one that matches. | ||
""" | ||
# Strip ANSI codes | ||
output = re.sub(r"\x1B\[[0-?]*[ -/]*[@-~]", "", output) | ||
|
||
for domain in HELPERS.keys(): | ||
processed = output | ||
for pre in PREPROCESSORS.get(domain, []): | ||
processed = pre(processed) | ||
lines = processed.splitlines() | ||
for i in range(len(lines)): | ||
slice = lines[i:] | ||
for helper in HELPERS.get(domain, []): | ||
try: | ||
before, after = helper(slice) | ||
except TypeError: | ||
pass | ||
else: | ||
return before, after |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters