Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(559)

Side by Side Diff: build/gn_helpers.py

Issue 1553993002: Add a Python parser for GN types to gn_helpers. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: comments Created 4 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | build/gn_helpers_unittest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # Copyright 2014 The Chromium Authors. All rights reserved. 1 # Copyright 2014 The Chromium Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be 2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file. 3 # found in the LICENSE file.
4 4
5 """Helper functions useful when writing scripts that are run from GN's 5 """Helper functions useful when writing scripts that integrate with GN.
6 exec_script function.""" 6
7 The main functions are ToGNString and FromGNString which convert between
8 serialized GN veriables and Python variables."""
7 9
8 class GNException(Exception): 10 class GNException(Exception):
9 pass 11 pass
10 12
11 13
12 def ToGNString(value, allow_dicts = True): 14 def ToGNString(value, allow_dicts = True):
13 """Prints the given value to stdout. 15 """Prints the given value to stdout.
14 16
15 allow_dicts indicates if this function will allow converting dictionaries 17 allow_dicts indicates if this function will allow converting dictionaries
16 to GN scopes. This is only possible at the top level, you can't nest a 18 to GN scopes. This is only possible at the top level, you can't nest a
17 GN scope in a list, so this should be set to False for recursive calls.""" 19 GN scope in a list, so this should be set to False for recursive calls."""
18 if isinstance(value, str): 20 if isinstance(value, str):
19 if value.find('\n') >= 0: 21 if value.find('\n') >= 0:
20 raise GNException("Trying to print a string with a newline in it.") 22 raise GNException("Trying to print a string with a newline in it.")
21 return '"' + value.replace('"', '\\"') + '"' 23 return '"' + \
24 value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \
25 '"'
26
27 if isinstance(value, bool):
28 if value:
29 return "true"
30 return "false"
22 31
23 if isinstance(value, list): 32 if isinstance(value, list):
24 return '[ %s ]' % ', '.join(ToGNString(v) for v in value) 33 return '[ %s ]' % ', '.join(ToGNString(v) for v in value)
25 34
26 if isinstance(value, dict): 35 if isinstance(value, dict):
27 if not allow_dicts: 36 if not allow_dicts:
28 raise GNException("Attempting to recursively print a dictionary.") 37 raise GNException("Attempting to recursively print a dictionary.")
29 result = "" 38 result = ""
30 for key in value: 39 for key in value:
31 if not isinstance(key, str): 40 if not isinstance(key, str):
32 raise GNException("Dictionary key is not a string.") 41 raise GNException("Dictionary key is not a string.")
33 result += "%s = %s\n" % (key, ToGNString(value[key], False)) 42 result += "%s = %s\n" % (key, ToGNString(value[key], False))
34 return result 43 return result
35 44
36 if isinstance(value, int): 45 if isinstance(value, int):
37 return str(value) 46 return str(value)
38 47
39 raise GNException("Unsupported type when printing to GN.") 48 raise GNException("Unsupported type when printing to GN.")
49
50
51 def FromGNString(input):
52 """Converts the input string from a GN serialized value to Python values.
53
54 For details on supported types see GNValueParser.Parse() below.
55
56 If your GN script did:
57 something = [ "file1", "file2" ]
58 args = [ "--values=$something" ]
59 The command line would look something like:
60 --values="[ \"file1\", \"file2\" ]"
61 Which when interpreted as a command line gives the value:
62 [ "file1", "file2" ]
63
64 You can parse this into a Python list using GN rules with:
65 input_values = FromGNValues(options.values)
66 Although the Python 'ast' module will parse many forms of such input, it
67 will not handle GN escaping properly, nor GN booleans. You should use this
68 function instead.
69
70
71 A NOTE ON STRING HANDLING:
72
73 If you just pass a string on the command line to your Python script, or use
74 string interpolation on a string variable, the strings will not be quoted:
75 str = "asdf"
76 args = [ str, "--value=$str" ]
77 Will yield the command line:
78 asdf --value=asdf
79 The unquoted asdf string will not be valid input to this function, which
80 accepts only quoted strings like GN scripts. In such cases, you can just use
81 the Python string literal directly.
82
83 The main use cases for this is for other types, in particular lists. When
84 using string interpolation on a list (as in the top example) the embedded
85 strings will be quoted and escaped according to GN rules so the list can be
86 re-parsed to get the same result."""
87 parser = GNValueParser(input)
88 return parser.Parse()
89
90
91 def UnescapeGNString(value):
Dirk Pranke 2016/01/22 00:34:02 Is this supposed to be a public function, or shoul
brettw 2016/01/22 18:12:14 I was thinking public since one could conceive of
92 """Given a string with GN escaping, returns the unescaped string.
93
94 Be careful not to feed with input from a Python parsing function like
95 'ast' because it will do Python unescaping, which will be incorrect when
96 fed into the GN unescaper."""
97 result = ''
98 i = 0
99 while i < len(value):
100 if value[i] == '\\':
101 if i < len(value) - 1:
102 next_char = value[i + 1]
103 if next_char == '$' or next_char == '"' or next_char == '\\':
Dirk Pranke 2016/01/22 00:34:02 nit: maybe if next_char in ('$', '"', '\\'):
brettw 2016/01/22 18:12:15 Done.
104 # These are the escaped characters GN supports.
105 result += next_char
106 i += 1
107 else:
108 # Any other backslash is a litera.
Dirk Pranke 2016/01/22 00:34:02 typo: s/litera/literal/ .
109 result += '\\'
110 else:
111 result += value[i]
112 i += 1
113 return result
114
115
116 class GNValueParser:
Dirk Pranke 2016/01/22 00:34:02 nit: inherit from object, like: class GNValuePar
brettw 2016/01/22 18:12:15 Yes, if you want to parse a specific type or somet
117 """Duplicates GN parsing of values and converts to Python types.
118
119 Normally you would use the wrapper function FromGNValue() below.
120
121 If you expect input as a specific type, you can also call one of the Parse*
122 functions directly. All functions throw GNException on invalid input. """
123 def __init__(self, string):
124 self.input = string
125 self.cur = 0
126
127 def IsDone(self):
128 return self.cur == len(self.input)
129
130 def ConsumeWhitespace(self):
131 while not self.IsDone() and self.input[self.cur] in ' \t\n':
132 self.cur += 1
133
134 def Parse(self):
135 """Converts a string representing a printed GN value to the Python type.
136
137 See additional usage notes on FromGNString above.
138
139 - GN booleans ('true', 'false') will be converted to Python booleans.
140
141 - GN numbers ('123') will be converted to Python numbers.
142
143 - GN strings (double-quoted as in '"asdf"') will be converted to Python
144 strings with GN escaping rules. GN string interpolation (embedded
145 variables preceeded by $) are not supported and will be returned as
146 literals.
147
148 - GN lists ('[1, "asdf", 3]') will be converted to Python lists.
149
150 - GN scopes ('{ ... }') are not supported."""
151 result = self.ParseAllowTrailing()
152 self.ConsumeWhitespace()
153 if not self.IsDone():
154 raise GNException("Trailing input after parsing:\n " +
155 self.input[self.cur:])
156 return result
157
158 def ParseAllowTrailing(self):
Dirk Pranke 2016/01/22 00:34:02 if this and the following functions are not public
159 """Internal version of Parse that doesn't check for trailing stuff."""
160 self.ConsumeWhitespace()
161 if self.IsDone():
162 raise GNException("Expected input to parse.")
163
164 next_char = self.input[self.cur]
165 if next_char == '[':
166 return self.ParseList()
167 elif next_char in "-0123456789":
Dirk Pranke 2016/01/22 00:34:02 nit: consider making this a helper function: IsDig
168 return self.ParseNumber()
169 elif next_char == '"':
170 return self.ParseString()
171 elif self.ConstantFollows('true'):
172 return True
173 elif self.ConstantFollows('false'):
174 return False
175 else:
176 raise GNException("Unexpected token: " + self.input[self.cur:])
177
178 def ParseNumber(self):
179 self.ConsumeWhitespace()
180 if self.IsDone():
181 raise GNException('Expected number but got nothing.')
182
183 begin = self.cur
184
185 # The first character can include a negative sign.
186 if not self.IsDone() and self.input[self.cur] in '-0123456789':
187 self.cur += 1
188 while not self.IsDone() and self.input[self.cur] in '0123456789':
Dirk Pranke 2016/01/22 00:34:02 maybe self.input[self.cur].isdigit() ? maybe not .
brettw 2016/01/22 18:12:14 I don't think in regex's and the code is longer. I
189 self.cur += 1
190
191 number_string = self.input[begin:self.cur]
192 if not len(number_string) or number_string == '-':
193 raise GNException("Not a valid number.")
194 return int(number_string)
195
196 def ParseString(self):
197 self.ConsumeWhitespace()
198 if self.IsDone():
199 raise GNException('Expected string but got nothing.')
200
201 if self.input[self.cur] != '"':
202 raise GNException('Expected string beginning in a " but got:\n ' +
203 self.input[self.cur:])
204 self.cur += 1 # Skip over quote.
205
206 begin = self.cur
207 while not self.IsDone() and self.input[self.cur] != '"':
208 if self.input[self.cur] == '\\':
209 self.cur += 1 # Skip over the backslash.
210 if self.IsDone():
211 raise GNException("String ends in a backslash in:\n " +
212 self.input)
213 self.cur += 1
214
215 if self.IsDone():
216 raise GNException('Unterminated string:\n ' + self.input[begin:])
217
218 end = self.cur
219 self.cur += 1 # Consume trailing ".
220
221 return UnescapeGNString(self.input[begin:end])
222
223 def ParseList(self):
224 self.ConsumeWhitespace()
225 if self.IsDone():
226 raise GNException('Expected string but got nothing.')
Dirk Pranke 2016/01/22 00:34:02 'Expected list' instead?
brettw 2016/01/22 18:12:15 Done.
227
228 # Skip over opening '['.
229 if self.input[self.cur] != '[':
230 raise GNException("Expected [ for list but got:\n " +
231 self.input[self.cur:])
232 self.cur += 1
233 self.ConsumeWhitespace()
234 if self.IsDone():
235 raise GNException("Unterminated list:\n " + self.input)
236
237 list_result = []
238 previous_had_trailing_comma = True
239 while not self.IsDone():
240 if self.input[self.cur] == ']':
241 self.cur += 1 # Skip over ']'.
242 return list_result
243
244 if not previous_had_trailing_comma:
245 raise GNException("List items not separated by comma.")
246
247 list_result += [ self.ParseAllowTrailing() ]
248 self.ConsumeWhitespace()
249 if self.IsDone():
250 break
251
252 # Consume comma if there is one.
253 previous_had_trailing_comma = self.input[self.cur] == ','
254 if previous_had_trailing_comma:
255 # Consume comma.
256 self.cur += 1
257 self.ConsumeWhitespace()
258
259 raise GNException("Unterminated list:\n " + self.input)
260
261 def ConstantFollows(self, constant):
Dirk Pranke 2016/01/22 00:34:02 nit: _ConstantFollows() if this isn't a public fun
brettw 2016/01/22 18:12:14 Done.
262 """Returns true if the given constant follows immediately at the current
263 location in the input. If it does, the text is consumed and the function
264 returns true. Otherwise, returns false and the current position is
265 unchanged."""
266 end = self.cur + len(constant)
267 if end >= len(self.input):
268 return False # Not enough room.
269 if self.input[self.cur:end] == constant:
270 self.cur = end
271 return True
272 return False
OLDNEW
« no previous file with comments | « no previous file | build/gn_helpers_unittest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698