parent
22cf9256f8
commit
84a63253a7
100
pyproject.toml
100
pyproject.toml
|
@ -10,7 +10,7 @@ keywords = ['bootconfig', 'xbc', 'configuration']
|
|||
dependencies = ['pyparsing']
|
||||
requires-python = '>=3.7'
|
||||
classifiers = [
|
||||
'Development Status :: 3 - Alpha',
|
||||
'Development Status :: 4 - Beta',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: OS Independent',
|
||||
|
@ -43,3 +43,101 @@ addopts = [
|
|||
"--import-mode=importlib",
|
||||
]
|
||||
pythonpath = 'src'
|
||||
|
||||
[tool.pylint.main]
|
||||
fail-under = 10
|
||||
ignore-patterns = ["^\\.#"]
|
||||
jobs = 0
|
||||
limit-inference-results = 100
|
||||
persistent = true
|
||||
py-version = "3.7"
|
||||
source-roots = 'src'
|
||||
suggestion-mode = true
|
||||
|
||||
[tool.pylint.basic]
|
||||
argument-naming-style = "snake_case"
|
||||
attr-naming-style = "snake_case"
|
||||
bad-names = ["foo", "bar", "baz", "toto", "tutu", "tata"]
|
||||
class-attribute-naming-style = "any"
|
||||
class-const-naming-style = "UPPER_CASE"
|
||||
class-naming-style = "PascalCase"
|
||||
# XBCParser
|
||||
const-naming-style = "PascalCase"
|
||||
docstring-min-length = -1
|
||||
function-naming-style = "snake_case"
|
||||
good-names = ["i", "j", "k", "ex", "Run", "_"]
|
||||
inlinevar-naming-style = "any"
|
||||
method-naming-style = "snake_case"
|
||||
module-naming-style = "snake_case"
|
||||
no-docstring-rgx = "^_"
|
||||
property-classes = ["abc.abstractproperty"]
|
||||
variable-naming-style = "snake_case"
|
||||
|
||||
[tool.pylint.classes]
|
||||
defining-attr-methods = ["__init__", "__new__", "setUp", "asyncSetUp", "__post_init__"]
|
||||
exclude-protected = ["_asdict", "_fields", "_replace", "_source", "_make", "os._exit"]
|
||||
valid-classmethod-first-arg = ["cls"]
|
||||
valid-metaclass-classmethod-first-arg = ["mcs"]
|
||||
|
||||
[tool.pylint.design]
|
||||
exclude-too-few-public-methods = 'XBCNode'
|
||||
max-args = 5
|
||||
max-attributes = 7
|
||||
max-bool-expr = 5
|
||||
max-branches = 12
|
||||
max-locals = 15
|
||||
max-parents = 7
|
||||
max-public-methods = 20
|
||||
max-returns = 6
|
||||
max-statements = 50
|
||||
min-public-methods = 2
|
||||
|
||||
[tool.pylint.exceptions]
|
||||
overgeneral-exceptions = ["builtins.BaseException", "builtins.Exception"]
|
||||
|
||||
[tool.pylint.format]
|
||||
ignore-long-lines = "^\\s*(# )?<?https?://\\S+>?$"
|
||||
indent-after-paren = 4
|
||||
indent-string = " "
|
||||
max-line-length = 100
|
||||
max-module-lines = 1000
|
||||
|
||||
[tool.pylint."messages control"]
|
||||
confidence = ["HIGH", "CONTROL_FLOW", "INFERENCE", "INFERENCE_FAILURE", "UNDEFINED"]
|
||||
disable = ["raw-checker-failed", "bad-inline-option", "locally-disabled", "file-ignored", "suppressed-message", "useless-suppression", "deprecated-pragma", "use-symbolic-message-instead", "use-implicit-booleaness-not-comparison-to-string", "use-implicit-booleaness-not-comparison-to-zero"]
|
||||
|
||||
[tool.pylint.miscellaneous]
|
||||
notes = ["FIXME", "XXX", "TODO"]
|
||||
|
||||
[tool.pylint.refactoring]
|
||||
max-nested-blocks = 5
|
||||
never-returning-functions = ["sys.exit", "argparse.parse_error"]
|
||||
|
||||
[tool.pylint.reports]
|
||||
evaluation = "max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10))"
|
||||
score = true
|
||||
|
||||
[tool.pylint.similarities]
|
||||
ignore-comments = true
|
||||
ignore-docstrings = true
|
||||
ignore-imports = true
|
||||
ignore-signatures = true
|
||||
min-similarity-lines = 4
|
||||
|
||||
[tool.pylint.typecheck]
|
||||
contextmanager-decorators = ["contextlib.contextmanager"]
|
||||
ignore-none = true
|
||||
ignore-on-opaque-inference = true
|
||||
ignored-checks-for-mixins = ["no-member", "not-async-context-manager", "not-context-manager", "attribute-defined-outside-init"]
|
||||
ignored-classes = ["optparse.Values", "thread._local", "_thread._local", "argparse.Namespace"]
|
||||
missing-member-hint = true
|
||||
missing-member-hint-distance = 1
|
||||
missing-member-max-choices = 1
|
||||
mixin-class-rgx = ".*[Mm]ixin"
|
||||
|
||||
[tool.pylint.variables]
|
||||
allow-global-unused-variables = true
|
||||
callbacks = ["cb_", "_cb"]
|
||||
dummy-variables-rgx = "_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_"
|
||||
ignored-argument-names = "_.*|^ignored_|^unused_"
|
||||
redefining-builtins-modules = ["six.moves", "past.builtins", "future.builtins", "builtins", "io"]
|
||||
|
|
|
@ -19,6 +19,17 @@
|
|||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
'''
|
||||
This module implements support for the eXtra Boot Configuration file
|
||||
format specified by the Linux kernel. For more information, please see
|
||||
https://docs.kernel.org/admin-guide/bootconfig.html for more
|
||||
information.
|
||||
|
||||
This is not a strictly-conforming implementation. In particular, this
|
||||
module does not adhere to the kernel's 32,767-byte restriction and does
|
||||
not enforce the maximum depth of 16 namespaces.
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
|
@ -43,102 +54,125 @@ from pyparsing import (
|
|||
from .utils import normalise
|
||||
from .version import version as __version__
|
||||
|
||||
class Node:
|
||||
def __init__(self, *args, type=None):
|
||||
class XBCNode:
|
||||
# pylint: disable=too-few-public-methods
|
||||
'An XBC XBCNode.'
|
||||
def __init__(self, *args, kind=None):
|
||||
if isinstance(args[0], str):
|
||||
self.args = args[0]
|
||||
elif isinstance(args[0][0], str):
|
||||
self.args = args[0][0]
|
||||
else:
|
||||
self.args = args[0][0][0]
|
||||
self.type = type
|
||||
self.type = kind
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
'The key associated with the node.'
|
||||
return self.args[0]
|
||||
|
||||
class Key(Node):
|
||||
class XBCKey(XBCNode):
|
||||
# pylint: disable=too-few-public-methods
|
||||
'An XBC key.'
|
||||
def __init__(self, *args):
|
||||
# pylint: disable=super-init-not-called
|
||||
self.args = args[0]
|
||||
self.type = 'key'
|
||||
|
||||
class KeyValue(Node):
|
||||
class XBCKeyValue(XBCNode):
|
||||
'An XBC key/value operation.'
|
||||
def __init__(self, *args):
|
||||
super().__init__(args, type='kv')
|
||||
super().__init__(args, kind='kv')
|
||||
|
||||
@property
|
||||
def op(self):
|
||||
'The operator being performed.'
|
||||
return self.args[1]
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
'The data associated with the operation.'
|
||||
return self.args[2]
|
||||
|
||||
class Block(Node):
|
||||
class XBCBlock(XBCNode):
|
||||
'An XBC block.'
|
||||
def __init__(self, *args):
|
||||
super().__init__(args, type='block')
|
||||
super().__init__(args, kind='block')
|
||||
|
||||
@property
|
||||
def contents(self):
|
||||
'The contents of the block.'
|
||||
return self.args[1]
|
||||
|
||||
key_fragment = Word(alphas + nums + '_-')
|
||||
key = DelimitedList(key_fragment, delim='.', combine=True)
|
||||
|
||||
bareval = CharsNotIn('{}#=+:;,\n\'"')
|
||||
strvals = QuotedString("'", multiline=True, unquote_results=False)
|
||||
strvald = QuotedString('"', multiline=True, unquote_results=False)
|
||||
value = bareval | strvald | strvals
|
||||
|
||||
assign = Literal('=')
|
||||
update = Literal(':=')
|
||||
append = Literal('+=')
|
||||
op = assign | update | append
|
||||
semi = Literal(';').suppress()
|
||||
lbrace = Literal('{').suppress()
|
||||
rbrace = Literal('}').suppress()
|
||||
terminal = Word(';\n').suppress()
|
||||
|
||||
NL = Literal('\n').suppress()
|
||||
WS = Word(' \t').suppress()
|
||||
WS_NL = Word(' \t\n').suppress()
|
||||
comment = Literal('#') + restOfLine
|
||||
|
||||
values = Group(value + ZeroOrMore(Literal(',').suppress() + Optional(WS_NL) + value), aslist=True)
|
||||
|
||||
keyvalue = Group(key + Optional(WS) + op + Optional(WS) + values, aslist=True)
|
||||
keyvalue.set_parse_action(lambda x: KeyValue(x))
|
||||
|
||||
key_stmt = key + Optional(WS) + Optional(assign).suppress() + Optional(WS)
|
||||
key_stmt.set_parse_action(lambda x: Key(x))
|
||||
|
||||
block = Forward()
|
||||
statement = (keyvalue | key_stmt)
|
||||
statements = DelimitedList(statement, delim=terminal)
|
||||
segment = Group(OneOrMore(block | statements), aslist=True)
|
||||
|
||||
block << Group(key + Optional(WS) + lbrace + segment + rbrace + Optional(NL))
|
||||
block.set_parse_action(lambda x: Block(x))
|
||||
|
||||
data = OneOrMore(segment)
|
||||
|
||||
XBCParser = data
|
||||
|
||||
XBCParser.ignore(comment)
|
||||
XBCParser = None
|
||||
|
||||
class ParseError(Exception):
|
||||
pass
|
||||
'Exception for parsing errors.'
|
||||
|
||||
def lex(data):
|
||||
tree = XBCParser.parseString(data).asList()
|
||||
return tree
|
||||
# pylint: disable=too-many-locals,global-statement,unnecessary-lambda
|
||||
'Run the lexer over the provided data.'
|
||||
global XBCParser
|
||||
|
||||
if XBCParser is None:
|
||||
key_fragment = Word(alphas + nums + '_-')
|
||||
key = DelimitedList(key_fragment, delim='.', combine=True)
|
||||
|
||||
bareval = CharsNotIn('{}#=+:;,\n\'"')
|
||||
strvals = QuotedString("'", multiline=True, unquote_results=False)
|
||||
strvald = QuotedString('"', multiline=True, unquote_results=False)
|
||||
value = bareval | strvald | strvals
|
||||
|
||||
assign = Literal('=')
|
||||
update = Literal(':=')
|
||||
append = Literal('+=')
|
||||
op = assign | update | append
|
||||
lbrace = Literal('{').suppress()
|
||||
rbrace = Literal('}').suppress()
|
||||
terminal = Word(';\n').suppress()
|
||||
|
||||
NL = Literal('\n').suppress() # pylint: disable=invalid-name
|
||||
WS = Word(' \t').suppress() # pylint: disable=invalid-name
|
||||
WS_NL = Word(' \t\n').suppress() # pylint: disable=invalid-name
|
||||
comment = Literal('#') + restOfLine
|
||||
|
||||
values = Group(
|
||||
value + ZeroOrMore(
|
||||
Literal(',').suppress() +
|
||||
Optional(WS_NL) + value
|
||||
), aslist=True
|
||||
)
|
||||
|
||||
keyvalue = Group(key + Optional(WS) + op + Optional(WS) + values, aslist=True)
|
||||
keyvalue.set_parse_action(lambda x: XBCKeyValue(x))
|
||||
|
||||
key_stmt = key + Optional(WS) + Optional(assign).suppress() + Optional(WS)
|
||||
key_stmt.set_parse_action(lambda x: XBCKey(x))
|
||||
|
||||
block = Forward()
|
||||
statement = keyvalue | key_stmt
|
||||
statements = DelimitedList(statement, delim=terminal)
|
||||
segment = Group(OneOrMore(block | statements), aslist=True)
|
||||
|
||||
# pylint: disable=expression-not-assigned
|
||||
block << Group(key + Optional(WS) + lbrace + segment + rbrace + Optional(NL))
|
||||
block.set_parse_action(lambda x: XBCBlock(x))
|
||||
|
||||
XBCParser = OneOrMore(segment)
|
||||
|
||||
XBCParser.ignore(comment)
|
||||
|
||||
# pylint: disable=too-many-function-args
|
||||
return XBCParser.parseString(data).asList()
|
||||
|
||||
def unquote(val):
|
||||
'Remove quotes and trailing whitespace from values.'
|
||||
if val[0] in '\'"' and val[0] == val[-1]:
|
||||
return val[1:-1]
|
||||
return val.strip()
|
||||
|
||||
def key_walk(d, key):
|
||||
'Walk the key to guard against post-block key assignments.'
|
||||
split = key.split('.')
|
||||
|
||||
for i in range(len(split) - 1, 0, -1):
|
||||
|
@ -148,6 +182,8 @@ def key_walk(d, key):
|
|||
d[x] = False
|
||||
|
||||
def parse_block(key, seq):
|
||||
# pylint: disable=too-many-branches,too-many-statements
|
||||
'Parse the AST in to a real data structure.'
|
||||
if isinstance(seq, list) and len(seq) == 1 and isinstance(seq[0], list):
|
||||
seq = seq[0]
|
||||
|
||||
|
@ -159,12 +195,13 @@ def parse_block(key, seq):
|
|||
else:
|
||||
k = item.key
|
||||
|
||||
if isinstance(item, Key):
|
||||
if isinstance(item, XBCKey):
|
||||
if k not in ret:
|
||||
ret[k] = True
|
||||
key_walk(ret, k)
|
||||
else:
|
||||
raise ParseError(f'key {k} already defined')
|
||||
elif isinstance(item, KeyValue):
|
||||
elif isinstance(item, XBCKeyValue):
|
||||
value = item.value
|
||||
op = item.op
|
||||
|
||||
|
@ -189,8 +226,8 @@ def parse_block(key, seq):
|
|||
assign = value
|
||||
|
||||
if isinstance(assign, list):
|
||||
for i in range(len(assign)):
|
||||
assign[i] = unquote(assign[i])
|
||||
for i, item in enumerate(assign):
|
||||
assign[i] = unquote(item)
|
||||
else:
|
||||
assign = unquote(assign)
|
||||
|
||||
|
@ -201,7 +238,7 @@ def parse_block(key, seq):
|
|||
|
||||
if '.' in k:
|
||||
key_walk(ret, k)
|
||||
elif isinstance(item, Block):
|
||||
elif isinstance(item, XBCBlock):
|
||||
value = item.contents
|
||||
|
||||
if k not in ret:
|
||||
|
@ -220,6 +257,7 @@ def parse_block(key, seq):
|
|||
return ret
|
||||
|
||||
def parse(data):
|
||||
'Call the lexer and then the parser.'
|
||||
tree = lex(data)
|
||||
|
||||
d = parse_block(None, tree)
|
||||
|
@ -227,14 +265,17 @@ def parse(data):
|
|||
return d
|
||||
|
||||
def loads_xbc(data):
|
||||
'Load XBC data provided in a string.'
|
||||
return parse(data)
|
||||
|
||||
def load_xbc(fp):
|
||||
with open(fp, mode='r') as f:
|
||||
'Open a file and parse its contents.'
|
||||
with open(fp, mode='r', encoding='UTF-8') as f:
|
||||
return loads_xbc(f.read())
|
||||
|
||||
def longest_key(L):
|
||||
lens = [len(x) for x in L]
|
||||
def longest_key(seq):
|
||||
'Find the deepest-nested key in the sequence provided.'
|
||||
lens = [len(x) for x in seq]
|
||||
shortest = min(lens)
|
||||
|
||||
if shortest < 1:
|
||||
|
@ -245,7 +286,7 @@ def longest_key(L):
|
|||
for i in range(shortest):
|
||||
count = {}
|
||||
|
||||
for item in L:
|
||||
for item in seq:
|
||||
j = item[i]
|
||||
|
||||
if j not in count:
|
||||
|
@ -253,40 +294,42 @@ def longest_key(L):
|
|||
|
||||
count[j] += 1
|
||||
if len(count.keys()) == 1:
|
||||
ret.append(L[0][i])
|
||||
ret.append(seq[0][i])
|
||||
else:
|
||||
return '.'.join(ret)
|
||||
return None
|
||||
|
||||
def longest_keys(keys):
|
||||
'Find the longest keys in the sequence provided.'
|
||||
keys = [k.split('.') for k in keys]
|
||||
ret = set()
|
||||
|
||||
for i in range(len(keys)):
|
||||
for j in range(1, len(keys)):
|
||||
longest = longest_key([keys[i], keys[j]])
|
||||
for a in keys:
|
||||
for b in keys[1:]:
|
||||
longest = longest_key([a, b])
|
||||
if longest is not None:
|
||||
ret.add(longest)
|
||||
ret.discard('')
|
||||
return ret
|
||||
|
||||
def make_block(data):
|
||||
# pylint: disable=too-many-locals,too-many-branches
|
||||
'Create XBC blocks.'
|
||||
ret = []
|
||||
|
||||
leafs = []
|
||||
blocks = set()
|
||||
block_keys = []
|
||||
|
||||
for key in data.keys():
|
||||
if '.' not in key:
|
||||
leafs.append(key)
|
||||
else:
|
||||
k, rest = key.split('.', maxsplit=1)
|
||||
k, _ = key.split('.', maxsplit=1)
|
||||
blocks.add(k)
|
||||
|
||||
keys = [k for k in data.keys() if '.' in k]
|
||||
temp = longest_keys(keys)
|
||||
if len(temp):
|
||||
if temp:
|
||||
mindots = 99
|
||||
for i in temp:
|
||||
if 0 < i.count('.') < mindots:
|
||||
|
@ -321,11 +364,13 @@ def make_block(data):
|
|||
return ret
|
||||
|
||||
def saves_xbc(data):
|
||||
'Export the provided dictionary to an XBC-formatted string.'
|
||||
ret = make_block(data)
|
||||
return '\n'.join(ret)
|
||||
|
||||
def save_xbc(data, filename):
|
||||
with open(filename, mode='w') as f:
|
||||
'Export the provided dictionary to an XBC-formatted string and save it.'
|
||||
with open(filename, mode='w', encoding='UTF-8') as f:
|
||||
f.write(saves_xbc(data))
|
||||
|
||||
__all__ = ['loads_xbc', 'load_xbc', 'saves_xbc', 'save_xbc', 'ParseError']
|
||||
|
|
|
@ -19,29 +19,37 @@
|
|||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
'''
|
||||
Utility functions for saving data in XBC format.
|
||||
'''
|
||||
|
||||
import re
|
||||
|
||||
from collections.abc import Sequence, Mapping
|
||||
|
||||
KEY_RE = re.compile(r'^[a-zA-Z0-9_-]+(?:\.(?:[a-zA-Z0-9_-]+))*$')
|
||||
NVAL_RE = re.compile(r'^[^{}=+:;,\n\'"]+$')
|
||||
quotes = '\'"'
|
||||
other = {'"': "'", "'": '"'}
|
||||
escapes = {
|
||||
QUOTES = '\'"'
|
||||
QUOTEMAP = {'"': "'", "'": '"'}
|
||||
ESCAPES = {
|
||||
'backslash': {'"': '\\x22', "'": '\\x27'},
|
||||
'html': {'"': '"', "'": '''},
|
||||
'url': {'"': '%22', "'": '%27'}
|
||||
}
|
||||
|
||||
def quote(data, escape='backslash'):
|
||||
'Quote data according to XBC rules.'
|
||||
# don't waste our time if it's a valid bare value.
|
||||
if NVAL_RE.match(data) is not None:
|
||||
return data
|
||||
esc = None
|
||||
|
||||
# how shall we escape embedded quotes?
|
||||
if isinstance(esc, Mapping):
|
||||
if '"' in escape and "'" in escape:
|
||||
esc = escape
|
||||
elif escape in escapes:
|
||||
esc = escapes[escape]
|
||||
else:
|
||||
esc = ESCAPES.get(escape, None)
|
||||
|
||||
if esc is None:
|
||||
raise ValueError('unrecognised escape format')
|
||||
|
@ -49,30 +57,30 @@ def quote(data, escape='backslash'):
|
|||
f = data[0]
|
||||
|
||||
# is this a quoted string?
|
||||
if f in quotes and data[-1] == f:
|
||||
if f in QUOTES and data[-1] == f:
|
||||
# return it if we don't need to do anything
|
||||
if f not in data[1:-1]:
|
||||
return data
|
||||
else:
|
||||
# escape embedded quotes
|
||||
x = data[1:-1].replace(f, esc[f])
|
||||
return f'{f}{x}{f}'
|
||||
else:
|
||||
|
||||
# if the other quote isn't used, wrap in it
|
||||
if f in quotes and other[f] not in data[1:]:
|
||||
q = other[f]
|
||||
return f'{q}{data}{q}'
|
||||
if f in QUOTES and QUOTEMAP[f] not in data[1:]:
|
||||
q = QUOTEMAP[f]
|
||||
# not a quoted string, but has only one kind of quote
|
||||
elif "'" in data and '"' not in data:
|
||||
return f'"{data}"'
|
||||
q = '"'
|
||||
elif '"' in data and "'" not in data:
|
||||
return f"'{data}'"
|
||||
# not a quoted string and has both types; we escape one
|
||||
q = "'"
|
||||
else:
|
||||
# not a quoted string and has both types; we escape one
|
||||
data = data.replace("'", esc["'"])
|
||||
return f"'{data}"
|
||||
q = "'"
|
||||
return f'{q}{data}{q}'
|
||||
|
||||
def normalise_string(string):
|
||||
'Normalise values according to XBC rules.'
|
||||
if not isinstance(string, str):
|
||||
string = str(string)
|
||||
|
||||
|
@ -82,36 +90,41 @@ def normalise_string(string):
|
|||
return string
|
||||
|
||||
def normalise(data):
|
||||
'''Normalise values according to XBC rules.'''
|
||||
if isinstance(data, str) or not isinstance(data, (Sequence, Mapping)):
|
||||
return normalise_string(data)
|
||||
elif isinstance(data, Sequence):
|
||||
L = []
|
||||
|
||||
if isinstance(data, Sequence):
|
||||
ret = []
|
||||
|
||||
for item in data:
|
||||
if isinstance(item, str) or not isinstance(item, (Sequence, Mapping)):
|
||||
L.append(normalise_string(item))
|
||||
ret.append(normalise_string(item))
|
||||
# we can unwind nested sequences
|
||||
elif isinstance(item, Sequence):
|
||||
L.extend(normalise(item))
|
||||
ret.extend(normalise(item))
|
||||
# ...but we can't do that with mappings, the format doesn't
|
||||
# support it.
|
||||
elif isinstance(item, Mapping):
|
||||
raise ValueError('nested mapping')
|
||||
else:
|
||||
raise ValueError(type(value))
|
||||
L = ', '.join(L)
|
||||
return L
|
||||
elif isinstance(data, Mapping):
|
||||
# this should be impossible to reach, but nothing truly is.
|
||||
raise ValueError(type(item))
|
||||
|
||||
ret = ', '.join(ret)
|
||||
|
||||
return ret
|
||||
|
||||
if isinstance(data, Mapping):
|
||||
d = {}
|
||||
for k, v in data.items():
|
||||
if Key.pattern.match(k) is None:
|
||||
if KEY_RE.match(k) is None:
|
||||
raise KeyError(k)
|
||||
else:
|
||||
k = Key(k)
|
||||
|
||||
v = normalise(v)
|
||||
|
||||
d[k] = v
|
||||
|
||||
return d
|
||||
|
||||
return data
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
# pylint: disable=missing-module-docstring,invalid-name
|
||||
version = '0.1.0'
|
|
@ -1,49 +1,67 @@
|
|||
|
||||
'''
|
||||
Tests for base-level keys.
|
||||
'''
|
||||
|
||||
import pytest
|
||||
|
||||
from xbc import loads_xbc, ParseError
|
||||
|
||||
|
||||
def test_key():
|
||||
'A key by itself exists.'
|
||||
assert loads_xbc('a') == {'a': True}
|
||||
|
||||
def test_dot_key():
|
||||
assert loads_xbc('a.a') == {'a.a': True}
|
||||
'A nested key by itself exists.'
|
||||
assert loads_xbc('a.a') == {'a': False, 'a.a': True}
|
||||
|
||||
def test_key_eq():
|
||||
'A key with an empty assignment exists.'
|
||||
assert loads_xbc('a =') == {'a': True}
|
||||
|
||||
def test_keyvalue():
|
||||
'A bare value can be assigned to a key.'
|
||||
assert loads_xbc('a = 1') == {'a': '1'}
|
||||
|
||||
def test_keyvalue_space():
|
||||
'A bare value can have spaces.'
|
||||
assert loads_xbc('a = a b') == {'a': 'a b'}
|
||||
|
||||
def test_dot_keyvalue():
|
||||
'A key being assigned to can have dots.'
|
||||
assert loads_xbc('a.a = 1') == {'a': False, 'a.a': '1'}
|
||||
|
||||
def test_keys():
|
||||
'Statements can be separated by semicolons.'
|
||||
assert loads_xbc('a;b') == {'a': True, 'b': True}
|
||||
|
||||
def test_dot_keys():
|
||||
assert loads_xbc('a.a;a.b') == {'a.a': True, 'a.b': True}
|
||||
'Keys in compound statements can have dots.'
|
||||
assert loads_xbc('a.a;a.b') == {'a': False, 'a.a': True, 'a.b': True}
|
||||
|
||||
def test_quoted():
|
||||
'Values can be quoted with single or double quotes.'
|
||||
assert loads_xbc('a = "b"') == {'a': 'b'}
|
||||
|
||||
def test_quoted_space():
|
||||
'Quoted values can have trailing whitespace preserved.'
|
||||
assert loads_xbc('a = "b "') == {'a': 'b '}
|
||||
|
||||
def test_array():
|
||||
'Multiple values can be assigned to a single key.'
|
||||
assert loads_xbc('a = 1, 2') == {'a': ['1', '2']}
|
||||
|
||||
def test_reassignment():
|
||||
'Keys cannot be reassigned.'
|
||||
with pytest.raises(ParseError):
|
||||
loads_xbc('a = 1\na = 2')
|
||||
|
||||
def test_reassignment_colon():
|
||||
'Keys cannot be reassigned, even in compound statements.'
|
||||
with pytest.raises(ParseError):
|
||||
loads_xbc('a = 1;a = 2')
|
||||
|
||||
def test_ovewrite_nonexistent():
|
||||
'Keys can only be updated if they exist.'
|
||||
with pytest.raises(ParseError):
|
||||
loads_xbc('a := 1')
|
||||
|
|
|
@ -1,24 +1,34 @@
|
|||
|
||||
'''
|
||||
Test output for XBC blocks.
|
||||
'''
|
||||
|
||||
import pytest
|
||||
|
||||
from xbc import loads_xbc, ParseError
|
||||
|
||||
def test_empty():
|
||||
'An empty block should cause the key to exist.'
|
||||
assert loads_xbc('a {}') == {'a': True}
|
||||
|
||||
def test_keyvalue():
|
||||
'A block should support key/value pairs.'
|
||||
assert loads_xbc('a { a = 1 }') == {'a': False, 'a.a': '1'}
|
||||
|
||||
def test_nested_block():
|
||||
'A block should support having block members.'
|
||||
assert loads_xbc('a { b { c = 1 } }') == {'a.b.c': '1', 'a': False, 'a.b': False}
|
||||
|
||||
def test_keyvalue_and_block():
|
||||
assert loads_xbc('a = 1\na { a = 1 }') == {'a': False, 'a': '1', 'a.a': '1'}
|
||||
'A key/value pair can be provided for a block key prior to the block.'
|
||||
assert loads_xbc('a = 1\na { a = 1 }') == {'a': '1', 'a.a': '1'}
|
||||
|
||||
def test_reassign_colon():
|
||||
'Attempting to assign to the same key should be an error inside a block too.'
|
||||
with pytest.raises(ParseError):
|
||||
loads_xbc('a { a = 1; a = 2 }')
|
||||
|
||||
def test_assign_after_block():
|
||||
'It is an error to assign to a block key after the block.'
|
||||
with pytest.raises(ParseError):
|
||||
loads_xbc('a { a = 1 }\na = 1')
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
|
||||
'''
|
||||
Tests for inputs which are invalid.
|
||||
'''
|
||||
|
||||
import pytest
|
||||
|
||||
from pyparsing.exceptions import ParseException
|
||||
|
@ -7,36 +11,44 @@ from xbc import loads_xbc, ParseError
|
|||
# this should fail but does not.
|
||||
@pytest.mark.xfail
|
||||
def test_whitespace_vc():
|
||||
'Whitespace is prohibited between values and commas.'
|
||||
with pytest.raises(ParseError):
|
||||
loads_xbc('x = a\n, b')
|
||||
|
||||
# this should fail but does not.
|
||||
@pytest.mark.xfail
|
||||
def test_extra_quote():
|
||||
'Strings cannot contain the quote style that bookends them.'
|
||||
with pytest.raises(ParseException):
|
||||
loads_xbc("x = '''")
|
||||
|
||||
def test_lone_plus():
|
||||
'Bare operators are not permitted.'
|
||||
with pytest.raises(ParseException):
|
||||
loads_xbc('+')
|
||||
|
||||
def test_lone_rbrace():
|
||||
'Braces can only appear as part of a block.'
|
||||
with pytest.raises(ParseException):
|
||||
loads_xbc('}')
|
||||
|
||||
def test_lone_lbrace():
|
||||
'Braces can only appear as part of a block.'
|
||||
with pytest.raises(ParseException):
|
||||
loads_xbc('{')
|
||||
|
||||
def test_lone_braces():
|
||||
'Braces can only appear as part of a block.'
|
||||
with pytest.raises(ParseException):
|
||||
loads_xbc('{}')
|
||||
|
||||
def test_lone_semi():
|
||||
'Semicolons are only permitted as part of a key or key/value statement.'
|
||||
with pytest.raises(ParseException):
|
||||
loads_xbc(';')
|
||||
|
||||
def test_empty():
|
||||
'XBC files cannot be empty.'
|
||||
with pytest.raises(ParseException):
|
||||
loads_xbc('\n')
|
||||
with pytest.raises(ParseException):
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
|
||||
import pytest
|
||||
|
||||
from xbc import loads_xbc
|
||||
|
||||
'''
|
||||
The tests in this file are samples drawn from
|
||||
https://lwn.net/Articles/806002/.
|
||||
'''
|
||||
|
||||
import pytest
|
||||
|
||||
from xbc import loads_xbc
|
||||
|
||||
def test_01():
|
||||
'Key/value example.'
|
||||
i = '''feature.option.foo = 1
|
||||
feature.option.bar = 2'''
|
||||
d = {
|
||||
|
@ -20,6 +21,7 @@ feature.option.bar = 2'''
|
|||
assert loads_xbc(i) == d
|
||||
|
||||
def test_02():
|
||||
'Block example.'
|
||||
i = '''feature.option {
|
||||
foo = 1
|
||||
bar = 2
|
||||
|
@ -33,6 +35,7 @@ def test_02():
|
|||
assert loads_xbc(i) == d
|
||||
|
||||
def test_03():
|
||||
'Array example.'
|
||||
i = 'feature.options = "foo", "bar"'
|
||||
d = {
|
||||
'feature': False,
|
||||
|
@ -41,6 +44,7 @@ def test_03():
|
|||
assert loads_xbc(i) == d
|
||||
|
||||
def test_10():
|
||||
'Compact example.'
|
||||
i = 'feature.option{foo=1;bar=2}'
|
||||
d = {
|
||||
'feature.option': False,
|
||||
|
@ -51,6 +55,7 @@ def test_10():
|
|||
assert loads_xbc(i) == d
|
||||
|
||||
def test_11():
|
||||
'Example of a possible configuration.'
|
||||
i = '''ftrace.event {
|
||||
task.task_newtask {
|
||||
filter = "pid < 128"
|
||||
|
@ -87,16 +92,21 @@ def test_11():
|
|||
'ftrace.event.synthetic': False,
|
||||
'ftrace.event.synthetic.initcall_latency': False,
|
||||
'ftrace.event.synthetic.initcall_latency.fields': ["unsigned long func", "u64 lat"],
|
||||
'ftrace.event.synthetic.initcall_latency.actions': "hist:keys=func.sym,lat:vals=lat:sort=lat",
|
||||
'ftrace.event.synthetic.initcall_latency.actions':
|
||||
"hist:keys=func.sym,lat:vals=lat:sort=lat",
|
||||
'ftrace.event.initcall': False,
|
||||
'ftrace.event.initcall.initcall_start': False,
|
||||
'ftrace.event.initcall.initcall_start.actions': "hist:keys=func:ts0=common_timestamp.usecs",
|
||||
'ftrace.event.initcall.initcall_start.actions':
|
||||
"hist:keys=func:ts0=common_timestamp.usecs",
|
||||
'ftrace.event.initcall.initcall_finish': False,
|
||||
'ftrace.event.initcall.initcall_finish.actions': "hist:keys=func:lat=common_timestamp.usecs-$ts0:onmatch(initcall.initcall_start).initcall_latency(func,$lat)"
|
||||
'ftrace.event.initcall.initcall_finish.actions':
|
||||
"hist:keys=func:lat=common_timestamp.usecs-$ts0:onmatch(" +
|
||||
"initcall.initcall_start).initcall_latency(func,$lat)"
|
||||
}
|
||||
assert loads_xbc(i) == d
|
||||
|
||||
def test_12():
|
||||
'Another example of a possible configuration.'
|
||||
i = '''ftrace.event.synthetic.initcall_latency {
|
||||
fields = "unsigned long func", "u64 lat"
|
||||
hist {
|
||||
|
@ -130,7 +140,8 @@ def test_12():
|
|||
'ftrace.event.synthetic.initcall_latency.hist.to': False,
|
||||
'ftrace.event.synthetic.initcall_latency.hist.to.event': 'initcall.initcall_finish',
|
||||
'ftrace.event.synthetic.initcall_latency.hist.to.key': 'func',
|
||||
'ftrace.event.synthetic.initcall_latency.hist.to.assigns': "lat=common_timestamp.usecs-$ts0",
|
||||
'ftrace.event.synthetic.initcall_latency.hist.to.assigns':
|
||||
"lat=common_timestamp.usecs-$ts0",
|
||||
'ftrace.event.synthetic.initcall_latency.hist.to.onmatch': ['func', '$lat'],
|
||||
'ftrace.event.synthetic.initcall_latency.hist.keys': ['func.sym', 'lat'],
|
||||
'ftrace.event.synthetic.initcall_latency.hist.vals': 'lat',
|
||||
|
|
Loading…
Reference in New Issue