Skip to content

Commit 7f37cd9

Browse files
committed
Parse bare words with hyphens inside containers as strings
A bare word containing a hyphen, such as foo-bar, parses as an AST BinOp (Name - Name). When such a word appeared inside a container like a tuple or list, _LiteralEval only rewrote bare ast.Name nodes into strings, so the BinOp survived, ast.literal_eval failed on it, and the entire input collapsed back to a raw string. Detect a bare-word BinOp (a BinOp whose leaves are all ast.Name) and replace it with its original source text as a string. BinOps that contain numbers, such as 1+1 or 2017-10-10, are left untouched to preserve their existing behavior. Fixes #561
1 parent 716bbc2 commit 7f37cd9

2 files changed

Lines changed: 53 additions & 0 deletions

File tree

fire/parser.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,14 @@ def _LiteralEval(value):
105105
for index, subchild in enumerate(child):
106106
if isinstance(subchild, ast.Name):
107107
child[index] = _Replacement(subchild)
108+
elif _IsBareWordBinOp(subchild):
109+
child[index] = _SourceReplacement(value, subchild)
108110

109111
elif isinstance(child, ast.Name):
110112
replacement = _Replacement(child)
111113
setattr(node, field, replacement)
114+
elif _IsBareWordBinOp(child):
115+
setattr(node, field, _SourceReplacement(value, child))
112116

113117
# ast.literal_eval supports the following types:
114118
# strings, bytes, numbers, tuples, lists, dicts, sets, booleans, and None
@@ -130,3 +134,42 @@ def _Replacement(node):
130134
if value in ('True', 'False', 'None'):
131135
return node
132136
return _StrNode(value)
137+
138+
139+
def _IsBareWordBinOp(node):
140+
"""Returns whether node is a BinOp made only of bare words and operators.
141+
142+
A bare word like foo-bar parses as a BinOp (Name - Name). Inside a container
143+
Fire should treat it as the string 'foo-bar' rather than failing to evaluate.
144+
BinOps that contain numbers (e.g. 1+1) are left alone so their existing
145+
behavior is preserved.
146+
147+
Args:
148+
node: An AST node.
149+
Returns:
150+
True if node is a BinOp whose leaves are all bare words.
151+
"""
152+
if not isinstance(node, ast.BinOp):
153+
return False
154+
has_name = False
155+
for child in ast.walk(node):
156+
if isinstance(child, ast.Name):
157+
has_name = True
158+
elif isinstance(child, (ast.BinOp, ast.operator, ast.expr_context)):
159+
continue
160+
else:
161+
return False
162+
return has_name
163+
164+
165+
def _SourceReplacement(source, node):
166+
"""Returns a String node holding the original source text of node.
167+
168+
Args:
169+
source: The full string being parsed.
170+
node: An AST node with position info, taken from source.
171+
Returns:
172+
A String node whose value is the slice of source spanning node.
173+
"""
174+
segment = ast.get_source_segment(source, node)
175+
return _StrNode(segment)

fire/parser_test.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,16 @@ def testDefaultParseValueBareWordsTuple(self):
111111
self.assertEqual(parser.DefaultParseValue('(one, 2, "3")'), ('one', 2, '3'))
112112
self.assertEqual(parser.DefaultParseValue('one, "2", 3'), ('one', '2', 3))
113113

114+
def testDefaultParseValueBareWordsWithHyphens(self):
115+
self.assertEqual(
116+
parser.DefaultParseValue('(foo-bar, baz)'), ('foo-bar', 'baz'))
117+
self.assertEqual(
118+
parser.DefaultParseValue('[foo-bar, baz]'), ['foo-bar', 'baz'])
119+
self.assertEqual(
120+
parser.DefaultParseValue('[a-b-c, d]'), ['a-b-c', 'd'])
121+
self.assertEqual(
122+
parser.DefaultParseValue('{a: foo-bar}'), {'a': 'foo-bar'})
123+
114124
def testDefaultParseValueNestedContainers(self):
115125
self.assertEqual(
116126
parser.DefaultParseValue(

0 commit comments

Comments
 (0)