Skip to content

Commit

Permalink
Merge pull request #3 from cs50/develop
Browse files Browse the repository at this point in the history
Update to 2.1.0
  • Loading branch information
Chad Sharp authored Aug 10, 2017
2 parents 5e44c6e + 01a88c9 commit d032a72
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 49 deletions.
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,16 @@ optional arguments:

`style50` takes zero or more files/directories to style check. If no arguments are given, `style50` will recursively check the current directory.

`MODE` can be one of `side_by_side` (default), `unified`, `raw`, and `json`. `side_by_side` and `unified` output side-by-side and unified diffs between the inputted file and the correctly styled version respectively. `raw` outputs the raw percentage of correct (unchanged) lines, while `json` outputs a json object containing information pertinent to the CS50 IDE plugin (coming soon).
`MODE` can be one of `character` (default), `split`, `unified`, `score`, and `json`. `character`, `split`, and `unified` output character-based, side-by-side, and unified diffs between the inputted file and the correctly styled version respectively. `score` outputs the raw percentage of correct (unchanged) lines, while `json` outputs a json object containing information pertinent to the CS50 IDE plugin (coming soon).

## Language Support
`style50` currently supports the following languages:
* C
* C++
* Python
* Javascript
* Java

- C++
- C
- Python
- Javascript
- Java

### Adding a new language

Expand All @@ -43,6 +44,7 @@ import re

from style50 import StyleCheck, Style50


class FooBar(StyleCheck):

