Skip to content
This repository was archived by the owner on Nov 3, 2023. It is now read-only.

Commit 9f462f6

Browse files
authored
Merge pull request #246 from Nurdok/feature/matrix-mul
Added matrix multiplication operator handling
2 parents 876d752 + c177e8f commit 9f462f6

File tree

2 files changed

+61
-5
lines changed

2 files changed

+61
-5
lines changed

src/pydocstyle/parser.py

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import tokenize as tk
77
from itertools import chain, dropwhile
88
from re import compile as re
9+
from .utils import log
910

1011
try:
1112
from StringIO import StringIO
@@ -223,17 +224,26 @@ def __init__(self, message):
223224

224225

225226
class TokenStream(object):
227+
# A logical newline is where a new expression or statement begins. When
228+
# there is a physical new line, but not a logical one, for example:
229+
# (x +
230+
# y)
231+
# The token will be tk.NL, not tk.NEWLINE.
232+
LOGICAL_NEWLINES = {tk.NEWLINE, tk.INDENT, tk.DEDENT}
233+
226234
def __init__(self, filelike):
227235
self._generator = tk.generate_tokens(filelike.readline)
228236
self.current = Token(*next(self._generator, None))
229237
self.line = self.current.start[0]
230-
self.log = logging.getLogger()
238+
self.log = log
239+
self.got_logical_newline = True
231240

232241
def move(self):
233242
previous = self.current
234243
current = self._next_from_generator()
235244
self.current = None if current is None else Token(*current)
236245
self.line = self.current.start[0] if self.current else self.line
246+
self.got_logical_newline = (previous.kind in self.LOGICAL_NEWLINES)
237247
return previous
238248

239249
def _next_from_generator(self):
@@ -270,8 +280,7 @@ class Parser(object):
270280

271281
def parse(self, filelike, filename):
272282
"""Parse the given file-like object and return its Module object."""
273-
# TODO: fix log
274-
self.log = logging.getLogger()
283+
self.log = log
275284
self.source = filelike.readlines()
276285
src = ''.join(self.source)
277286
try:
@@ -336,6 +345,8 @@ def parse_decorators(self):
336345
at_arguments = False
337346

338347
while self.current is not None:
348+
self.log.debug("parsing decorators, current token is %r (%s)",
349+
self.current.kind, self.current.value)
339350
if (self.current.kind == tk.NAME and
340351
self.current.value in ['def', 'class']):
341352
# Done with decorators - found function or class proper
@@ -373,9 +384,12 @@ def parse_definitions(self, class_, all=False):
373384
while self.current is not None:
374385
self.log.debug("parsing definition list, current token is %r (%s)",
375386
self.current.kind, self.current.value)
387+
self.log.debug('got_newline: %s', self.stream.got_logical_newline)
376388
if all and self.current.value == '__all__':
377389
self.parse_all()
378-
elif self.current.kind == tk.OP and self.current.value == '@':
390+
elif (self.current.kind == tk.OP and
391+
self.current.value == '@' and
392+
self.stream.got_logical_newline):
379393
self.consume(tk.OP)
380394
self.parse_decorators()
381395
elif self.current.value in ['def', 'class']:
@@ -471,6 +485,7 @@ def parse_definition(self, class_):
471485
assert self.current.kind != tk.INDENT
472486
docstring = self.parse_docstring()
473487
decorators = self._accumulated_decorators
488+
self.log.debug("current accumulated decorators: %s", decorators)
474489
self._accumulated_decorators = []
475490
self.log.debug("parsing nested definitions.")
476491
children = list(self.parse_definitions(class_))

src/tests/parser_test.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""Parser tests."""
22

33
import six
4+
import sys
45
import pytest
56
import textwrap
6-
from pydocstyle.parser import Parser, Decorator, ParseError
7+
from pydocstyle.parser import Parser, ParseError
78

89

910
class CodeSnippet(six.StringIO):
@@ -416,6 +417,46 @@ def test_raise_from():
416417
parser.parse(code, 'file_path')
417418

418419

420+
@pytest.mark.skipif(six.PY2, reason='Matrix multiplication operator is '
421+
'invalid in Python 2.x')
422+
def test_simple_matrix_multiplication():
423+
"""Make sure 'a @ b' doesn't trip the parser."""
424+
if sys.version_info.minor < 5:
425+
return
426+
parser = Parser()
427+
code = CodeSnippet("""
428+
def foo():
429+
a @ b
430+
""")
431+
parser.parse(code, 'file_path')
432+
433+
434+
@pytest.mark.skipif(six.PY2, reason='Matrix multiplication operator is '
435+
'invalid in Python 2.x')
436+
def test_matrix_multiplication_with_decorators():
437+
"""Make sure 'a @ b' doesn't trip the parser."""
438+
if sys.version_info.minor < 5:
439+
return
440+
parser = Parser()
441+
code = CodeSnippet("""
442+
def foo():
443+
a @ b
444+
(a
445+
@b)
446+
@a
447+
def b():
448+
pass
449+
""")
450+
module = parser.parse(code, 'file_path')
451+
452+
outer_function, = module.children
453+
assert outer_function.name == 'foo'
454+
455+
inner_function, = outer_function.children
456+
assert len(inner_function.decorators) == 1
457+
assert inner_function.decorators[0].name == 'a'
458+
459+
419460
def test_module_publicity():
420461
"""Test that a module that has a single leading underscore is private."""
421462
parser = Parser()

0 commit comments

Comments
 (0)