From db75314805c2b5df57883f49b7d93c62a079189e Mon Sep 17 00:00:00 2001 From: Jared Garst Date: Sun, 2 Oct 2016 07:22:51 -0700 Subject: [PATCH] add format string support format strings require support for two new nodes: FormattedValue(expr valu, int? conversion, expr? format_spec) JoinedStr(expr* values) --- astroid/as_string.py | 15 ++++++++++++++- astroid/node_classes.py | 22 ++++++++++++++++++++++ astroid/nodes.py | 2 ++ astroid/rebuilder.py | 12 ++++++++++++ astroid/tests/unittest_python3.py | 11 +++++++++-- tox.ini | 4 ++-- 6 files changed, 61 insertions(+), 5 deletions(-) diff --git a/astroid/as_string.py b/astroid/as_string.py index 3d669c4..8eaebc1 100644 --- a/astroid/as_string.py +++ b/astroid/as_string.py @@ -481,6 +481,19 @@ def visit_asyncwith(self, node): def visit_asyncfor(self, node): return 'async %s' % self.visit_for(node) + def visit_joinedstr(self, node): + # Special treatment for constants, + # as we want to join literals not reprs + string = ''.join( + value.value if type(value).__name__ == 'Const' + else value.accept(self) + for value in node.values + ) + return "f'%s'" % string + + def visit_formattedvalue(self, node): + return '{%s}' % node.value.accept(self) + def _import_string(names): """return a list of (name, asname) formatted as a string""" @@ -490,7 +503,7 @@ def _import_string(names): _names.append('%s as %s' % (name, asname)) else: _names.append(name) - return ', '.join(_names) + return ', '.join(_names) if sys.version_info >= (3, 0): diff --git a/astroid/node_classes.py b/astroid/node_classes.py index 42fdf7c..9873158 100644 --- a/astroid/node_classes.py +++ b/astroid/node_classes.py @@ -1863,6 +1863,28 @@ class DictUnpack(NodeNG): """Represents the unpacking of dicts into dicts using PEP 448.""" +class FormattedValue(bases.NodeNG): + """Represents a PEP 498 format string.""" + _astroid_fields = ('value', 'format_spec') + value = None + conversion = None + format_spec = None + + def postinit(self, value, conversion=None, format_spec=None): + self.value = value + self.conversion = conversion + self.format_spec = format_spec + + +class JoinedStr(bases.NodeNG): + """Represents a list of string expressions to be joined.""" + _astroid_fields = ('values',) + value = None + + def postinit(self, values=None): + self.values = values + + # constants ############################################################## CONST_CLS = { diff --git a/astroid/nodes.py b/astroid/nodes.py index 1c279cc..3397294 100644 --- a/astroid/nodes.py +++ b/astroid/nodes.py @@ -37,6 +37,7 @@ TryExcept, TryFinally, Tuple, UnaryOp, While, With, Yield, YieldFrom, const_factory, AsyncFor, Await, AsyncWith, + FormattedValue, JoinedStr, # Backwards-compatibility aliases Backquote, Discard, AssName, AssAttr, Getattr, CallFunc, From, # Node not present in the builtin ast module. @@ -75,4 +76,5 @@ UnaryOp, While, With, Yield, YieldFrom, + FormattedValue, JoinedStr, ) diff --git a/astroid/rebuilder.py b/astroid/rebuilder.py index d80033f..91774cf 100644 --- a/astroid/rebuilder.py +++ b/astroid/rebuilder.py @@ -984,6 +984,24 @@ def visit_await(self, node, parent): return self._visit_with(new.AsyncWith, node, parent, assign_ctx=assign_ctx) + def visit_joinedstr(self, node, parent, assign_ctx=None): + newnode = new.JoinedStr() + newnode.lineno = node.lineno + newnode.col_offset = node.col_offset + newnode.parent = parent + newnode.postinit([self.visit(child, newnode) + for child in node.values]) + return newnode + + def visit_formattedvalue(self, node, parent, assign_ctx=None): + newnode = new.FormattedValue() + newnode.lineno = node.lineno + newnode.col_offset = node.col_offset + newnode.parent = parent + newnode.postinit(self.visit(node.value, newnode), + node.conversion, + _visit_or_none(node, 'format_spec', self, newnode, assign_ctx=assign_ctx)) + return newnode if sys.version_info >= (3, 0): TreeRebuilder = TreeRebuilder3k diff --git a/astroid/tests/unittest_python3.py b/astroid/tests/unittest_python3.py index ad8e57b..e555bb4 100644 --- a/astroid/tests/unittest_python3.py +++ b/astroid/tests/unittest_python3.py @@ -87,7 +87,7 @@ def test_metaclass_error(self): @require_version('3.0') def test_metaclass_imported(self): astroid = self.builder.string_build(dedent(""" - from abc import ABCMeta + from abc import ABCMeta class Test(metaclass=ABCMeta): pass""")) klass = astroid.body[1] @@ -98,7 +98,7 @@ class Test(metaclass=ABCMeta): pass""")) @require_version('3.0') def test_as_string(self): body = dedent(""" - from abc import ABCMeta + from abc import ABCMeta class Test(metaclass=ABCMeta): pass""") astroid = self.builder.string_build(body) klass = astroid.body[1] @@ -239,6 +239,13 @@ def test_unpacking_in_dict_getitem(self): self.assertIsInstance(value, nodes.Const) self.assertEqual(value.value, expected) + @require_version('3.6') + def test_format_string(self): + code = "f'{greetings} {person}'" + node = extract_node(code) + self.assertEqual(node.as_string(), code) + + if __name__ == '__main__': unittest.main()