Skip to content

Commit

Permalink
Merge b8e1c88 into 9f6efab
Browse files Browse the repository at this point in the history
  • Loading branch information
Eric Chea authored and Eric Chea committed Dec 9, 2014
2 parents 9f6efab + b8e1c88 commit 2c3dcf6
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 23 deletions.
161 changes: 149 additions & 12 deletions radon/raw.py
Expand Up @@ -30,7 +30,8 @@
# comments = Comments lines
# blank = Blank lines (or whitespace-only lines)
Module = collections.namedtuple('Module', ['loc', 'lloc', 'sloc',
'comments', 'multi', 'blank'])
'comments', 'multi', 'blank',
'single_comments'])


def _generate(code):
Expand Down Expand Up @@ -148,6 +149,145 @@ def aux(sub_tokens):
return sum(aux(sub) for sub in _split_tokens(tokens, OP, ';'))


def remove_lines(doc, lines_to_remove):
'''Removes lines from a document.
:param doc: [str], document cast into an array.
:param lines_to_remove: [int], list of lines to remove from the doc.
:return: [str], doc with specified lines removed.
'''

for line_number in lines_to_remove:
doc[line_number] = []
return [line.strip() for line in doc if line]


def is_multiline_string(doc, line_count, quote_type):
'''Cases to catch multiline_strings.
:param doc: [str], a document cast into an array.
:param line_count: int, zero based index that points to the current line
in an docuement.
:param quote_type: str, one of the two multiline quotes available in python.
:return: bool, True if the triple quoted line is a multiline string.
'''

line = doc[line_count]
previous_line = doc[line_count-1]

if line.count('=') > 0 and line.index('=') < line.index(quote_type)\
or line_count != 0 and '=' in previous_line[-3]:
return True

else:
return False


def find_multiline_comments(lines_to_remove, end, doc, line_count, quote_type):
'''
:param lines_to_remove: [int], a zero based index that represents lines to
to be removed from a document.
:param end: bool, if True then the first of the two multiline comments has
been found.
:param doc: [str], a document cast into an array.
:param line_count: int, zero based index that points to the current line
in an docuement.
:param quote_type: str, one of the two multiline quotes available in python.
:return: tuple, lines_to_remove = same as that passed in, with additions.
end = bool, updated version of end paramater.
'''

# Exceptions: Quote type needs to exist, to get the first line of a
# multine comment it cannot be the last.
if quote_type and end is False and line_count < len(doc) - 1:
quote_type = quote_type[0]
end = True

if is_multiline_string(doc, line_count, quote_type):
# If a multiline string is found move the cursor until the end
# of the multiline string.
line_count += 1
while quote_type not in doc[line_count]:
line_count += 1
if quote_type in doc[line_count]:
line_count += 1
break
return lines_to_remove, False, line_count

lines_to_remove.append(line_count)
return lines_to_remove, end, line_count

elif end and not quote_type:
lines_to_remove.append(line_count)

elif end and quote_type:
lines_to_remove.append(line_count)
end = False

return lines_to_remove, end, line_count


def find_comments(lines_to_remove, line_count, line):
'''Find single line comments in a python file.
:param lines_to_remove: [int], a zero based index that represents lines to
to be removed from a document.
:param line_count: int, zero based index that points to the current line
in an docuement.
:param line: str, the current line in a document being examined.
:return: [int], same as parameter with additional indices that were found.
'''

if not line:
return (lines_to_remove, True)

if line[0] == "#" or line.count("'''") == 2 and '=' not in line or\
line.count('"""') == 2 and '=' not in line:
lines_to_remove.append(line_count)
return (lines_to_remove, True)

return (lines_to_remove, False)


def remove_python_documentation(doc):
'''Removes all the documentation from python code.
:param doc: [str], each line of a code recasted as an array
:return: [str], doc that was passed in, excluding lines of documentation.
'''

multi_quos = ["'''", '"""']
lines_to_remove = []
end = False
comments = 0
multi = 1
new_count = 0
for line_count, line in enumerate(doc):

if new_count > line_count:
continue
lines_to_remove, removed = find_comments(lines_to_remove,
line_count,
line)
if removed:
comments += 1
continue

quote_type = [multi_quo for multi_quo in multi_quos\
if multi_quo in doc[line_count]]

# end is True if the first of a pair of multiline comments is found and
# end will revert back to False when both pairs are found.
lines_to_remove, end, new_count = find_multiline_comments(
lines_to_remove=lines_to_remove,
end=end,
doc=doc,
line_count=line_count,
quote_type=quote_type)
if end:
multi += 1
# Set multi equal to 0 if no multi strings were found
if multi == 1:
multi = 0
return len(remove_lines(doc, lines_to_remove)), comments, multi


def analyze(source):
'''Analyze the source code and return a namedtuple with the following
fields:
Expand All @@ -164,31 +304,28 @@ def analyze(source):
Multiline strings are not counted as comments, since, to the Python
interpreter, they are not comments but strings.
'''
loc = sloc = lloc = comments = multi = blank = 0
lloc = comments = multi = blank = 0

