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 ```xbc
foo { foo {
bar { bar {
fluff = 1 fluff = 1
} }
} }
# is equivalent to # is equivalent to
foo.bar.fluff = 1 foo.bar.fluff = 1

View File

@ -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]
@ -43,3 +43,101 @@ 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"]

View File

@ -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']

View File

@ -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': {'"': '&quot;', "'": '&apos;'}, 'html': {'"': '&quot;', "'": '&apos;'},
'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']

View File

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

View File

@ -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')

View File

@ -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')

View File

@ -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('')

View File

@ -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