# Copyright 2014 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file.
"""Helper functions useful when writing scripts that integrate with GN.
The main functions are ToGNString() and FromGNString(), to convert between
serialized GN veriables and Python variables.
# Computes ASCII code of an element of encoded Python 2 str / Python 3 bytes.
_Ord = ord if sys.version_info.major < 3 elselambda c: c
def _TranslateToGnChars(s): for decoded_ch in s.encode('utf-8'): # str in Python 2, bytes in Python 3.
code = _Ord(decoded_ch) # int if code in (34, 36, 92): # For '"', '$', or '\\'. yield'\\' + chr(code) elif 32 <= code < 127: yield chr(code) else: yield'$0x%02X' % code
def ToGNString(value, pretty=False): """Returns a stringified GN equivalent of a Python value.
Args:
value: The Python value to convert.
pretty: Whether to pretty print. Iftrue, then non-empty lists are rendered
recursively with one item per line, with indents. Otherwise lists are
rendered without new line.
Returns:
The stringified GN equivalent to |value|.
Raises:
GNError: |value| cannot be printed to GN. """
# Emits all output tokens without intervening whitespaces. def GenerateTokens(v, level): if isinstance(v, basestring_compat): yield'"' + ''.join(_TranslateToGnChars(v)) + '"'
elif isinstance(v, bool): yield'true'if v else'false'
elif isinstance(v, int): yield str(v)
elif isinstance(v, list): yield'[' for i, item in enumerate(v): if i > 0: yield',' for tok in GenerateTokens(item, level + 1): yield tok yield']'
elif isinstance(v, dict): if level > 0: yield'{' for key in sorted(v): ifnot isinstance(key, basestring_compat): raise GNError('Dictionary key is not a string.') ifnot key or key[0].isdigit() ornot key.replace('_', '').isalnum(): raise GNError('Dictionary key is not a valid GN identifier.') yield key # No quotations. yield'=' for tok in GenerateTokens(v[key], level + 1): yield tok if level > 0: yield'}'
else: # Not supporting float: Add only when needed. raise GNError('Unsupported type when printing to GN.')
can_start = lambda tok: tok and tok notin',}]='
can_end = lambda tok: tok and tok notin',{[='
# Adds whitespaces, trying to keep everything (except dicts) in 1 line. def PlainGlue(gen):
prev_tok = None for i, tok in enumerate(gen): if i > 0: if can_end(prev_tok) and can_start(tok): yield'\n'# New dict item. elif prev_tok == '['and tok == ']': yield' '# Special case for []. elif tok != ',': yield' ' yield tok
prev_tok = tok
# Adds whitespaces so non-empty lists can span multiple lines, with indent. def PrettyGlue(gen):
prev_tok = None
level = 0 for i, tok in enumerate(gen): if i > 0: if can_end(prev_tok) and can_start(tok): yield'\n' + ' ' * level # New dict item. elif tok == '='or prev_tok in'=': yield' '# Separator before and after '=', on same line. if tok in']}':
level -= 1 # Exclude '[]' and '{}' cases. if int(prev_tok == '[') + int(tok == ']') == 1 or \
int(prev_tok == '{') + int(tok == '}') == 1: yield'\n' + ' ' * level yield tok if tok in'[{':
level += 1 if tok == ',': yield'\n' + ' ' * level
prev_tok = tok
token_gen = GenerateTokens(value, 0)
ret = ''.join((PrettyGlue if pretty else PlainGlue)(token_gen)) # Add terminating '\n' for dict |value| or multi-line output. if isinstance(value, dict) or'\n'in ret: return ret + '\n' return ret
def FromGNString(input_string): """Converts the input string from a GN serialized value to Python values.
For details on supported types see GNValueParser.Parse() below.
If your GN script did:
something = [ "file1", "file2" ]
args = [ "--values=$something" ]
The command line would look something like:
--values="[ \"file1\", \"file2\" ]"
Which when interpreted as a command line gives the value:
[ "file1", "file2" ]
You can parse this into a Python list using GN rules with:
input_values = FromGNValues(options.values)
Although the Python 'ast' module will parse many forms of such input, it
will not handle GN escaping properly, nor GN booleans. You should use this
function instead.
A NOTE ON STRING HANDLING:
If you just pass a string on the command line to your Python script, or use
string interpolation on a string variable, the strings will not be quoted:
str = "asdf"
args = [ str, "--value=$str" ]
Will yield the command line:
asdf --value=asdf
The unquoted asdf string will not be valid input to this function, which
accepts only quoted strings like GN scripts. In such cases, you can just use
the Python string literal directly.
The main use cases for this isfor other types, in particular lists. When
using string interpolation on a list (asin the top example) the embedded
strings will be quoted and escaped according to GN rules so the list can be
re-parsed to get the same result. """
parser = GNValueParser(input_string) return parser.Parse()
def FromGNArgs(input_string): """Converts a string with a bunch of gn arg assignments into a Python dict.
Given a whitespace-separated list of
<ident> = (integer | string | boolean | <list of the former>)
Only simple types and lists supported; variables, structs, calls and other, more complicated things are not.
This routine is meant to handle only the simple sorts of values that
arise in parsing --args. """
parser = GNValueParser(input_string) return parser.ParseArgs()
def UnescapeGNString(value): """Given a string with GN escaping, returns the unescaped string.
Be careful not to feed with input from a Python parsing function like 'ast' because it will do Python unescaping, which will be incorrect when
fed into the GN unescaper.
Args:
value: Input string to unescape. """
result = ''
i = 0 while i < len(value): if value[i] == '\\': if i < len(value) - 1:
next_char = value[i + 1] if next_char in ('$', '"', '\\'): # These are the escaped characters GN supports.
result += next_char
i += 1 else: # Any other backslash is a literal.
result += '\\' else:
result += value[i]
i += 1 return result
def ReplaceImports(self): """Replaces import(...) lines with the contents of the imports.
Recurses on itself until there are no imports remaining, in the case of
nested imports. """
lines = self.input.splitlines() ifnot any(line.startswith('import(') for line in lines): return for line in lines: ifnot line.startswith('import('): continue
regex_match = IMPORT_RE.match(line) ifnot regex_match: raise GNError('Not a valid import string: %s' % line)
import_path = os.path.join(self.checkout_root, regex_match.group(1)) with open(import_path) as f:
imported_args = f.read()
self.input = self.input.replace(line, imported_args) # Call ourselves again if we've just replaced an import() with additional # imports.
self.ReplaceImports()
# Consume each comment, line by line. whilenot self.IsDone() and self.input[self.cur] == '#': # Consume the rest of the comment, up until the end of the line. whilenot self.IsDone() and self.input[self.cur] != '\n':
self.cur += 1 # Move the cursor to the next line (if there is one). ifnot self.IsDone():
self.cur += 1
self._ConsumeWhitespace()
def Parse(self): """Converts a string representing a printed GN value to the Python type.
See additional usage notes on FromGNString() above.
* GN booleans ('true', 'false') will be converted to Python booleans.
* GN numbers ('123') will be converted to Python numbers.
* GN strings (double-quoted asin'"asdf"') will be converted to Python
strings with GN escaping rules. GN string interpolation (embedded
variables preceded by $) are not supported and will be returned as
literals.
* GN lists ('[1, "asdf", 3]') will be converted to Python lists.
* GN scopes ('{ ... }') are not supported.
Raises:
GNError: Parse fails. """
result = self._ParseAllowTrailing()
self.ConsumeCommentAndWhitespace() ifnot self.IsDone(): raise GNError("Trailing input after parsing:\n " + self.input[self.cur:]) return result
def ParseArgs(self): """Converts a whitespace-separated list of ident=literals to a dict.
See additional usage notes on FromGNArgs(), above.
whilenot self.IsDone():
ident = self._ParseIdent()
self.ConsumeCommentAndWhitespace() if self.input[self.cur] != '=': raise GNError("Unexpected token: " + self.input[self.cur:])
self.cur += 1
self.ConsumeCommentAndWhitespace()
val = self._ParseAllowTrailing()
self.ConsumeCommentAndWhitespace()
d[ident] = val
return d
def _ParseAllowTrailing(self): """Internal version of Parse() that doesn't check for trailing stuff."""
self.ConsumeCommentAndWhitespace() if self.IsDone(): raise GNError("Expected input to parse.")
next_char = self.input[self.cur] while next_char.isalpha() or next_char.isdigit() or next_char=='_':
ident += next_char
self.cur += 1
next_char = self.input[self.cur]
return ident
def ParseNumber(self):
self.ConsumeCommentAndWhitespace() if self.IsDone(): raise GNError('Expected number but got nothing.')
begin = self.cur
# The first character can include a negative sign. ifnot self.IsDone() and _IsDigitOrMinus(self.input[self.cur]):
self.cur += 1 whilenot self.IsDone() and self.input[self.cur].isdigit():
self.cur += 1
number_string = self.input[begin:self.cur] ifnot len(number_string) or number_string == '-': raise GNError('Not a valid number.') return int(number_string)
def ParseString(self):
self.ConsumeCommentAndWhitespace() if self.IsDone(): raise GNError('Expected string but got nothing.')
if self.input[self.cur] != '"': raise GNError('Expected string beginning in a " but got:\n ' +
self.input[self.cur:])
self.cur += 1 # Skip over quote.
begin = self.cur whilenot self.IsDone() and self.input[self.cur] != '"': if self.input[self.cur] == '\\':
self.cur += 1 # Skip over the backslash. if self.IsDone(): raise GNError('String ends in a backslash in:\n ' + self.input)
self.cur += 1
if self.IsDone(): raise GNError('Unterminated string:\n ' + self.input[begin:])
end = self.cur
self.cur += 1 # Consume trailing ".
return UnescapeGNString(self.input[begin:end])
def ParseList(self):
self.ConsumeCommentAndWhitespace() if self.IsDone(): raise GNError('Expected list but got nothing.')
# Skip over opening '['. if self.input[self.cur] != '[': raise GNError('Expected [ for list but got:\n ' + self.input[self.cur:])
self.cur += 1
self.ConsumeCommentAndWhitespace() if self.IsDone(): raise GNError('Unterminated list:\n ' + self.input)
def _ConstantFollows(self, constant): """Checks and maybe consumes a string constant at current input location.
Param:
constant: The string constant to check.
Returns: Trueif |constant| follows immediately at the current location in the
input. In this case, the string is consumed as a side effect. Otherwise,
returns Falseand the current position is unchanged. """
end = self.cur + len(constant) if end > len(self.input): returnFalse# Not enough room. if self.input[self.cur:end] == constant:
self.cur = end returnTrue returnFalse
def ReadBuildVars(output_directory): """Parses $output_directory/build_vars.json into a dict.""" with open(os.path.join(output_directory, BUILD_VARS_FILENAME)) as f: return json.load(f)
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.