# Cast source code into an array, devoid of blank lines
source_array = [line.strip() for line in source.split('\n') if line]
sloc = len(source_array)
loc, single_comments, multi = remove_python_documentation(source_array)
lines = iter(source.splitlines())
for lineno, line in enumerate(lines, 1):
loc += 1
line = line.strip()
if not line:
blank += 1
continue
# If this is not a blank line, then it counts as a
# source line of code
sloc += 1
try:
# Process a logical line that spans on multiple lines
tokens, sloc_incr, multi_incr = _get_all_tokens(line, lines)
tokens, _, _ = _get_all_tokens(line, lines)
except StopIteration:
raise SyntaxError('SyntaxError at line: {0}'.format(lineno))
# Update tracked metrics
loc += sloc_incr # LOC and SLOC increments are the same
sloc += sloc_incr
multi += multi_incr
# Add the comments
comments += list(map(TOKEN_NUMBER, tokens)).count(COMMENT)
# Process a logical line
# Split it on semicolons because they increase the number of logical
# lines
for sub_tokens in _split_tokens(tokens, OP, ';'):
lloc += _logical(sub_tokens)
return Module(loc, lloc, sloc, comments, multi, blank)
return Module(loc, lloc, sloc, comments, multi, blank, single_comments)
4 changes: 2 additions & 2 deletions radon/tests/test_cli_tools.py
Expand Up @@ -160,9 +160,9 @@ def testCCToDict(self):
class TestDictConversion(unittest.TestCase):

def test_raw_to_dict(self):
self.assertEqual(tools.raw_to_dict(Module(103, 123, 98, 8, 19, 5)),
self.assertEqual(tools.raw_to_dict(Module(103, 123, 98, 8, 19, 5, 3)),
{'loc': 103, 'lloc': 123, 'sloc': 98, 'comments': 8,
'multi': 19, 'blank': 5})
'multi': 19, 'blank': 5, 'single_comments': 3})

def test_cc_to_xml(self):
self.assertEqual(tools.dict_to_xml({'filename': CC_TO_XML_CASE}),
Expand Down
101 changes: 92 additions & 9 deletions radon/tests/test_raw.py
Expand Up @@ -141,13 +141,13 @@ def test_logical(self):

ANALYZE_CASES = [
('''
''', (0, 0, 0, 0, 0, 0)),
''', (0, 0, 0, 0, 0, 0, 0)),

('''
"""
doc?
"""
''', (3, 1, 3, 0, 3, 0)),
''', (0, 1, 3, 0, 3, 0, 0)),

('''
# just a comment
Expand All @@ -156,13 +156,13 @@ def test_logical(self):
else:
# you'll never get here
print('ven')
''', (6, 4, 6, 2, 0, 0)),
''', (4, 4, 6, 2, 0, 0, 2)),

('''
#
#
#
''', (3, 0, 3, 3, 0, 0)),
''', (0, 0, 3, 3, 0, 0, 3)),

('''
if a:
Expand All @@ -171,15 +171,15 @@ def test_logical(self):
else:
print
''', (6, 4, 4, 0, 0, 2)),
''', (4, 4, 4, 0, 0, 2, 0)),

# In this case the docstring is not counted as a multi-line string
# because in fact it is on one line!
('''
def f(n):
"""here"""
return n * f(n - 1)
''', (3, 3, 3, 0, 0, 0)),
''', (2, 3, 3, 0, 0, 0, 1)),

('''
def hip(a, k):
Expand All @@ -194,11 +194,94 @@ def fib(n):
"""
if n <= 1: return 1 # otherwise it will melt the cpu
return fib(n - 2) + fib(n - 1)
''', (12, 9, 11, 2, 4, 1)),
''', (6, 9, 10, 2, 3, 1, 1)),

('''
a = [1, 2, 3,
''', SyntaxError),

# Test that handling of parameters with a value passed in.
('''
def foo(n=1):
"""
Try it with n = 294942: it will take a fairly long time.
"""
if n <= 1: return 1 # otherwise it will melt the cpu
''', (2, 4, 5, 1, 3, 0, 0)),

('''
def foo(n=1):
"""
Try it with n = 294942: it will take a fairly long time.
"""
if n <= 1: return 1 # otherwise it will melt the cpu
string = """This is a string not a comment"""
''', (3, 5, 6, 1, 3, 0, 0)),

('''
def foo(n=1):
"""
Try it with n = 294942: it will take a fairly long time.
"""
if n <= 1: return 1 # otherwise it will melt the cpu
string = """
This is a string not a comment
"""
''', (5, 5, 8, 1, 3, 0, 0)),

('''
def foo(n=1):
"""
Try it with n = 294942: it will take a fairly long time.
"""
if n <= 1: return 1 # otherwise it will melt the cpu
string ="""
This is a string not a comment
"""
test = 0
''', (6, 6, 9, 1, 3, 0, 0)),

# Breaking lines still treated as single line of code.
('''
def foo(n=1):
"""
Try it with n = 294942: it will take a fairly long time.
"""
if n <= 1: return 1 # otherwise it will melt the cpu
string =\
"""
This is a string not a comment
"""
test = 0
''', (6, 6, 9, 1, 3, 0, 0)),

# Test handling of last line comment.
('''
def foo(n=1):
"""
Try it with n = 294942: it will take a fairly long time.
"""
if n <= 1: return 1 # otherwise it will melt the cpu
string =\
"""
This is a string not a comment
"""
test = 0
# Comment
''', (6, 6, 10, 2, 3, 0, 1)),

('''
def foo(n=1):
"""
Try it with n = 294942: it will take a fairly long time.
"""
if n <= 1: return 1 # otherwise it will melt the cpu
test = 0
string =\
"""
This is a string not a comment
"""
''', (6, 6, 9, 1, 3, 0, 0)),
]


Expand All @@ -217,5 +300,5 @@ def test_analyze(self):
else:
result = analyze(self.code)
self.assertEqual(result, self.expected)
# blank + sloc = loc
self.assertTrue(result[0] == result[2] + result[5])
# sloc - single_comments - multi = loc
self.assertTrue(result[0] == result[2] - result[6] - result[4])

0 comments on commit 2c3dcf6

Please sign in to comment.