From 004768afdd70dd1adce7e78f8755e39b1db67947 Mon Sep 17 00:00:00 2001 From: dataflow-solutions-sk Date: Sun, 16 Aug 2026 23:08:17 +0200 Subject: [PATCH] Fix parsing of bracketed lists with unquoted hyphen-prefixed items DefaultParseValue('[a,-b,c]') previously fell back to treating the whole value as a plain string, because the bareword-replacement AST walk in _LiteralEval replaced the inner Name node of UnaryOp(USub, Name('b')) with a string Constant, leaving an invalid UnaryOp(USub, Constant('b')) that ast.literal_eval rejects. Now the walk detects a UnaryOp(USub, Name) -- i.e. a unary minus applied to a bare identifier -- and replaces the whole UnaryOp node with a single string Constant '-' + name, so it parses as the string '-b' instead. Numeric negation (UnaryOp(USub, Constant(2))) is untouched and still evaluates to the int -2. --- fire/parser.py | 27 +++++++++++++++++++++++++++ fire/parser_test.py | 6 ++++++ 2 files changed, 33 insertions(+) diff --git a/fire/parser.py b/fire/parser.py index a335cc2c..3a067e8f 100644 --- a/fire/parser.py +++ b/fire/parser.py @@ -105,11 +105,16 @@ def _LiteralEval(value): for index, subchild in enumerate(child): if isinstance(subchild, ast.Name): child[index] = _Replacement(subchild) + elif _IsNegativeBareword(subchild): + child[index] = _StrNode('-' + subchild.operand.id) elif isinstance(child, ast.Name): replacement = _Replacement(child) setattr(node, field, replacement) + elif _IsNegativeBareword(child): + setattr(node, field, _StrNode('-' + child.operand.id)) + # ast.literal_eval supports the following types: # strings, bytes, numbers, tuples, lists, dicts, sets, booleans, and None # (bytes and set literals only starting with Python 3.2) @@ -130,3 +135,25 @@ def _Replacement(node): if value in ('True', 'False', 'None'): return node return _StrNode(value) + + +def _IsNegativeBareword(node): + """Returns whether node is a unary minus applied to a bare identifier. + + For example, the token '-b' in '[a,-b,c]' parses as + ast.UnaryOp(op=USub, operand=ast.Name('b')). Since ast.literal_eval + can't negate a string, this can't simply be handled by replacing the + inner Name node with a string Constant (as _Replacement does) -- the + whole UnaryOp node needs to be replaced with a single string Constant + of '-' + the identifier's name. + + Args: + node: An AST node. + Returns: + True if node is ast.UnaryOp(ast.USub, ast.Name) where the Name isn't + one of the builtin constants supported by literal_eval. + """ + return (isinstance(node, ast.UnaryOp) + and isinstance(node.op, ast.USub) + and isinstance(node.operand, ast.Name) + and node.operand.id not in ('True', 'False', 'None')) diff --git a/fire/parser_test.py b/fire/parser_test.py index a404eea2..60b12647 100644 --- a/fire/parser_test.py +++ b/fire/parser_test.py @@ -96,6 +96,12 @@ def testDefaultParseValueLists(self): def testDefaultParseValueBareWordsLists(self): self.assertEqual(parser.DefaultParseValue('[one, 2, "3"]'), ['one', 2, '3']) + def testDefaultParseValueBareWordsListsWithLeadingHyphen(self): + self.assertEqual(parser.DefaultParseValue('[a,-b,c]'), ['a', '-b', 'c']) + self.assertEqual(parser.DefaultParseValue('[1,-2,3]'), [1, -2, 3]) + self.assertIsInstance(parser.DefaultParseValue('[1,-2,3]')[1], int) + self.assertEqual(parser.DefaultParseValue('-b'), '-b') + def testDefaultParseValueDict(self): self.assertEqual( parser.DefaultParseValue('{"abc": 5, "123": 1}'), {'abc': 5, '123': 1})