parent
22cf9256f8
commit
84a63253a7
|
@ -54,9 +54,9 @@ Keys are composable. The following examples are equivalent:
|
||||||
|
|
||||||
```xbc
|
```xbc
|
||||||
foo {
|
foo {
|
||||||
bar {
|
bar {
|
||||||
fluff = 1
|
fluff = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
# is equivalent to
|
# is equivalent to
|
||||||
foo.bar.fluff = 1
|
foo.bar.fluff = 1
|
||||||
|
|
126
pyproject.toml
126
pyproject.toml
|
@ -2,7 +2,7 @@
|
||||||
name = 'py-xbc'
|
name = 'py-xbc'
|
||||||
dynamic = ['version']
|
dynamic = ['version']
|
||||||
authors = [
|
authors = [
|
||||||
{ name = 'Síle Ekaterin Liszka', email = 'sheila@vulpine.house' }
|
{ name = 'Síle Ekaterin Liszka', email = 'sheila@vulpine.house' }
|
||||||
]
|
]
|
||||||
description = 'A library for manipulating eXtra BootConfig (XBC) files'
|
description = 'A library for manipulating eXtra BootConfig (XBC) files'
|
||||||
readme = 'README.md'
|
readme = 'README.md'
|
||||||
|
@ -10,18 +10,18 @@ keywords = ['bootconfig', 'xbc', 'configuration']
|
||||||
dependencies = ['pyparsing']
|
dependencies = ['pyparsing']
|
||||||
requires-python = '>=3.7'
|
requires-python = '>=3.7'
|
||||||
classifiers = [
|
classifiers = [
|
||||||
'Development Status :: 3 - Alpha',
|
'Development Status :: 4 - Beta',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
'License :: OSI Approved :: MIT License',
|
'License :: OSI Approved :: MIT License',
|
||||||
'Operating System :: OS Independent',
|
'Operating System :: OS Independent',
|
||||||
'Programming Language :: Python :: 3',
|
'Programming Language :: Python :: 3',
|
||||||
'Programming Language :: Python :: 3.7',
|
'Programming Language :: Python :: 3.7',
|
||||||
'Programming Language :: Python :: 3.8',
|
'Programming Language :: Python :: 3.8',
|
||||||
'Programming Language :: Python :: 3.9',
|
'Programming Language :: Python :: 3.9',
|
||||||
'Programming Language :: Python :: 3.10',
|
'Programming Language :: Python :: 3.10',
|
||||||
'Programming Language :: Python :: 3.11',
|
'Programming Language :: Python :: 3.11',
|
||||||
'Programming Language :: Python :: 3.12',
|
'Programming Language :: Python :: 3.12',
|
||||||
'Topic :: Utilities'
|
'Topic :: Utilities'
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
|
@ -42,4 +42,102 @@ version = {attr = "xbc.version.version"}
|
||||||
addopts = [
|
addopts = [
|
||||||
"--import-mode=importlib",
|
"--import-mode=importlib",
|
||||||
]
|
]
|
||||||
pythonpath = 'src'
|
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,313 +19,358 @@
|
||||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# 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
|
import re
|
||||||
|
|
||||||
from collections.abc import Mapping, Sequence
|
from collections.abc import Mapping, Sequence
|
||||||
|
|
||||||
from pyparsing import (
|
from pyparsing import (
|
||||||
alphas,
|
alphas,
|
||||||
CharsNotIn,
|
CharsNotIn,
|
||||||
DelimitedList,
|
DelimitedList,
|
||||||
Forward,
|
Forward,
|
||||||
Group,
|
Group,
|
||||||
nums,
|
nums,
|
||||||
Literal,
|
Literal,
|
||||||
OneOrMore,
|
OneOrMore,
|
||||||
Optional,
|
Optional,
|
||||||
Regex,
|
Regex,
|
||||||
restOfLine,
|
restOfLine,
|
||||||
QuotedString,
|
QuotedString,
|
||||||
Word,
|
Word,
|
||||||
ZeroOrMore
|
ZeroOrMore
|
||||||
)
|
)
|
||||||
|
|
||||||
from .utils import normalise
|
from .utils import normalise
|
||||||
from .version import version as __version__
|
from .version import version as __version__
|
||||||
|
|
||||||
class Node:
|
class XBCNode:
|
||||||
def __init__(self, *args, type=None):
|
# pylint: disable=too-few-public-methods
|
||||||
if isinstance(args[0], str):
|
'An XBC XBCNode.'
|
||||||
self.args = args[0]
|
def __init__(self, *args, kind=None):
|
||||||
elif isinstance(args[0][0], str):
|
if isinstance(args[0], str):
|
||||||
self.args = args[0][0]
|
self.args = args[0]
|
||||||
else:
|
elif isinstance(args[0][0], str):
|
||||||
self.args = args[0][0][0]
|
self.args = args[0][0]
|
||||||
self.type = type
|
else:
|
||||||
|
self.args = args[0][0][0]
|
||||||
|
self.type = kind
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def key(self):
|
def key(self):
|
||||||
return self.args[0]
|
'The key associated with the node.'
|
||||||
|
return self.args[0]
|
||||||
|
|
||||||
class Key(Node):
|
class XBCKey(XBCNode):
|
||||||
def __init__(self, *args):
|
# pylint: disable=too-few-public-methods
|
||||||
self.args = args[0]
|
'An XBC key.'
|
||||||
self.type = 'key'
|
def __init__(self, *args):
|
||||||
|
# pylint: disable=super-init-not-called
|
||||||
|
self.args = args[0]
|
||||||
|
self.type = 'key'
|
||||||
|
|
||||||
class KeyValue(Node):
|
class XBCKeyValue(XBCNode):
|
||||||
def __init__(self, *args):
|
'An XBC key/value operation.'
|
||||||
super().__init__(args, type='kv')
|
def __init__(self, *args):
|
||||||
|
super().__init__(args, kind='kv')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def op(self):
|
def op(self):
|
||||||
return self.args[1]
|
'The operator being performed.'
|
||||||
|
return self.args[1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def value(self):
|
def value(self):
|
||||||
return self.args[2]
|
'The data associated with the operation.'
|
||||||
|
return self.args[2]
|
||||||
|
|
||||||
class Block(Node):
|
class XBCBlock(XBCNode):
|
||||||
def __init__(self, *args):
|
'An XBC block.'
|
||||||
super().__init__(args, type='block')
|
def __init__(self, *args):
|
||||||
|
super().__init__(args, kind='block')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def contents(self):
|
def contents(self):
|
||||||
return self.args[1]
|
'The contents of the block.'
|
||||||
|
return self.args[1]
|
||||||
|
|
||||||
key_fragment = Word(alphas + nums + '_-')
|
XBCParser = None
|
||||||
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)
|
|
||||||
|
|
||||||
class ParseError(Exception):
|
class ParseError(Exception):
|
||||||
pass
|
'Exception for parsing errors.'
|
||||||
|
|
||||||
def lex(data):
|
def lex(data):
|
||||||
tree = XBCParser.parseString(data).asList()
|
# pylint: disable=too-many-locals,global-statement,unnecessary-lambda
|
||||||
return tree
|
'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):
|
def unquote(val):
|
||||||
if val[0] in '\'"' and val[0] == val[-1]:
|
'Remove quotes and trailing whitespace from values.'
|
||||||
return val[1:-1]
|
if val[0] in '\'"' and val[0] == val[-1]:
|
||||||
return val.strip()
|
return val[1:-1]
|
||||||
|
return val.strip()
|
||||||
|
|
||||||
def key_walk(d, key):
|
def key_walk(d, key):
|
||||||
split = key.split('.')
|
'Walk the key to guard against post-block key assignments.'
|
||||||
|
split = key.split('.')
|
||||||
|
|
||||||
for i in range(len(split) - 1, 0, -1):
|
for i in range(len(split) - 1, 0, -1):
|
||||||
x = '.'.join(split[:i])
|
x = '.'.join(split[:i])
|
||||||
print(x)
|
print(x)
|
||||||
if x not in d:
|
if x not in d:
|
||||||
d[x] = False
|
d[x] = False
|
||||||
|
|
||||||
def parse_block(key, seq):
|
def parse_block(key, seq):
|
||||||
if isinstance(seq, list) and len(seq) == 1 and isinstance(seq[0], list):
|
# pylint: disable=too-many-branches,too-many-statements
|
||||||
seq = seq[0]
|
'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]
|
||||||
|
|
||||||
ret = {}
|
ret = {}
|
||||||
|
|
||||||
for item in seq:
|
for item in seq:
|
||||||
if key is not None:
|
if key is not None:
|
||||||
k = f'{key}.{item.key}'
|
k = f'{key}.{item.key}'
|
||||||
else:
|
else:
|
||||||
k = item.key
|
k = item.key
|
||||||
|
|
||||||
if isinstance(item, Key):
|
if isinstance(item, XBCKey):
|
||||||
if k not in ret:
|
if k not in ret:
|
||||||
ret[k] = True
|
ret[k] = True
|
||||||
else:
|
key_walk(ret, k)
|
||||||
raise ParseError(f'key {k} already defined')
|
else:
|
||||||
elif isinstance(item, KeyValue):
|
raise ParseError(f'key {k} already defined')
|
||||||
value = item.value
|
elif isinstance(item, XBCKeyValue):
|
||||||
op = item.op
|
value = item.value
|
||||||
|
op = item.op
|
||||||
|
|
||||||
if op == '=':
|
if op == '=':
|
||||||
if k in ret:
|
if k in ret:
|
||||||
raise ParseError(f'key {k} already defined')
|
raise ParseError(f'key {k} already defined')
|
||||||
assign = value
|
assign = value
|
||||||
else:
|
else:
|
||||||
if k not in ret:
|
if k not in ret:
|
||||||
raise ParseError(f'key {k} not defined')
|
raise ParseError(f'key {k} not defined')
|
||||||
|
|
||||||
if op == '+=':
|
if op == '+=':
|
||||||
if isinstance(ret[k], str):
|
if isinstance(ret[k], str):
|
||||||
assign = [ret[k]]
|
assign = [ret[k]]
|
||||||
else:
|
else:
|
||||||
assign = ret[k]
|
assign = ret[k]
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
assign.append(value)
|
assign.append(value)
|
||||||
else:
|
else:
|
||||||
assign.extend(value)
|
assign.extend(value)
|
||||||
else:
|
else:
|
||||||
assign = value
|
assign = value
|
||||||
|
|
||||||
if isinstance(assign, list):
|
if isinstance(assign, list):
|
||||||
for i in range(len(assign)):
|
for i, item in enumerate(assign):
|
||||||
assign[i] = unquote(assign[i])
|
assign[i] = unquote(item)
|
||||||
else:
|
else:
|
||||||
assign = unquote(assign)
|
assign = unquote(assign)
|
||||||
|
|
||||||
if isinstance(assign, list) and len(assign) == 1:
|
if isinstance(assign, list) and len(assign) == 1:
|
||||||
assign = assign[0]
|
assign = assign[0]
|
||||||
|
|
||||||
ret[k] = assign
|
ret[k] = assign
|
||||||
|
|
||||||
if '.' in k:
|
if '.' in k:
|
||||||
key_walk(ret, k)
|
key_walk(ret, k)
|
||||||
elif isinstance(item, Block):
|
elif isinstance(item, XBCBlock):
|
||||||
value = item.contents
|
value = item.contents
|
||||||
|
|
||||||
if k not in ret:
|
if k not in ret:
|
||||||
ret[k] = False
|
ret[k] = False
|
||||||
|
|
||||||
if not isinstance(value, list):
|
if not isinstance(value, list):
|
||||||
value = [value]
|
value = [value]
|
||||||
|
|
||||||
d = parse_block(k, value)
|
d = parse_block(k, value)
|
||||||
|
|
||||||
for k, v in d.items():
|
for k, v in d.items():
|
||||||
if k in ret:
|
if k in ret:
|
||||||
continue
|
continue
|
||||||
ret[k] = v
|
ret[k] = v
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def parse(data):
|
def parse(data):
|
||||||
tree = lex(data)
|
'Call the lexer and then the parser.'
|
||||||
|
tree = lex(data)
|
||||||
|
|
||||||
d = parse_block(None, tree)
|
d = parse_block(None, tree)
|
||||||
|
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def loads_xbc(data):
|
def loads_xbc(data):
|
||||||
return parse(data)
|
'Load XBC data provided in a string.'
|
||||||
|
return parse(data)
|
||||||
|
|
||||||
def load_xbc(fp):
|
def load_xbc(fp):
|
||||||
with open(fp, mode='r') as f:
|
'Open a file and parse its contents.'
|
||||||
return loads_xbc(f.read())
|
with open(fp, mode='r', encoding='UTF-8') as f:
|
||||||
|
return loads_xbc(f.read())
|
||||||
|
|
||||||
def longest_key(L):
|
def longest_key(seq):
|
||||||
lens = [len(x) for x in L]
|
'Find the deepest-nested key in the sequence provided.'
|
||||||
shortest = min(lens)
|
lens = [len(x) for x in seq]
|
||||||
|
shortest = min(lens)
|
||||||
|
|
||||||
if shortest < 1:
|
if shortest < 1:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
ret = []
|
ret = []
|
||||||
|
|
||||||
for i in range(shortest):
|
for i in range(shortest):
|
||||||
count = {}
|
count = {}
|
||||||
|
|
||||||
for item in L:
|
for item in seq:
|
||||||
j = item[i]
|
j = item[i]
|
||||||
|
|
||||||
if j not in count:
|
if j not in count:
|
||||||
count[j] = 0
|
count[j] = 0
|
||||||
|
|
||||||
count[j] += 1
|
count[j] += 1
|
||||||
if len(count.keys()) == 1:
|
if len(count.keys()) == 1:
|
||||||
ret.append(L[0][i])
|
ret.append(seq[0][i])
|
||||||
else:
|
else:
|
||||||
return '.'.join(ret)
|
return '.'.join(ret)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def longest_keys(keys):
|
def longest_keys(keys):
|
||||||
keys = [k.split('.') for k in keys]
|
'Find the longest keys in the sequence provided.'
|
||||||
ret = set()
|
keys = [k.split('.') for k in keys]
|
||||||
|
ret = set()
|
||||||
|
|
||||||
for i in range(len(keys)):
|
for a in keys:
|
||||||
for j in range(1, len(keys)):
|
for b in keys[1:]:
|
||||||
longest = longest_key([keys[i], keys[j]])
|
longest = longest_key([a, b])
|
||||||
if longest is not None:
|
if longest is not None:
|
||||||
ret.add(longest)
|
ret.add(longest)
|
||||||
ret.discard('')
|
ret.discard('')
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def make_block(data):
|
def make_block(data):
|
||||||
ret = []
|
# pylint: disable=too-many-locals,too-many-branches
|
||||||
|
'Create XBC blocks.'
|
||||||
|
ret = []
|
||||||
|
|
||||||
leafs = []
|
leafs = []
|
||||||
blocks = set()
|
blocks = set()
|
||||||
block_keys = []
|
|
||||||
|
|
||||||
for key in data.keys():
|
for key in data.keys():
|
||||||
if '.' not in key:
|
if '.' not in key:
|
||||||
leafs.append(key)
|
leafs.append(key)
|
||||||
else:
|
else:
|
||||||
k, rest = key.split('.', maxsplit=1)
|
k, _ = key.split('.', maxsplit=1)
|
||||||
blocks.add(k)
|
blocks.add(k)
|
||||||
|
|
||||||
keys = [k for k in data.keys() if '.' in k]
|
keys = [k for k in data.keys() if '.' in k]
|
||||||
temp = longest_keys(keys)
|
temp = longest_keys(keys)
|
||||||
if len(temp):
|
if temp:
|
||||||
mindots = 99
|
mindots = 99
|
||||||
for i in temp:
|
for i in temp:
|
||||||
if 0 < i.count('.') < mindots:
|
if 0 < i.count('.') < mindots:
|
||||||
mindots = i.count('.')
|
mindots = i.count('.')
|
||||||
temp = [i for i in temp if i.count('.') == mindots]
|
temp = [i for i in temp if i.count('.') == mindots]
|
||||||
blocks = set(temp)
|
blocks = set(temp)
|
||||||
|
|
||||||
for key in leafs:
|
for key in leafs:
|
||||||
if data[key] is True:
|
if data[key] is True:
|
||||||
ret.append(f'{key}')
|
ret.append(f'{key}')
|
||||||
elif data[key] is False:
|
elif data[key] is False:
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
value = normalise(data[key])
|
value = normalise(data[key])
|
||||||
ret.append(f'{key} = {value}')
|
ret.append(f'{key} = {value}')
|
||||||
|
|
||||||
for key in blocks:
|
for key in blocks:
|
||||||
block = {}
|
block = {}
|
||||||
klen = len(key) + 1
|
klen = len(key) + 1
|
||||||
|
|
||||||
for k, v in data.items():
|
for k, v in data.items():
|
||||||
if not k.startswith(f'{key}.'):
|
if not k.startswith(f'{key}.'):
|
||||||
continue
|
continue
|
||||||
block[k[klen:]] = v
|
block[k[klen:]] = v
|
||||||
|
|
||||||
chunk = make_block(block)
|
chunk = make_block(block)
|
||||||
ret.append(key + ' {')
|
ret.append(key + ' {')
|
||||||
for line in chunk:
|
for line in chunk:
|
||||||
ret.append(f'\t{line}')
|
ret.append(f'\t{line}')
|
||||||
ret.append('}')
|
ret.append('}')
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
def saves_xbc(data):
|
def saves_xbc(data):
|
||||||
ret = make_block(data)
|
'Export the provided dictionary to an XBC-formatted string.'
|
||||||
return '\n'.join(ret)
|
ret = make_block(data)
|
||||||
|
return '\n'.join(ret)
|
||||||
|
|
||||||
def save_xbc(data, filename):
|
def save_xbc(data, filename):
|
||||||
with open(filename, mode='w') as f:
|
'Export the provided dictionary to an XBC-formatted string and save it.'
|
||||||
f.write(saves_xbc(data))
|
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']
|
__all__ = ['loads_xbc', 'load_xbc', 'saves_xbc', 'save_xbc', 'ParseError']
|
||||||
|
|
163
src/xbc/utils.py
163
src/xbc/utils.py
|
@ -19,101 +19,114 @@
|
||||||
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||||
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
|
|
||||||
|
'''
|
||||||
|
Utility functions for saving data in XBC format.
|
||||||
|
'''
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from collections.abc import Sequence, Mapping
|
from collections.abc import Sequence, Mapping
|
||||||
|
|
||||||
KEY_RE = re.compile(r'^[a-zA-Z0-9_-]+(?:\.(?:[a-zA-Z0-9_-]+))*$')
|
KEY_RE = re.compile(r'^[a-zA-Z0-9_-]+(?:\.(?:[a-zA-Z0-9_-]+))*$')
|
||||||
NVAL_RE = re.compile(r'^[^{}=+:;,\n\'"]+$')
|
NVAL_RE = re.compile(r'^[^{}=+:;,\n\'"]+$')
|
||||||
quotes = '\'"'
|
QUOTES = '\'"'
|
||||||
other = {'"': "'", "'": '"'}
|
QUOTEMAP = {'"': "'", "'": '"'}
|
||||||
escapes = {
|
ESCAPES = {
|
||||||
'backslash': {'"': '\\x22', "'": '\\x27'},
|
'backslash': {'"': '\\x22', "'": '\\x27'},
|
||||||
'html': {'"': '"', "'": '''},
|
'html': {'"': '"', "'": '''},
|
||||||
'url': {'"': '%22', "'": '%27'}
|
'url': {'"': '%22', "'": '%27'}
|
||||||
}
|
}
|
||||||
|
|
||||||
def quote(data, escape='backslash'):
|
def quote(data, escape='backslash'):
|
||||||
esc = None
|
'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?
|
# how shall we escape embedded quotes?
|
||||||
if isinstance(esc, Mapping):
|
if isinstance(esc, Mapping):
|
||||||
if '"' in escape and "'" in escape:
|
if '"' in escape and "'" in escape:
|
||||||
esc = escape
|
esc = escape
|
||||||
elif escape in escapes:
|
else:
|
||||||
esc = escapes[escape]
|
esc = ESCAPES.get(escape, None)
|
||||||
|
|
||||||
if esc is None:
|
if esc is None:
|
||||||
raise ValueError('unrecognised escape format')
|
raise ValueError('unrecognised escape format')
|
||||||
|
|
||||||
f = data[0]
|
f = data[0]
|
||||||
|
|
||||||
# is this a quoted string?
|
# 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
|
# return it if we don't need to do anything
|
||||||
if f not in data[1:-1]:
|
if f not in data[1:-1]:
|
||||||
return data
|
return data
|
||||||
else:
|
# escape embedded quotes
|
||||||
# escape embedded quotes
|
x = data[1:-1].replace(f, esc[f])
|
||||||
x = data[1:-1].replace(f, esc[f])
|
return f'{f}{x}{f}'
|
||||||
return f'{f}{x}{f}'
|
|
||||||
else:
|
# if the other quote isn't used, wrap in it
|
||||||
# if the other quote isn't used, wrap in it
|
if f in QUOTES and QUOTEMAP[f] not in data[1:]:
|
||||||
if f in quotes and other[f] not in data[1:]:
|
q = QUOTEMAP[f]
|
||||||
q = other[f]
|
# not a quoted string, but has only one kind of quote
|
||||||
return f'{q}{data}{q}'
|
elif "'" in data and '"' not in data:
|
||||||
# not a quoted string, but has only one kind of quote
|
q = '"'
|
||||||
elif "'" in data and '"' not in data:
|
elif '"' in data and "'" not in data:
|
||||||
return f'"{data}"'
|
q = "'"
|
||||||
elif '"' in data and "'" not in data:
|
else:
|
||||||
return f"'{data}'"
|
# not a quoted string and has both types; we escape one
|
||||||
# not a quoted string and has both types; we escape one
|
data = data.replace("'", esc["'"])
|
||||||
else:
|
q = "'"
|
||||||
data = data.replace("'", esc["'"])
|
return f'{q}{data}{q}'
|
||||||
return f"'{data}"
|
|
||||||
|
|
||||||
def normalise_string(string):
|
def normalise_string(string):
|
||||||
if not isinstance(string, str):
|
'Normalise values according to XBC rules.'
|
||||||
string = str(string)
|
if not isinstance(string, str):
|
||||||
|
string = str(string)
|
||||||
|
|
||||||
if NVAL_RE.match(string) is None:
|
if NVAL_RE.match(string) is None:
|
||||||
string = quote(string)
|
string = quote(string)
|
||||||
|
|
||||||
return string
|
return string
|
||||||
|
|
||||||
def normalise(data):
|
def normalise(data):
|
||||||
if isinstance(data, str) or not isinstance(data, (Sequence, Mapping)):
|
'''Normalise values according to XBC rules.'''
|
||||||
return normalise_string(data)
|
if isinstance(data, str) or not isinstance(data, (Sequence, Mapping)):
|
||||||
elif isinstance(data, Sequence):
|
return normalise_string(data)
|
||||||
L = []
|
|
||||||
|
|
||||||
for item in data:
|
if isinstance(data, Sequence):
|
||||||
if isinstance(item, str) or not isinstance(item, (Sequence, Mapping)):
|
ret = []
|
||||||
L.append(normalise_string(item))
|
|
||||||
# we can unwind nested sequences
|
|
||||||
elif isinstance(item, Sequence):
|
|
||||||
L.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):
|
|
||||||
d = {}
|
|
||||||
for k, v in data.items():
|
|
||||||
if Key.pattern.match(k) is None:
|
|
||||||
raise KeyError(k)
|
|
||||||
else:
|
|
||||||
k = Key(k)
|
|
||||||
|
|
||||||
v = normalise(v)
|
for item in data:
|
||||||
|
if isinstance(item, str) or not isinstance(item, (Sequence, Mapping)):
|
||||||
|
ret.append(normalise_string(item))
|
||||||
|
# we can unwind nested sequences
|
||||||
|
elif isinstance(item, Sequence):
|
||||||
|
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:
|
||||||
|
# this should be impossible to reach, but nothing truly is.
|
||||||
|
raise ValueError(type(item))
|
||||||
|
|
||||||
d[k] = v
|
ret = ', '.join(ret)
|
||||||
return d
|
|
||||||
|
|
||||||
return data
|
return ret
|
||||||
|
|
||||||
|
if isinstance(data, Mapping):
|
||||||
|
d = {}
|
||||||
|
for k, v in data.items():
|
||||||
|
if KEY_RE.match(k) is None:
|
||||||
|
raise KeyError(k)
|
||||||
|
|
||||||
|
v = normalise(v)
|
||||||
|
|
||||||
|
d[k] = v
|
||||||
|
|
||||||
|
return d
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
__all__ = ['quote', 'normalise', 'normalise_string']
|
__all__ = ['quote', 'normalise', 'normalise_string']
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
version = '0.1.0'
|
# pylint: disable=missing-module-docstring,invalid-name
|
||||||
|
version = '0.1.0'
|
||||||
|
|
|
@ -1,49 +1,67 @@
|
||||||
|
|
||||||
|
'''
|
||||||
|
Tests for base-level keys.
|
||||||
|
'''
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from xbc import loads_xbc, ParseError
|
from xbc import loads_xbc, ParseError
|
||||||
|
|
||||||
|
|
||||||
def test_key():
|
def test_key():
|
||||||
assert loads_xbc('a') == {'a': True}
|
'A key by itself exists.'
|
||||||
|
assert loads_xbc('a') == {'a': True}
|
||||||
|
|
||||||
def test_dot_key():
|
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():
|
def test_key_eq():
|
||||||
assert loads_xbc('a =') == {'a': True}
|
'A key with an empty assignment exists.'
|
||||||
|
assert loads_xbc('a =') == {'a': True}
|
||||||
|
|
||||||
def test_keyvalue():
|
def test_keyvalue():
|
||||||
assert loads_xbc('a = 1') == {'a': '1'}
|
'A bare value can be assigned to a key.'
|
||||||
|
assert loads_xbc('a = 1') == {'a': '1'}
|
||||||
|
|
||||||
def test_keyvalue_space():
|
def test_keyvalue_space():
|
||||||
assert loads_xbc('a = a b') == {'a': 'a b'}
|
'A bare value can have spaces.'
|
||||||
|
assert loads_xbc('a = a b') == {'a': 'a b'}
|
||||||
|
|
||||||
def test_dot_keyvalue():
|
def test_dot_keyvalue():
|
||||||
assert loads_xbc('a.a = 1') == {'a': False, 'a.a': '1'}
|
'A key being assigned to can have dots.'
|
||||||
|
assert loads_xbc('a.a = 1') == {'a': False, 'a.a': '1'}
|
||||||
|
|
||||||
def test_keys():
|
def test_keys():
|
||||||
assert loads_xbc('a;b') == {'a': True, 'b': True}
|
'Statements can be separated by semicolons.'
|
||||||
|
assert loads_xbc('a;b') == {'a': True, 'b': True}
|
||||||
|
|
||||||
def test_dot_keys():
|
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():
|
def test_quoted():
|
||||||
assert loads_xbc('a = "b"') == {'a': 'b'}
|
'Values can be quoted with single or double quotes.'
|
||||||
|
assert loads_xbc('a = "b"') == {'a': 'b'}
|
||||||
|
|
||||||
def test_quoted_space():
|
def test_quoted_space():
|
||||||
assert loads_xbc('a = "b "') == {'a': 'b '}
|
'Quoted values can have trailing whitespace preserved.'
|
||||||
|
assert loads_xbc('a = "b "') == {'a': 'b '}
|
||||||
|
|
||||||
def test_array():
|
def test_array():
|
||||||
assert loads_xbc('a = 1, 2') == {'a': ['1', '2']}
|
'Multiple values can be assigned to a single key.'
|
||||||
|
assert loads_xbc('a = 1, 2') == {'a': ['1', '2']}
|
||||||
|
|
||||||
def test_reassignment():
|
def test_reassignment():
|
||||||
with pytest.raises(ParseError):
|
'Keys cannot be reassigned.'
|
||||||
loads_xbc('a = 1\na = 2')
|
with pytest.raises(ParseError):
|
||||||
|
loads_xbc('a = 1\na = 2')
|
||||||
|
|
||||||
def test_reassignment_colon():
|
def test_reassignment_colon():
|
||||||
with pytest.raises(ParseError):
|
'Keys cannot be reassigned, even in compound statements.'
|
||||||
loads_xbc('a = 1;a = 2')
|
with pytest.raises(ParseError):
|
||||||
|
loads_xbc('a = 1;a = 2')
|
||||||
|
|
||||||
def test_ovewrite_nonexistent():
|
def test_ovewrite_nonexistent():
|
||||||
with pytest.raises(ParseError):
|
'Keys can only be updated if they exist.'
|
||||||
loads_xbc('a := 1')
|
with pytest.raises(ParseError):
|
||||||
|
loads_xbc('a := 1')
|
||||||
|
|
|
@ -1,24 +1,34 @@
|
||||||
|
|
||||||
|
'''
|
||||||
|
Test output for XBC blocks.
|
||||||
|
'''
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from xbc import loads_xbc, ParseError
|
from xbc import loads_xbc, ParseError
|
||||||
|
|
||||||
def test_empty():
|
def test_empty():
|
||||||
assert loads_xbc('a {}') == {'a': True}
|
'An empty block should cause the key to exist.'
|
||||||
|
assert loads_xbc('a {}') == {'a': True}
|
||||||
|
|
||||||
def test_keyvalue():
|
def test_keyvalue():
|
||||||
assert loads_xbc('a { a = 1 }') == {'a': False, 'a.a': '1'}
|
'A block should support key/value pairs.'
|
||||||
|
assert loads_xbc('a { a = 1 }') == {'a': False, 'a.a': '1'}
|
||||||
|
|
||||||
def test_nested_block():
|
def test_nested_block():
|
||||||
assert loads_xbc('a { b { c = 1 } }') == {'a.b.c': '1', 'a': False, 'a.b': False}
|
'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():
|
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():
|
def test_reassign_colon():
|
||||||
with pytest.raises(ParseError):
|
'Attempting to assign to the same key should be an error inside a block too.'
|
||||||
loads_xbc('a { a = 1; a = 2 }')
|
with pytest.raises(ParseError):
|
||||||
|
loads_xbc('a { a = 1; a = 2 }')
|
||||||
|
|
||||||
def test_assign_after_block():
|
def test_assign_after_block():
|
||||||
with pytest.raises(ParseError):
|
'It is an error to assign to a block key after the block.'
|
||||||
loads_xbc('a { a = 1 }\na = 1')
|
with pytest.raises(ParseError):
|
||||||
|
loads_xbc('a { a = 1 }\na = 1')
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
|
|
||||||
|
'''
|
||||||
|
Tests for inputs which are invalid.
|
||||||
|
'''
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from pyparsing.exceptions import ParseException
|
from pyparsing.exceptions import ParseException
|
||||||
|
@ -7,37 +11,45 @@ from xbc import loads_xbc, ParseError
|
||||||
# this should fail but does not.
|
# this should fail but does not.
|
||||||
@pytest.mark.xfail
|
@pytest.mark.xfail
|
||||||
def test_whitespace_vc():
|
def test_whitespace_vc():
|
||||||
with pytest.raises(ParseError):
|
'Whitespace is prohibited between values and commas.'
|
||||||
loads_xbc('x = a\n, b')
|
with pytest.raises(ParseError):
|
||||||
|
loads_xbc('x = a\n, b')
|
||||||
|
|
||||||
# this should fail but does not.
|
# this should fail but does not.
|
||||||
@pytest.mark.xfail
|
@pytest.mark.xfail
|
||||||
def test_extra_quote():
|
def test_extra_quote():
|
||||||
with pytest.raises(ParseException):
|
'Strings cannot contain the quote style that bookends them.'
|
||||||
loads_xbc("x = '''")
|
with pytest.raises(ParseException):
|
||||||
|
loads_xbc("x = '''")
|
||||||
|
|
||||||
def test_lone_plus():
|
def test_lone_plus():
|
||||||
with pytest.raises(ParseException):
|
'Bare operators are not permitted.'
|
||||||
loads_xbc('+')
|
with pytest.raises(ParseException):
|
||||||
|
loads_xbc('+')
|
||||||
|
|
||||||
def test_lone_rbrace():
|
def test_lone_rbrace():
|
||||||
with pytest.raises(ParseException):
|
'Braces can only appear as part of a block.'
|
||||||
loads_xbc('}')
|
with pytest.raises(ParseException):
|
||||||
|
loads_xbc('}')
|
||||||
|
|
||||||
def test_lone_lbrace():
|
def test_lone_lbrace():
|
||||||
with pytest.raises(ParseException):
|
'Braces can only appear as part of a block.'
|
||||||
loads_xbc('{')
|
with pytest.raises(ParseException):
|
||||||
|
loads_xbc('{')
|
||||||
|
|
||||||
def test_lone_braces():
|
def test_lone_braces():
|
||||||
with pytest.raises(ParseException):
|
'Braces can only appear as part of a block.'
|
||||||
loads_xbc('{}')
|
with pytest.raises(ParseException):
|
||||||
|
loads_xbc('{}')
|
||||||
|
|
||||||
def test_lone_semi():
|
def test_lone_semi():
|
||||||
with pytest.raises(ParseException):
|
'Semicolons are only permitted as part of a key or key/value statement.'
|
||||||
loads_xbc(';')
|
with pytest.raises(ParseException):
|
||||||
|
loads_xbc(';')
|
||||||
|
|
||||||
def test_empty():
|
def test_empty():
|
||||||
with pytest.raises(ParseException):
|
'XBC files cannot be empty.'
|
||||||
loads_xbc('\n')
|
with pytest.raises(ParseException):
|
||||||
with pytest.raises(ParseException):
|
loads_xbc('\n')
|
||||||
loads_xbc('')
|
with pytest.raises(ParseException):
|
||||||
|
loads_xbc('')
|
||||||
|
|
|
@ -1,139 +1,150 @@
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
from xbc import loads_xbc
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
The tests in this file are samples drawn from
|
The tests in this file are samples drawn from
|
||||||
https://lwn.net/Articles/806002/.
|
https://lwn.net/Articles/806002/.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from xbc import loads_xbc
|
||||||
|
|
||||||
def test_01():
|
def test_01():
|
||||||
i = '''feature.option.foo = 1
|
'Key/value example.'
|
||||||
|
i = '''feature.option.foo = 1
|
||||||
feature.option.bar = 2'''
|
feature.option.bar = 2'''
|
||||||
d = {
|
d = {
|
||||||
'feature.option': False,
|
'feature.option': False,
|
||||||
'feature': False,
|
'feature': False,
|
||||||
'feature.option.foo': '1',
|
'feature.option.foo': '1',
|
||||||
'feature.option.bar': '2'
|
'feature.option.bar': '2'
|
||||||
}
|
}
|
||||||
assert loads_xbc(i) == d
|
assert loads_xbc(i) == d
|
||||||
|
|
||||||
def test_02():
|
def test_02():
|
||||||
i = '''feature.option {
|
'Block example.'
|
||||||
foo = 1
|
i = '''feature.option {
|
||||||
bar = 2
|
foo = 1
|
||||||
|
bar = 2
|
||||||
}'''
|
}'''
|
||||||
d = {
|
d = {
|
||||||
'feature.option': False,
|
'feature.option': False,
|
||||||
'feature': False,
|
'feature': False,
|
||||||
'feature.option.foo': '1',
|
'feature.option.foo': '1',
|
||||||
'feature.option.bar': '2'
|
'feature.option.bar': '2'
|
||||||
}
|
}
|
||||||
assert loads_xbc(i) == d
|
assert loads_xbc(i) == d
|
||||||
|
|
||||||
def test_03():
|
def test_03():
|
||||||
i = 'feature.options = "foo", "bar"'
|
'Array example.'
|
||||||
d = {
|
i = 'feature.options = "foo", "bar"'
|
||||||
'feature': False,
|
d = {
|
||||||
'feature.options': ['foo', 'bar']
|
'feature': False,
|
||||||
}
|
'feature.options': ['foo', 'bar']
|
||||||
assert loads_xbc(i) == d
|
}
|
||||||
|
assert loads_xbc(i) == d
|
||||||
|
|
||||||
def test_10():
|
def test_10():
|
||||||
i = 'feature.option{foo=1;bar=2}'
|
'Compact example.'
|
||||||
d = {
|
i = 'feature.option{foo=1;bar=2}'
|
||||||
'feature.option': False,
|
d = {
|
||||||
'feature': False,
|
'feature.option': False,
|
||||||
'feature.option.foo': '1',
|
'feature': False,
|
||||||
'feature.option.bar': '2'
|
'feature.option.foo': '1',
|
||||||
}
|
'feature.option.bar': '2'
|
||||||
assert loads_xbc(i) == d
|
}
|
||||||
|
assert loads_xbc(i) == d
|
||||||
|
|
||||||
def test_11():
|
def test_11():
|
||||||
i = '''ftrace.event {
|
'Example of a possible configuration.'
|
||||||
task.task_newtask {
|
i = '''ftrace.event {
|
||||||
filter = "pid < 128"
|
task.task_newtask {
|
||||||
enable
|
filter = "pid < 128"
|
||||||
}
|
enable
|
||||||
kprobes.vfs_read {
|
}
|
||||||
probes = "vfs_read $arg1 $arg2"
|
kprobes.vfs_read {
|
||||||
filter = "common_pid < 200"
|
probes = "vfs_read $arg1 $arg2"
|
||||||
enable
|
filter = "common_pid < 200"
|
||||||
}
|
enable
|
||||||
synthetic.initcall_latency {
|
}
|
||||||
fields = "unsigned long func", "u64 lat"
|
synthetic.initcall_latency {
|
||||||
actions = "hist:keys=func.sym,lat:vals=lat:sort=lat"
|
fields = "unsigned long func", "u64 lat"
|
||||||
}
|
actions = "hist:keys=func.sym,lat:vals=lat:sort=lat"
|
||||||
initcall.initcall_start {
|
}
|
||||||
actions = "hist:keys=func:ts0=common_timestamp.usecs"
|
initcall.initcall_start {
|
||||||
}
|
actions = "hist:keys=func:ts0=common_timestamp.usecs"
|
||||||
initcall.initcall_finish {
|
}
|
||||||
actions = "hist:keys=func:lat=common_timestamp.usecs-$ts0:onmatch(initcall.initcall_start).initcall_latency(func,$lat)"
|
initcall.initcall_finish {
|
||||||
}
|
actions = "hist:keys=func:lat=common_timestamp.usecs-$ts0:onmatch(initcall.initcall_start).initcall_latency(func,$lat)"
|
||||||
|
}
|
||||||
}'''
|
}'''
|
||||||
d = {
|
d = {
|
||||||
'ftrace': False,
|
'ftrace': False,
|
||||||
'ftrace.event': False,
|
'ftrace.event': False,
|
||||||
'ftrace.event.task': False,
|
'ftrace.event.task': False,
|
||||||
'ftrace.event.task.task_newtask': False,
|
'ftrace.event.task.task_newtask': False,
|
||||||
'ftrace.event.task.task_newtask.filter': "pid < 128",
|
'ftrace.event.task.task_newtask.filter': "pid < 128",
|
||||||
'ftrace.event.task.task_newtask.enable': True,
|
'ftrace.event.task.task_newtask.enable': True,
|
||||||
'ftrace.event.kprobes': False,
|
'ftrace.event.kprobes': False,
|
||||||
'ftrace.event.kprobes.vfs_read': False,
|
'ftrace.event.kprobes.vfs_read': False,
|
||||||
'ftrace.event.kprobes.vfs_read.probes': "vfs_read $arg1 $arg2",
|
'ftrace.event.kprobes.vfs_read.probes': "vfs_read $arg1 $arg2",
|
||||||
'ftrace.event.kprobes.vfs_read.filter': "common_pid < 200",
|
'ftrace.event.kprobes.vfs_read.filter': "common_pid < 200",
|
||||||
'ftrace.event.kprobes.vfs_read.enable': True,
|
'ftrace.event.kprobes.vfs_read.enable': True,
|
||||||
'ftrace.event.synthetic': False,
|
'ftrace.event.synthetic': False,
|
||||||
'ftrace.event.synthetic.initcall_latency': False,
|
'ftrace.event.synthetic.initcall_latency': False,
|
||||||
'ftrace.event.synthetic.initcall_latency.fields': ["unsigned long func", "u64 lat"],
|
'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':
|
||||||
'ftrace.event.initcall': False,
|
"hist:keys=func.sym,lat:vals=lat:sort=lat",
|
||||||
'ftrace.event.initcall.initcall_start': False,
|
'ftrace.event.initcall': False,
|
||||||
'ftrace.event.initcall.initcall_start.actions': "hist:keys=func:ts0=common_timestamp.usecs",
|
'ftrace.event.initcall.initcall_start': False,
|
||||||
'ftrace.event.initcall.initcall_finish': False,
|
'ftrace.event.initcall.initcall_start.actions':
|
||||||
'ftrace.event.initcall.initcall_finish.actions': "hist:keys=func:lat=common_timestamp.usecs-$ts0:onmatch(initcall.initcall_start).initcall_latency(func,$lat)"
|
"hist:keys=func:ts0=common_timestamp.usecs",
|
||||||
}
|
'ftrace.event.initcall.initcall_finish': False,
|
||||||
assert loads_xbc(i) == d
|
'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():
|
def test_12():
|
||||||
i = '''ftrace.event.synthetic.initcall_latency {
|
'Another example of a possible configuration.'
|
||||||
fields = "unsigned long func", "u64 lat"
|
i = '''ftrace.event.synthetic.initcall_latency {
|
||||||
hist {
|
fields = "unsigned long func", "u64 lat"
|
||||||
from {
|
hist {
|
||||||
event = initcall.initcall_start
|
from {
|
||||||
key = func
|
event = initcall.initcall_start
|
||||||
assigns = "ts0=common_timestamp.usecs"
|
key = func
|
||||||
}
|
assigns = "ts0=common_timestamp.usecs"
|
||||||
to {
|
}
|
||||||
event = initcall.initcall_finish
|
to {
|
||||||
key = func
|
event = initcall.initcall_finish
|
||||||
assigns = "lat=common_timestamp.usecs-$ts0"
|
key = func
|
||||||
onmatch = func, $lat
|
assigns = "lat=common_timestamp.usecs-$ts0"
|
||||||
}
|
onmatch = func, $lat
|
||||||
keys = func.sym, lat
|
}
|
||||||
vals = lat
|
keys = func.sym, lat
|
||||||
sort = lat
|
vals = lat
|
||||||
}
|
sort = lat
|
||||||
|
}
|
||||||
}'''
|
}'''
|
||||||
d = {
|
d = {
|
||||||
'ftrace': False,
|
'ftrace': False,
|
||||||
'ftrace.event': False,
|
'ftrace.event': False,
|
||||||
'ftrace.event.synthetic': False,
|
'ftrace.event.synthetic': False,
|
||||||
'ftrace.event.synthetic.initcall_latency': False,
|
'ftrace.event.synthetic.initcall_latency': False,
|
||||||
'ftrace.event.synthetic.initcall_latency.fields': ["unsigned long func", "u64 lat"],
|
'ftrace.event.synthetic.initcall_latency.fields': ["unsigned long func", "u64 lat"],
|
||||||
'ftrace.event.synthetic.initcall_latency.hist': False,
|
'ftrace.event.synthetic.initcall_latency.hist': False,
|
||||||
'ftrace.event.synthetic.initcall_latency.hist.from': False,
|
'ftrace.event.synthetic.initcall_latency.hist.from': False,
|
||||||
'ftrace.event.synthetic.initcall_latency.hist.from.event': 'initcall.initcall_start',
|
'ftrace.event.synthetic.initcall_latency.hist.from.event': 'initcall.initcall_start',
|
||||||
'ftrace.event.synthetic.initcall_latency.hist.from.key': 'func',
|
'ftrace.event.synthetic.initcall_latency.hist.from.key': 'func',
|
||||||
'ftrace.event.synthetic.initcall_latency.hist.from.assigns': "ts0=common_timestamp.usecs",
|
'ftrace.event.synthetic.initcall_latency.hist.from.assigns': "ts0=common_timestamp.usecs",
|
||||||
'ftrace.event.synthetic.initcall_latency.hist.to': False,
|
'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.event': 'initcall.initcall_finish',
|
||||||
'ftrace.event.synthetic.initcall_latency.hist.to.key': 'func',
|
'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':
|
||||||
'ftrace.event.synthetic.initcall_latency.hist.to.onmatch': ['func', '$lat'],
|
"lat=common_timestamp.usecs-$ts0",
|
||||||
'ftrace.event.synthetic.initcall_latency.hist.keys': ['func.sym', 'lat'],
|
'ftrace.event.synthetic.initcall_latency.hist.to.onmatch': ['func', '$lat'],
|
||||||
'ftrace.event.synthetic.initcall_latency.hist.vals': 'lat',
|
'ftrace.event.synthetic.initcall_latency.hist.keys': ['func.sym', 'lat'],
|
||||||
'ftrace.event.synthetic.initcall_latency.hist.sort': 'lat'
|
'ftrace.event.synthetic.initcall_latency.hist.vals': 'lat',
|
||||||
}
|
'ftrace.event.synthetic.initcall_latency.hist.sort': 'lat'
|
||||||
assert loads_xbc(i) == d
|
}
|
||||||
|
assert loads_xbc(i) == d
|
||||||
|
|
Loading…
Reference in New Issue