satisfy pylint

current v0.1.0
Síle Ekaterin Liszka 2024-01-15 17:08:01 -08:00
parent 22cf9256f8
commit 84a63253a7
Signed by: VulpineAmethyst
SSH Key Fingerprint: SHA256:VcHwQ6SUfi/p0Csfxe3SabX/TImWER0PhoJqkt+GlmE
9 changed files with 696 additions and 488 deletions

View File

@ -54,9 +54,9 @@ Keys are composable. The following examples are equivalent:
```xbc
foo {
bar {
fluff = 1
}
bar {
fluff = 1
}
}
# is equivalent to
foo.bar.fluff = 1

View File

@ -2,7 +2,7 @@
name = 'py-xbc'
dynamic = ['version']
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'
readme = 'README.md'
@ -10,18 +10,18 @@ keywords = ['bootconfig', 'xbc', 'configuration']
dependencies = ['pyparsing']
requires-python = '>=3.7'
classifiers = [
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Utilities'
'Development Status :: 4 - Beta',
'Intended Audience :: Developers',
'License :: OSI Approved :: MIT License',
'Operating System :: OS Independent',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Topic :: Utilities'
]
[project.urls]
@ -42,4 +42,102 @@ version = {attr = "xbc.version.version"}
addopts = [
"--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"]

View File

@ -19,313 +19,358 @@
# 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
from pyparsing import (
alphas,
CharsNotIn,
DelimitedList,
Forward,
Group,
nums,
Literal,
OneOrMore,
Optional,
Regex,
restOfLine,
QuotedString,
Word,
ZeroOrMore
alphas,
CharsNotIn,
DelimitedList,
Forward,
Group,
nums,
Literal,
OneOrMore,
Optional,
Regex,
restOfLine,
QuotedString,
Word,
ZeroOrMore
)
from .utils import normalise
from .version import version as __version__
class Node:
def __init__(self, *args, type=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
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 = kind
@property
def key(self):
return self.args[0]
@property
def key(self):
'The key associated with the node.'
return self.args[0]
class Key(Node):
def __init__(self, *args):
self.args = args[0]
self.type = 'key'
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):
def __init__(self, *args):
super().__init__(args, type='kv')
class XBCKeyValue(XBCNode):
'An XBC key/value operation.'
def __init__(self, *args):
super().__init__(args, kind='kv')
@property
def op(self):
return self.args[1]
@property
def op(self):
'The operator being performed.'
return self.args[1]
@property
def value(self):
return self.args[2]
@property
def value(self):
'The data associated with the operation.'
return self.args[2]
class Block(Node):
def __init__(self, *args):
super().__init__(args, type='block')
class XBCBlock(XBCNode):
'An XBC block.'
def __init__(self, *args):
super().__init__(args, kind='block')
@property
def contents(self):
return self.args[1]
@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):
if val[0] in '\'"' and val[0] == val[-1]:
return val[1:-1]
return val.strip()
'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):
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):
x = '.'.join(split[:i])
print(x)
if x not in d:
d[x] = False
for i in range(len(split) - 1, 0, -1):
x = '.'.join(split[:i])
print(x)
if x not in d:
d[x] = False
def parse_block(key, seq):
if isinstance(seq, list) and len(seq) == 1 and isinstance(seq[0], list):
seq = seq[0]
# 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]
ret = {}
ret = {}
for item in seq:
if key is not None:
k = f'{key}.{item.key}'
else:
k = item.key
for item in seq:
if key is not None:
k = f'{key}.{item.key}'
else:
k = item.key
if isinstance(item, Key):
if k not in ret:
ret[k] = True
else:
raise ParseError(f'key {k} already defined')
elif isinstance(item, KeyValue):
value = item.value
op = item.op
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, XBCKeyValue):
value = item.value
op = item.op
if op == '=':
if k in ret:
raise ParseError(f'key {k} already defined')
assign = value
else:
if k not in ret:
raise ParseError(f'key {k} not defined')
if op == '=':
if k in ret:
raise ParseError(f'key {k} already defined')
assign = value
else:
if k not in ret:
raise ParseError(f'key {k} not defined')
if op == '+=':
if isinstance(ret[k], str):
assign = [ret[k]]
else:
assign = ret[k]
if isinstance(value, str):
assign.append(value)
else:
assign.extend(value)
else:
assign = value
if op == '+=':
if isinstance(ret[k], str):
assign = [ret[k]]
else:
assign = ret[k]
if isinstance(value, str):
assign.append(value)
else:
assign.extend(value)
else:
assign = value
if isinstance(assign, list):
for i in range(len(assign)):
assign[i] = unquote(assign[i])
else:
assign = unquote(assign)
if isinstance(assign, list):
for i, item in enumerate(assign):
assign[i] = unquote(item)
else:
assign = unquote(assign)
if isinstance(assign, list) and len(assign) == 1:
assign = assign[0]
if isinstance(assign, list) and len(assign) == 1:
assign = assign[0]
ret[k] = assign
ret[k] = assign
if '.' in k:
key_walk(ret, k)
elif isinstance(item, Block):
value = item.contents
if '.' in k:
key_walk(ret, k)
elif isinstance(item, XBCBlock):
value = item.contents
if k not in ret:
ret[k] = False
if k not in ret:
ret[k] = False
if not isinstance(value, list):
value = [value]
if not isinstance(value, list):
value = [value]
d = parse_block(k, value)
d = parse_block(k, value)
for k, v in d.items():
if k in ret:
continue
ret[k] = v
for k, v in d.items():
if k in ret:
continue
ret[k] = v
return ret
return ret
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):
return parse(data)
'Load XBC data provided in a string.'
return parse(data)
def load_xbc(fp):
with open(fp, mode='r') as f:
return loads_xbc(f.read())
'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]
shortest = min(lens)
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:
return None
if shortest < 1:
return None
ret = []
ret = []
for i in range(shortest):
count = {}
for i in range(shortest):
count = {}
for item in L:
j = item[i]
for item in seq:
j = item[i]
if j not in count:
count[j] = 0
if j not in count:
count[j] = 0
count[j] += 1
if len(count.keys()) == 1:
ret.append(L[0][i])
else:
return '.'.join(ret)
return None
count[j] += 1
if len(count.keys()) == 1:
ret.append(seq[0][i])
else:
return '.'.join(ret)
return None
def longest_keys(keys):
keys = [k.split('.') for k in keys]
ret = set()
'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]])
if longest is not None:
ret.add(longest)
ret.discard('')
return ret
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):
ret = []
# pylint: disable=too-many-locals,too-many-branches
'Create XBC blocks.'
ret = []
leafs = []
blocks = set()
block_keys = []
leafs = []
blocks = set()
for key in data.keys():
if '.' not in key:
leafs.append(key)
else:
k, rest = key.split('.', maxsplit=1)
blocks.add(k)
for key in data.keys():
if '.' not in key:
leafs.append(key)
else:
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):
mindots = 99
for i in temp:
if 0 < i.count('.') < mindots:
mindots = i.count('.')
temp = [i for i in temp if i.count('.') == mindots]
blocks = set(temp)
keys = [k for k in data.keys() if '.' in k]
temp = longest_keys(keys)
if temp:
mindots = 99
for i in temp:
if 0 < i.count('.') < mindots:
mindots = i.count('.')
temp = [i for i in temp if i.count('.') == mindots]
blocks = set(temp)
for key in leafs:
if data[key] is True:
ret.append(f'{key}')
elif data[key] is False:
continue
else:
value = normalise(data[key])
ret.append(f'{key} = {value}')
for key in leafs:
if data[key] is True:
ret.append(f'{key}')
elif data[key] is False:
continue
else:
value = normalise(data[key])
ret.append(f'{key} = {value}')
for key in blocks:
block = {}
klen = len(key) + 1
for key in blocks:
block = {}
klen = len(key) + 1
for k, v in data.items():
if not k.startswith(f'{key}.'):
continue
block[k[klen:]] = v
for k, v in data.items():
if not k.startswith(f'{key}.'):
continue
block[k[klen:]] = v
chunk = make_block(block)
ret.append(key + ' {')
for line in chunk:
ret.append(f'\t{line}')
ret.append('}')
chunk = make_block(block)
ret.append(key + ' {')
for line in chunk:
ret.append(f'\t{line}')
ret.append('}')
return ret
return ret
def saves_xbc(data):
ret = make_block(data)
return '\n'.join(ret)
'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:
f.write(saves_xbc(data))
'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']

View File

@ -19,101 +19,114 @@
# 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 = {
'backslash': {'"': '\\x22', "'": '\\x27'},
'html': {'"': '&quot;', "'": '&apos;'},
'url': {'"': '%22', "'": '%27'}
KEY_RE = re.compile(r'^[a-zA-Z0-9_-]+(?:\.(?:[a-zA-Z0-9_-]+))*$')
NVAL_RE = re.compile(r'^[^{}=+:;,\n\'"]+$')
QUOTES = '\'"'
QUOTEMAP = {'"': "'", "'": '"'}
ESCAPES = {
'backslash': {'"': '\\x22', "'": '\\x27'},
'html': {'"': '&quot;', "'": '&apos;'},
'url': {'"': '%22', "'": '%27'}
}
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?
if isinstance(esc, Mapping):
if '"' in escape and "'" in escape:
esc = escape
elif escape in escapes:
esc = escapes[escape]
# how shall we escape embedded quotes?
if isinstance(esc, Mapping):
if '"' in escape and "'" in escape:
esc = escape
else:
esc = ESCAPES.get(escape, None)
if esc is None:
raise ValueError('unrecognised escape format')
if esc is None:
raise ValueError('unrecognised escape format')
f = data[0]
f = data[0]
# is this a quoted string?
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}'
# not a quoted string, but has only one kind of quote
elif "'" in data and '"' not in data:
return f'"{data}"'
elif '"' in data and "'" not in data:
return f"'{data}'"
# not a quoted string and has both types; we escape one
else:
data = data.replace("'", esc["'"])
return f"'{data}"
# is this a quoted string?
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
# escape embedded quotes
x = data[1:-1].replace(f, esc[f])
return f'{f}{x}{f}'
# if the other quote isn't used, wrap in it
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:
q = '"'
elif '"' in data and "'" not in data:
q = "'"
else:
# not a quoted string and has both types; we escape one
data = data.replace("'", esc["'"])
q = "'"
return f'{q}{data}{q}'
def normalise_string(string):
if not isinstance(string, str):
string = str(string)
'Normalise values according to XBC rules.'
if not isinstance(string, str):
string = str(string)
if NVAL_RE.match(string) is None:
string = quote(string)
if NVAL_RE.match(string) is None:
string = quote(string)
return string
return string
def normalise(data):
if isinstance(data, str) or not isinstance(data, (Sequence, Mapping)):
return normalise_string(data)
elif isinstance(data, Sequence):
L = []
'''Normalise values according to XBC rules.'''
if isinstance(data, str) or not isinstance(data, (Sequence, Mapping)):
return normalise_string(data)
for item in data:
if isinstance(item, str) or not isinstance(item, (Sequence, Mapping)):
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)
if isinstance(data, Sequence):
ret = []
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
return d
ret = ', '.join(ret)
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']

View File

@ -1 +1,2 @@
version = '0.1.0'
# pylint: disable=missing-module-docstring,invalid-name
version = '0.1.0'

View File

@ -1,49 +1,67 @@
'''
Tests for base-level keys.
'''
import pytest
from xbc import loads_xbc, ParseError
def test_key():
assert loads_xbc('a') == {'a': True}
'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():
assert loads_xbc('a =') == {'a': True}
'A key with an empty assignment exists.'
assert loads_xbc('a =') == {'a': True}
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():
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():
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():
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():
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():
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():
assert loads_xbc('a = "b "') == {'a': 'b '}
'Quoted values can have trailing whitespace preserved.'
assert loads_xbc('a = "b "') == {'a': 'b '}
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():
with pytest.raises(ParseError):
loads_xbc('a = 1\na = 2')
'Keys cannot be reassigned.'
with pytest.raises(ParseError):
loads_xbc('a = 1\na = 2')
def test_reassignment_colon():
with pytest.raises(ParseError):
loads_xbc('a = 1;a = 2')
'Keys cannot be reassigned, even in compound statements.'
with pytest.raises(ParseError):
loads_xbc('a = 1;a = 2')
def test_ovewrite_nonexistent():
with pytest.raises(ParseError):
loads_xbc('a := 1')
'Keys can only be updated if they exist.'
with pytest.raises(ParseError):
loads_xbc('a := 1')

View File

@ -1,24 +1,34 @@
'''
Test output for XBC blocks.
'''
import pytest
from xbc import loads_xbc, ParseError
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():
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():
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():
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():
with pytest.raises(ParseError):
loads_xbc('a { a = 1; a = 2 }')
'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():
with pytest.raises(ParseError):
loads_xbc('a { a = 1 }\na = 1')
'It is an error to assign to a block key after the block.'
with pytest.raises(ParseError):
loads_xbc('a { a = 1 }\na = 1')

View File

@ -1,4 +1,8 @@
'''
Tests for inputs which are invalid.
'''
import pytest
from pyparsing.exceptions import ParseException
@ -7,37 +11,45 @@ from xbc import loads_xbc, ParseError
# this should fail but does not.
@pytest.mark.xfail
def test_whitespace_vc():
with pytest.raises(ParseError):
loads_xbc('x = a\n, b')
'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():
with pytest.raises(ParseException):
loads_xbc("x = '''")
'Strings cannot contain the quote style that bookends them.'
with pytest.raises(ParseException):
loads_xbc("x = '''")
def test_lone_plus():
with pytest.raises(ParseException):
loads_xbc('+')
'Bare operators are not permitted.'
with pytest.raises(ParseException):
loads_xbc('+')
def test_lone_rbrace():
with pytest.raises(ParseException):
loads_xbc('}')
'Braces can only appear as part of a block.'
with pytest.raises(ParseException):
loads_xbc('}')
def test_lone_lbrace():
with pytest.raises(ParseException):
loads_xbc('{')
'Braces can only appear as part of a block.'
with pytest.raises(ParseException):
loads_xbc('{')
def test_lone_braces():
with pytest.raises(ParseException):
loads_xbc('{}')
'Braces can only appear as part of a block.'
with pytest.raises(ParseException):
loads_xbc('{}')
def test_lone_semi():
with pytest.raises(ParseException):
loads_xbc(';')
'Semicolons are only permitted as part of a key or key/value statement.'
with pytest.raises(ParseException):
loads_xbc(';')
def test_empty():
with pytest.raises(ParseException):
loads_xbc('\n')
with pytest.raises(ParseException):
loads_xbc('')
'XBC files cannot be empty.'
with pytest.raises(ParseException):
loads_xbc('\n')
with pytest.raises(ParseException):
loads_xbc('')

View File

@ -1,139 +1,150 @@
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():
i = '''feature.option.foo = 1
'Key/value example.'
i = '''feature.option.foo = 1
feature.option.bar = 2'''
d = {
'feature.option': False,
'feature': False,
'feature.option.foo': '1',
'feature.option.bar': '2'
}
assert loads_xbc(i) == d
d = {
'feature.option': False,
'feature': False,
'feature.option.foo': '1',
'feature.option.bar': '2'
}
assert loads_xbc(i) == d
def test_02():
i = '''feature.option {
foo = 1
bar = 2
'Block example.'
i = '''feature.option {
foo = 1
bar = 2
}'''
d = {
'feature.option': False,
'feature': False,
'feature.option.foo': '1',
'feature.option.bar': '2'
}
assert loads_xbc(i) == d
d = {
'feature.option': False,
'feature': False,
'feature.option.foo': '1',
'feature.option.bar': '2'
}
assert loads_xbc(i) == d
def test_03():
i = 'feature.options = "foo", "bar"'
d = {
'feature': False,
'feature.options': ['foo', 'bar']
}
assert loads_xbc(i) == d
'Array example.'
i = 'feature.options = "foo", "bar"'
d = {
'feature': False,
'feature.options': ['foo', 'bar']
}
assert loads_xbc(i) == d
def test_10():
i = 'feature.option{foo=1;bar=2}'
d = {
'feature.option': False,
'feature': False,
'feature.option.foo': '1',
'feature.option.bar': '2'
}
assert loads_xbc(i) == d
'Compact example.'
i = 'feature.option{foo=1;bar=2}'
d = {
'feature.option': False,
'feature': False,
'feature.option.foo': '1',
'feature.option.bar': '2'
}
assert loads_xbc(i) == d
def test_11():
i = '''ftrace.event {
task.task_newtask {
filter = "pid < 128"
enable
}
kprobes.vfs_read {
probes = "vfs_read $arg1 $arg2"
filter = "common_pid < 200"
enable
}
synthetic.initcall_latency {
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_finish {
actions = "hist:keys=func:lat=common_timestamp.usecs-$ts0:onmatch(initcall.initcall_start).initcall_latency(func,$lat)"
}
'Example of a possible configuration.'
i = '''ftrace.event {
task.task_newtask {
filter = "pid < 128"
enable
}
kprobes.vfs_read {
probes = "vfs_read $arg1 $arg2"
filter = "common_pid < 200"
enable
}
synthetic.initcall_latency {
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_finish {
actions = "hist:keys=func:lat=common_timestamp.usecs-$ts0:onmatch(initcall.initcall_start).initcall_latency(func,$lat)"
}
}'''
d = {
'ftrace': False,
'ftrace.event': False,
'ftrace.event.task': False,
'ftrace.event.task.task_newtask': False,
'ftrace.event.task.task_newtask.filter': "pid < 128",
'ftrace.event.task.task_newtask.enable': True,
'ftrace.event.kprobes': False,
'ftrace.event.kprobes.vfs_read': False,
'ftrace.event.kprobes.vfs_read.probes': "vfs_read $arg1 $arg2",
'ftrace.event.kprobes.vfs_read.filter': "common_pid < 200",
'ftrace.event.kprobes.vfs_read.enable': True,
'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.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_finish': False,
'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
d = {
'ftrace': False,
'ftrace.event': False,
'ftrace.event.task': False,
'ftrace.event.task.task_newtask': False,
'ftrace.event.task.task_newtask.filter': "pid < 128",
'ftrace.event.task.task_newtask.enable': True,
'ftrace.event.kprobes': False,
'ftrace.event.kprobes.vfs_read': False,
'ftrace.event.kprobes.vfs_read.probes': "vfs_read $arg1 $arg2",
'ftrace.event.kprobes.vfs_read.filter': "common_pid < 200",
'ftrace.event.kprobes.vfs_read.enable': True,
'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.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_finish': False,
'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():
i = '''ftrace.event.synthetic.initcall_latency {
fields = "unsigned long func", "u64 lat"
hist {
from {
event = initcall.initcall_start
key = func
assigns = "ts0=common_timestamp.usecs"
}
to {
event = initcall.initcall_finish
key = func
assigns = "lat=common_timestamp.usecs-$ts0"
onmatch = func, $lat
}
keys = func.sym, lat
vals = lat
sort = lat
}
'Another example of a possible configuration.'
i = '''ftrace.event.synthetic.initcall_latency {
fields = "unsigned long func", "u64 lat"
hist {
from {
event = initcall.initcall_start
key = func
assigns = "ts0=common_timestamp.usecs"
}
to {
event = initcall.initcall_finish
key = func
assigns = "lat=common_timestamp.usecs-$ts0"
onmatch = func, $lat
}
keys = func.sym, lat
vals = lat
sort = lat
}
}'''
d = {
'ftrace': False,
'ftrace.event': False,
'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.hist': 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.key': 'func',
'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.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.onmatch': ['func', '$lat'],
'ftrace.event.synthetic.initcall_latency.hist.keys': ['func.sym', 'lat'],
'ftrace.event.synthetic.initcall_latency.hist.vals': 'lat',
'ftrace.event.synthetic.initcall_latency.hist.sort': 'lat'
}
assert loads_xbc(i) == d
d = {
'ftrace': False,
'ftrace.event': False,
'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.hist': 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.key': 'func',
'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.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.onmatch': ['func', '$lat'],
'ftrace.event.synthetic.initcall_latency.hist.keys': ['func.sym', 'lat'],
'ftrace.event.synthetic.initcall_latency.hist.vals': 'lat',
'ftrace.event.synthetic.initcall_latency.hist.sort': 'lat'
}
assert loads_xbc(i) == d