# REQUIRED: this property informs style50 what file extensions this
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,5 @@
"console_scripts": ["style50=style50.__main__:main"],
},
url="https://github.com/cs50/style50",
version="2.0.0"
version="2.1.0"
)
4 changes: 2 additions & 2 deletions style50/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ def main():
# Define command-line arguments.
parser = argparse.ArgumentParser()
parser.add_argument("files", nargs="*", help="files/directories to lint", default=["."])
parser.add_argument("-o", "--output", action="store", default="side-by-side",
choices=["side-by-side", "unified", "raw", "json"], metavar="MODE",
parser.add_argument("-o", "--output", action="store", default="character",
choices=["character", "split", "unified", "score", "json"], metavar="MODE",
help="specify output mode")
parser.add_argument("-v", "--verbose", action="store_true",
help="print full tracebacks of errors")
Expand Down
7 changes: 7 additions & 0 deletions style50/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ def count_comments(self, code):
prev_type = t_type
return comments

def count_lines(self, code):
"""
count_lines ignores blank lines by default,
but blank lines are relavent to style per pep8
"""
return len(code.splitlines())

# TODO: Determine which options (if any) should be passed to autopep8
def style(self, code):
return autopep8.fix_code(code, options={"max_line_length": 100})
Expand Down
111 changes: 71 additions & 40 deletions style50/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
import itertools
import json
import os
import re
import struct
import subprocess
from termios import TIOCGWINSZ
import sys
import tempfile
from termios import TIOCGWINSZ

import icdiff
import six
Expand All @@ -25,7 +25,7 @@ def get_terminal_size(fallback=(80, 24)):
Return tuple containing columns and rows of controlling terminal, trying harder
than shutil.get_terminal_size to find a tty before returning fallback.
Theoretically, stdout, stderr, and stdin could all be different ttys which could
Theoretically, stdout, stderr, and stdin could all be different ttys that could
cause us to get the wrong measurements (instead of using the fallback) but the much more
common case is that IO is piped.
"""
Expand All @@ -52,9 +52,11 @@ class Style50(object):
"""
Class which checks a list of files/directories for style.
"""

# Dict which maps file extensions to check classes
extension_map = {}

def __init__(self, paths, output="side-by-side"):
def __init__(self, paths, output="character"):
# Creates a generator of all the files found recursively in `paths`.
self.files = itertools.chain.from_iterable(
[path] if not os.path.isdir(path)
Expand All @@ -64,35 +66,50 @@ def __init__(self, paths, output="side-by-side"):
for path in paths)

# Set run function as apropriate for output mode.
if output == "side-by-side":
self.run = self.run_diff
self.diff = self.side_by_side
elif output == "unified":
self.run = self.run_diff
self.diff = self.unified
elif output == "raw":
self.run = self.run_raw
if output == "score":
self.run = self.run_score
elif output == "json":
self.run = self.run_json
else:
raise Error("invalid output type")
self.run = self.run_diff
# Set diff function as needed
if output == "character":
self.diff = self.char_diff
elif output == "split":
self.diff = self.split_diff
elif output == "unified":
self.diff = self.unified
else:
raise Error("invalid output type")

def run_diff(self):
"""
Run checks on self.files, printing diff of styled/unstyled output to stdout.
"""
sep = "-" * COLUMNS
for file in self.files:
termcolor.cprint("{0}\n{1}\n{0}".format(sep, file), "cyan")
files = tuple(self.files)
# Use same header as more.
header, footer = (termcolor.colored("{0}\n{{}}\n{0}\n".format(
":" * 14), "cyan"), "\n") if len(files) > 1 else ("", "")

first = True
for file in files:
# Only print footer after first file has been printed
if first:
first = False
else:
print(footer, end="")

print(header.format(file), end="")

try:
results = self._check(file)
except Error as e:
termcolor.cprint(e.msg, "yellow", file=sys.stderr)
continue

# Display results.
if results.diffs:
print(*self.diff(results.original, results.styled), sep="\n")
print(*self.diff(results.original, results.styled), sep="", end="")
else:
termcolor.cprint("no style errors found", "green")

Expand All @@ -103,7 +120,7 @@ def run_diff(self):
def run_json(self):
"""
Run checks on self.files, printing json object
containing information relavent to the IDE plugin at the end.
containing information relavent to the CS50 IDE plugin at the end.
"""
checks = {}
for file in self.files:
Expand All @@ -115,12 +132,12 @@ def run_json(self):

checks[file] = {
"comments": results.comment_ratio >= results.COMMENT_MIN,
"diff": "<pre>{}</pre>".format("".join(self._html_diff(results.original, results.styled))),
"diff": "<pre>{}</pre>".format("".join(self.html_diff(results.original, results.styled))),
}

json.dump(checks, sys.stdout)

def run_raw(self):
def run_score(self):
"""
Run checks on self.files, printing raw percentage to stdout.
"""
Expand Down Expand Up @@ -163,30 +180,48 @@ def _check(self, file):
return check(code)

@staticmethod
def side_by_side(old, new):
def split_diff(old, new):
"""
Returns a generator yielding the side-by-side diff of `old` and `new`).
"""
return icdiff.ConsoleDiff(cols=COLUMNS).make_table(old.splitlines(), new.splitlines())
return (line + "\n" for line in icdiff.ConsoleDiff(cols=COLUMNS).make_table(old.splitlines(), new.splitlines()))

@staticmethod
def unified(old, new):
"""
Returns a generator yielding a unified diff between `old` and `new`.
"""
for diff in difflib.ndiff(old.splitlines(), new.splitlines()):
for diff in difflib.ndiff(old.splitlines(True), new.splitlines(True)):
if diff[0] == " ":
yield diff
elif diff[0] == "?":
continue
else:
yield termcolor.colored(diff, "red" if diff[0] == "-" else "green", attrs=["bold"])

def html_diff(self, old, new):
"""
Return HTML formatted character-based diff between old and new (used for CS50 IDE).
"""
def fmt_html(content, dtype):
content = cgi.escape(content, quote=True)
return content if dtype == " " else "<{1}><{0}></{1}>".format(content, "ins" if dtype == "+" else "del")

return self._char_diff(old, new, fmt_html)

def char_diff(self, old, new):
"""
Return color-coded character-based diff between `old` and `new`.
"""
def fmt_color(content, dtype):
return termcolor.colored(content, None, "on_green" if dtype == "+" else "on_red" if dtype == "-" else None)
return self._char_diff(old, new, fmt_color)

@staticmethod
def _html_diff(old, new):
def _char_diff(old, new, fmt):
"""
Returns a generator over an HTML-formatted char-based diff.
Not line based so not a suitable replacement for seld.diff
Returns a char-based diff between `old` and `new` where blocks are
formatted by `fmt`.
"""
differ = difflib.ndiff(old, new)
# Type of difference.
Expand All @@ -197,20 +232,16 @@ def _html_diff(old, new):
# Get next diff or None if we're at the end
d = next(differ, (None,))
if d[0] != dtype:
if buffer:
# Escape HTML.
content = cgi.escape("".join(buffer), quote=True)
if dtype == " ":
yield content
else:
yield "<{0}>{1}</{0}>".format("ins" if dtype == "+"
else "del", content)
yield fmt("".join(buffer), dtype)
dtype = d[0]
buffer.clear()
buffer = []

if dtype is None:
break
buffer.append(d[2:])

# Show insertions/deletions of whitespace clearly
ch = d[2] if dtype == " " else d[2].replace("\n", "\\n\n").replace("\t", "\\t")
buffer.append(ch)


class StyleMeta(ABCMeta):
Expand Down Expand Up @@ -252,9 +283,9 @@ def __init__(self, code):

self.styled = self.style(code)

# Count number of differences between styled and unstyled code.
self.diffs = sum(d[0] == "+"
for d in difflib.ndiff(code.splitlines(), self.styled.splitlines()))
# Count number of differences between styled and unstyled code (average of added and removed lines).
self.diffs = sum(d[0] == "+" or d[0] == "-"
for d in difflib.ndiff(code.splitlines(True), self.styled.splitlines(True))) / 2

self.lines = self.count_lines(self.styled)
self.score = 1 - self.diffs / self.lines
Expand All @@ -263,7 +294,7 @@ def count_lines(self, code):
"""
Count lines of code (by default ignores empty lines, but child could override to do more).
"""
return sum(1 for line in code.splitlines() if line.strip())
return sum(bool(line.strip()) for line in code.splitlines())

@staticmethod
def run(command, input=None, exit=0, shell=False):
Expand Down

0 comments on commit d032a72

Please sign in to comment.