diff --git a/parso/python/normalizer.py b/parso/python/normalizer.py index 0b9af46..f4da332 100644 --- a/parso/python/normalizer.py +++ b/parso/python/normalizer.py @@ -8,6 +8,10 @@ _STAR_EXPR_PARENTS = ('testlist_star_expr', 'testlist_comp', 'exprlist') _MAX_BLOCK_SIZE = 20 +def _is_bytes_literal(string): + return 'b' in string.string_prefix.lower() + + def _iter_stmts(scope): """ Iterates over all statements and splits up simple_stmt. @@ -123,6 +127,11 @@ class ErrorFinder(Normalizer): and _is_future_import(node) and not _is_future_import_first(node): message = "from __future__ imports must occur at the beginning of the file" self._add_syntax_error(message, node) + elif node.type == 'import_as_names': + if node.children[-1] == ',': + # from foo import a, + message = "trailing comma not allowed without surrounding parentheses" + self._add_syntax_error(message, node) elif node.type in _STAR_EXPR_PARENTS: if node.parent.type == 'del_stmt': self._add_syntax_error("can't use starred expression here", node.parent) @@ -153,6 +162,17 @@ class ErrorFinder(Normalizer): # foo(x for x in [], b) message = "Generator expression must be parenthesized if not sole argument" self._add_syntax_error(message, node) + elif node.type == 'atom': + first = node.children[0] + # e.g. 's' b'' + message = "cannot mix bytes and nonbytes literals" + # TODO this check is only relevant for Python 3+ + if first.type == 'string': + first_is_bytes = _is_bytes_literal(first) + for string in node.children[1:]: + if first_is_bytes != _is_bytes_literal(string): + self._add_syntax_error(message, node) + break yield diff --git a/parso/python/tree.py b/parso/python/tree.py index 8a4fb41..d199be7 100644 --- a/parso/python/tree.py +++ b/parso/python/tree.py @@ -25,6 +25,8 @@ Any subclasses of :class:`Scope`, including :class:`Module` has an attribute [] """ +import re + from parso._compatibility import utf8_repr, unicode from parso.tree import Node, BaseNode, Leaf, ErrorNode, ErrorLeaf, \ search_ancestor @@ -208,6 +210,10 @@ class String(Literal): type = 'string' __slots__ = () + @property + def string_prefix(self): + return re.match('\w*(?=[\'"])', self.value).group(0) + class _StringComparisonMixin(object): def __eq__(self, other): diff --git a/test/normalizer_issue_files/allowed_syntax.py b/test/normalizer_issue_files/allowed_syntax.py index fcc19b1..032715d 100644 --- a/test/normalizer_issue_files/allowed_syntax.py +++ b/test/normalizer_issue_files/allowed_syntax.py @@ -5,6 +5,10 @@ gather some of the potentially dangerous ones. from __future__ import division +'' '' +''r''u'' +b'' BR'' + for x in [1]: try: continue # Only the other continue and pass is an error. diff --git a/test/test_python_errors.py b/test/test_python_errors.py index 35bab3f..ef50e16 100644 --- a/test/test_python_errors.py +++ b/test/test_python_errors.py @@ -72,6 +72,7 @@ def test_indentation_errors(code, positions): 'yield', 'try: pass\nexcept: pass\nexcept X: pass', 'f(x for x in bar, 1)', + 'from foo import a,', # IndentationError ' foo', @@ -111,6 +112,7 @@ def test_python_exception_matches(code): ('async def foo():\n def nofoo():[x async for x in []]', '3.6'), ('[*[] for a in [1]]', '3.5'), ('{**{} for a in [1]}', '3.5'), + ('"s" b""', '3.5'), ] ) def test_python_exception_matches_version(code, version):