diff --git a/parso/grammar.py b/parso/grammar.py index 19125ce..7af2064 100644 --- a/parso/grammar.py +++ b/parso/grammar.py @@ -220,7 +220,7 @@ class PythonFStringGrammar(Grammar): ) return p.parse(tokens=tokens) - def parse_leaf(leaf, error_recovery=True): + def parse_leaf(self, leaf, error_recovery=True): code = leaf._get_payload() return self.parse(code, error_recovery=True, start_pos=leaf.start_pos) diff --git a/parso/normalizer.py b/parso/normalizer.py index 0a7cbbd..0a0e6b7 100644 --- a/parso/normalizer.py +++ b/parso/normalizer.py @@ -13,7 +13,7 @@ class _NormalizerMeta(type): class Normalizer(use_metaclass(_NormalizerMeta)): def __init__(self, grammar, config): - self._grammar = grammar + self.grammar = grammar self._config = config self.issues = [] diff --git a/parso/python/errors.py b/parso/python/errors.py index a39b678..537844a 100644 --- a/parso/python/errors.py +++ b/parso/python/errors.py @@ -254,7 +254,7 @@ class ErrorFinder(Normalizer): def __init__(self, *args, **kwargs): super(ErrorFinder, self).__init__(*args, **kwargs) self._error_dict = {} - self.version = self._grammar.version_info + self.version = self.grammar.version_info def initialize(self, node): def create_context(node): @@ -836,6 +836,54 @@ class _TryStmtRule(SyntaxRule): self.add_issue(default_except, message=self.message) +@ErrorFinder.register_rule(type='string') +class _FStringRule(SyntaxRule): + _fstring_grammar = None + message_empty = "f-string: empty expression not allowed" # f'{}' + "f-string: single '}' is not allowed" # f'}' + "f-string: expressions nested too deeply" # f'{1:{5:{3}}}' + message_backslash = "f-string expression part cannot include a backslash" # f'{"\"}' or f'{"\\"}' + message_comment = "f-string expression part cannot include '#'" # f'{#}' + "f-string: unterminated string" # f'{"}' + "f-string: mismatched '(', '{', or '['" + "f-string: invalid conversion character: expected 's', 'r', or 'a'" # f'{1!b}' + "f-string: unexpected end of string" # Doesn't really happen?! + "f-string: expecting '}'" # f'{' + + @classmethod + def _load_grammar(cls): + import parso + + if cls._fstring_grammar is None: + cls._fstring_grammar = parso.load_grammar(language='python-f-string') + return cls._fstring_grammar + + def is_issue(self, fstring): + if 'f' not in fstring.string_prefix.lower(): + return + + parsed = self._load_grammar().parse_leaf(fstring) + for child in parsed.children: + type = child.type + if type == 'expression': + self._check_expression(child.children[1]) + + def _check_expression(self, python_expr): + value = python_expr.value + if '\\' in value: + self.add_issue(python_expr, message=self.message_backslash) + return + if '#' in value: + self.add_issue(python_expr, message=self.message_comment) + return + # This is now nested parsing. We parsed the fstring and now + # we're parsing Python again. + module = self._normalizer.grammar.parse(value) + parsed_expr = module.children[0] + if parsed_expr.type == 'endmarker': + self.add_issue(python_expr, message=self.message_empty) + + class _CheckAssignmentRule(SyntaxRule): def _check_assignment(self, node, is_deletion=False): error = None diff --git a/parso/python/fstring.py b/parso/python/fstring.py index 58cac7d..025a48c 100644 --- a/parso/python/fstring.py +++ b/parso/python/fstring.py @@ -186,5 +186,5 @@ class Parser(parser.BaseParser): def convert_leaf(self, pgen_grammar, type, value, prefix, start_pos): # TODO this is so ugly. - leaf_type = TokenNamespace.token_map[type] + leaf_type = TokenNamespace.token_map[type].lower() return TypedLeaf(leaf_type, value, start_pos, prefix) diff --git a/parso/tree.py b/parso/tree.py index 8730af9..a5404e2 100644 --- a/parso/tree.py +++ b/parso/tree.py @@ -203,7 +203,10 @@ class Leaf(NodeOrLeaf): @utf8_repr def __repr__(self): - return "<%s: %s>" % (type(self).__name__, self.value) + value = self.value + if not value: + value = self.type + return "<%s: %s>" % (type(self).__name__, value) class TypedLeaf(Leaf): diff --git a/test/failing_examples.py b/test/failing_examples.py index 980a604..7127ac3 100644 --- a/test/failing_examples.py +++ b/test/failing_examples.py @@ -138,6 +138,12 @@ FAILING_EXAMPLES = [ 'def x():\n 1\n 2', 'if 1:\nfoo', 'if 1: blubb\nif 1:\npass\nTrue and False', + + # f-strings + 'f"{}"', + 'f"{\\}"', + #'f"{\'\\\'}"', + 'f"{#}"', ] GLOBAL_NONLOCAL_ERROR = [