Skip to content

Commit

Permalink
Merge pull request #277 from cs50/develop
Browse files Browse the repository at this point in the history
v3.0
  • Loading branch information
cmlsharp authored Aug 9, 2019
2 parents 6104cd3 + 2765584 commit 054dfe5
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 129 deletions.
126 changes: 0 additions & 126 deletions help50

This file was deleted.

45 changes: 45 additions & 0 deletions help50/__init__.py
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
106 changes: 106 additions & 0 deletions help50/__main__.py
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()
79 changes: 79 additions & 0 deletions help50/internal.py
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
9 changes: 6 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,14 @@
],
license="GPLv3",
description="This is help50, a command-line tool that helps students understand error messages.",
install_requires=["argparse", "requests", "pexpect", "termcolor"],
install_requires=["pexpect", "termcolor", "lib50>=1.1.10"],
keywords="help50",
name="help50",
scripts=["help50"],
packages=["help50"],
entry_points={
"console_scripts": ["help50=help50.__main__:main"]
},
py_requires="3.6",
url="https://github.com/cs50/help50",
version="2.0.1"
version="3.0.0"
)

0 comments on commit 054dfe5

Please sign in to comment.