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

@ -10,7 +10,7 @@ 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',
@ -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,6 +19,17 @@
# 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
@ -43,102 +54,125 @@ from pyparsing import (
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
'An XBC XBCNode.'
def __init__(self, *args, kind=None):
if isinstance(args[0], str): if isinstance(args[0], str):
self.args = args[0] self.args = args[0]
elif isinstance(args[0][0], str): elif isinstance(args[0][0], str):
self.args = args[0][0] self.args = args[0][0]
else: else:
self.args = args[0][0][0] self.args = args[0][0][0]
self.type = type self.type = kind
@property @property
def key(self): def key(self):
'The key associated with the node.'
return self.args[0] return self.args[0]
class Key(Node): class XBCKey(XBCNode):
# pylint: disable=too-few-public-methods
'An XBC key.'
def __init__(self, *args): def __init__(self, *args):
# pylint: disable=super-init-not-called
self.args = args[0] self.args = args[0]
self.type = 'key' self.type = 'key'
class KeyValue(Node): class XBCKeyValue(XBCNode):
'An XBC key/value operation.'
def __init__(self, *args): def __init__(self, *args):
super().__init__(args, type='kv') super().__init__(args, kind='kv')
@property @property
def op(self): def op(self):
'The operator being performed.'
return self.args[1] return self.args[1]
@property @property
def value(self): def value(self):
'The data associated with the operation.'
return self.args[2] return self.args[2]
class Block(Node): class XBCBlock(XBCNode):
'An XBC block.'
def __init__(self, *args): def __init__(self, *args):
super().__init__(args, type='block') super().__init__(args, kind='block')
@property @property
def contents(self): def contents(self):
'The contents of the block.'
return self.args[1] 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):
'Remove quotes and trailing whitespace from values.'
if val[0] in '\'"' and val[0] == val[-1]: if val[0] in '\'"' and val[0] == val[-1]:
return val[1:-1] return val[1:-1]
return val.strip() return val.strip()
def key_walk(d, key): def key_walk(d, key):
'Walk the key to guard against post-block key assignments.'
split = key.split('.') split = key.split('.')
for i in range(len(split) - 1, 0, -1): for i in range(len(split) - 1, 0, -1):
@ -148,6 +182,8 @@ def key_walk(d, key):
d[x] = False d[x] = False
def parse_block(key, seq): def parse_block(key, seq):
# pylint: disable=too-many-branches,too-many-statements
'Parse the AST in to a real data structure.'
if isinstance(seq, list) and len(seq) == 1 and isinstance(seq[0], list): if isinstance(seq, list) and len(seq) == 1 and isinstance(seq[0], list):
seq = seq[0] seq = seq[0]
@ -159,12 +195,13 @@ def parse_block(key, seq):
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
key_walk(ret, k)
else: else:
raise ParseError(f'key {k} already defined') raise ParseError(f'key {k} already defined')
elif isinstance(item, KeyValue): elif isinstance(item, XBCKeyValue):
value = item.value value = item.value
op = item.op op = item.op
@ -189,8 +226,8 @@ def parse_block(key, seq):
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)
@ -201,7 +238,7 @@ def parse_block(key, seq):
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:
@ -220,6 +257,7 @@ def parse_block(key, seq):
return ret return ret
def parse(data): def parse(data):
'Call the lexer and then the parser.'
tree = lex(data) tree = lex(data)
d = parse_block(None, tree) d = parse_block(None, tree)
@ -227,14 +265,17 @@ def parse(data):
return d return d
def loads_xbc(data): def loads_xbc(data):
'Load XBC data provided in a string.'
return parse(data) 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.'
with open(fp, mode='r', encoding='UTF-8') as f:
return loads_xbc(f.read()) 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.'
lens = [len(x) for x in seq]
shortest = min(lens) shortest = min(lens)
if shortest < 1: if shortest < 1:
@ -245,7 +286,7 @@ def longest_key(L):
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:
@ -253,40 +294,42 @@ def longest_key(L):
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):
'Find the longest keys in the sequence provided.'
keys = [k.split('.') for k in keys] keys = [k.split('.') for k in keys]
ret = set() 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):
# pylint: disable=too-many-locals,too-many-branches
'Create XBC blocks.'
ret = [] 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:
@ -321,11 +364,13 @@ def make_block(data):
return ret return ret
def saves_xbc(data): def saves_xbc(data):
'Export the provided dictionary to an XBC-formatted string.'
ret = make_block(data) ret = make_block(data)
return '\n'.join(ret) 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.'
with open(filename, mode='w', encoding='UTF-8') as f:
f.write(saves_xbc(data)) 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,29 +19,37 @@
# 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'):
'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 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')
@ -49,30 +57,30 @@ def quote(data, escape='backslash'):
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 other[f] not in data[1:]: if f in QUOTES and QUOTEMAP[f] not in data[1:]:
q = other[f] q = QUOTEMAP[f]
return f'{q}{data}{q}'
# not a quoted string, but has only one kind of quote # not a quoted string, but has only one kind of quote
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: elif '"' in data and "'" not in data:
return f"'{data}'" q = "'"
# not a quoted string and has both types; we escape one
else: else:
# not a quoted string and has both types; we escape one
data = data.replace("'", esc["'"]) data = data.replace("'", esc["'"])
return f"'{data}" q = "'"
return f'{q}{data}{q}'
def normalise_string(string): def normalise_string(string):
'Normalise values according to XBC rules.'
if not isinstance(string, str): if not isinstance(string, str):
string = str(string) string = str(string)
@ -82,36 +90,41 @@ def normalise_string(string):
return string return string
def normalise(data): def normalise(data):
'''Normalise values according to XBC rules.'''
if isinstance(data, str) or not isinstance(data, (Sequence, Mapping)): if isinstance(data, str) or not isinstance(data, (Sequence, Mapping)):
return normalise_string(data) return normalise_string(data)
elif isinstance(data, Sequence):
L = [] if isinstance(data, Sequence):
ret = []
for item in data: for item in data:
if isinstance(item, str) or not isinstance(item, (Sequence, Mapping)): if isinstance(item, str) or not isinstance(item, (Sequence, Mapping)):
L.append(normalise_string(item)) ret.append(normalise_string(item))
# we can unwind nested sequences # we can unwind nested sequences
elif isinstance(item, Sequence): elif isinstance(item, Sequence):
L.extend(normalise(item)) ret.extend(normalise(item))
# ...but we can't do that with mappings, the format doesn't # ...but we can't do that with mappings, the format doesn't
# support it. # support it.
elif isinstance(item, Mapping): elif isinstance(item, Mapping):
raise ValueError('nested mapping') raise ValueError('nested mapping')
else: else:
raise ValueError(type(value)) # this should be impossible to reach, but nothing truly is.
L = ', '.join(L) raise ValueError(type(item))
return L
elif isinstance(data, Mapping): ret = ', '.join(ret)
return ret
if isinstance(data, Mapping):
d = {} d = {}
for k, v in data.items(): for k, v in data.items():
if Key.pattern.match(k) is None: if KEY_RE.match(k) is None:
raise KeyError(k) raise KeyError(k)
else:
k = Key(k)
v = normalise(v) v = normalise(v)
d[k] = v d[k] = v
return d return d
return data return data

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():
'A key by itself exists.'
assert loads_xbc('a') == {'a': True} 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():
'A key with an empty assignment exists.'
assert loads_xbc('a =') == {'a': True} assert loads_xbc('a =') == {'a': True}
def test_keyvalue(): def test_keyvalue():
'A bare value can be assigned to a key.'
assert loads_xbc('a = 1') == {'a': '1'} assert loads_xbc('a = 1') == {'a': '1'}
def test_keyvalue_space(): def test_keyvalue_space():
'A bare value can have spaces.'
assert loads_xbc('a = a b') == {'a': 'a b'} assert loads_xbc('a = a b') == {'a': 'a b'}
def test_dot_keyvalue(): def test_dot_keyvalue():
'A key being assigned to can have dots.'
assert loads_xbc('a.a = 1') == {'a': False, 'a.a': '1'} assert loads_xbc('a.a = 1') == {'a': False, 'a.a': '1'}
def test_keys(): def test_keys():
'Statements can be separated by semicolons.'
assert loads_xbc('a;b') == {'a': True, 'b': True} 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():
'Values can be quoted with single or double quotes.'
assert loads_xbc('a = "b"') == {'a': 'b'} assert loads_xbc('a = "b"') == {'a': 'b'}
def test_quoted_space(): def test_quoted_space():
'Quoted values can have trailing whitespace preserved.'
assert loads_xbc('a = "b "') == {'a': 'b '} assert loads_xbc('a = "b "') == {'a': 'b '}
def test_array(): def test_array():
'Multiple values can be assigned to a single key.'
assert loads_xbc('a = 1, 2') == {'a': ['1', '2']} assert loads_xbc('a = 1, 2') == {'a': ['1', '2']}
def test_reassignment(): def test_reassignment():
'Keys cannot be reassigned.'
with pytest.raises(ParseError): with pytest.raises(ParseError):
loads_xbc('a = 1\na = 2') loads_xbc('a = 1\na = 2')
def test_reassignment_colon(): def test_reassignment_colon():
'Keys cannot be reassigned, even in compound statements.'
with pytest.raises(ParseError): with pytest.raises(ParseError):
loads_xbc('a = 1;a = 2') loads_xbc('a = 1;a = 2')
def test_ovewrite_nonexistent(): def test_ovewrite_nonexistent():
'Keys can only be updated if they exist.'
with pytest.raises(ParseError): with pytest.raises(ParseError):
loads_xbc('a := 1') 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():
'An empty block should cause the key to exist.'
assert loads_xbc('a {}') == {'a': True} assert loads_xbc('a {}') == {'a': True}
def test_keyvalue(): def test_keyvalue():
'A block should support key/value pairs.'
assert loads_xbc('a { a = 1 }') == {'a': False, 'a.a': '1'} assert loads_xbc('a { a = 1 }') == {'a': False, 'a.a': '1'}
def test_nested_block(): def test_nested_block():
'A block should support having block members.'
assert loads_xbc('a { b { c = 1 } }') == {'a.b.c': '1', 'a': False, 'a.b': False} 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():
'Attempting to assign to the same key should be an error inside a block too.'
with pytest.raises(ParseError): with pytest.raises(ParseError):
loads_xbc('a { a = 1; a = 2 }') loads_xbc('a { a = 1; a = 2 }')
def test_assign_after_block(): def test_assign_after_block():
'It is an error to assign to a block key after the block.'
with pytest.raises(ParseError): with pytest.raises(ParseError):
loads_xbc('a { a = 1 }\na = 1') 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,36 +11,44 @@ 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():
'Whitespace is prohibited between values and commas.'
with pytest.raises(ParseError): with pytest.raises(ParseError):
loads_xbc('x = a\n, b') 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():
'Strings cannot contain the quote style that bookends them.'
with pytest.raises(ParseException): with pytest.raises(ParseException):
loads_xbc("x = '''") loads_xbc("x = '''")
def test_lone_plus(): def test_lone_plus():
'Bare operators are not permitted.'
with pytest.raises(ParseException): with pytest.raises(ParseException):
loads_xbc('+') loads_xbc('+')
def test_lone_rbrace(): def test_lone_rbrace():
'Braces can only appear as part of a block.'
with pytest.raises(ParseException): with pytest.raises(ParseException):
loads_xbc('}') loads_xbc('}')
def test_lone_lbrace(): def test_lone_lbrace():
'Braces can only appear as part of a block.'
with pytest.raises(ParseException): with pytest.raises(ParseException):
loads_xbc('{') loads_xbc('{')
def test_lone_braces(): def test_lone_braces():
'Braces can only appear as part of a block.'
with pytest.raises(ParseException): with pytest.raises(ParseException):
loads_xbc('{}') loads_xbc('{}')
def test_lone_semi(): def test_lone_semi():
'Semicolons are only permitted as part of a key or key/value statement.'
with pytest.raises(ParseException): with pytest.raises(ParseException):
loads_xbc(';') loads_xbc(';')
def test_empty(): def test_empty():
'XBC files cannot be empty.'
with pytest.raises(ParseException): with pytest.raises(ParseException):
loads_xbc('\n') loads_xbc('\n')
with pytest.raises(ParseException): with pytest.raises(ParseException):

View File

@ -1,14 +1,15 @@
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():
'Key/value example.'
i = '''feature.option.foo = 1 i = '''feature.option.foo = 1
feature.option.bar = 2''' feature.option.bar = 2'''
d = { d = {
@ -20,6 +21,7 @@ feature.option.bar = 2'''
assert loads_xbc(i) == d assert loads_xbc(i) == d
def test_02(): def test_02():
'Block example.'
i = '''feature.option { i = '''feature.option {
foo = 1 foo = 1
bar = 2 bar = 2
@ -33,6 +35,7 @@ def test_02():
assert loads_xbc(i) == d assert loads_xbc(i) == d
def test_03(): def test_03():
'Array example.'
i = 'feature.options = "foo", "bar"' i = 'feature.options = "foo", "bar"'
d = { d = {
'feature': False, 'feature': False,
@ -41,6 +44,7 @@ def test_03():
assert loads_xbc(i) == d assert loads_xbc(i) == d
def test_10(): def test_10():
'Compact example.'
i = 'feature.option{foo=1;bar=2}' i = 'feature.option{foo=1;bar=2}'
d = { d = {
'feature.option': False, 'feature.option': False,
@ -51,6 +55,7 @@ def test_10():
assert loads_xbc(i) == d assert loads_xbc(i) == d
def test_11(): def test_11():
'Example of a possible configuration.'
i = '''ftrace.event { i = '''ftrace.event {
task.task_newtask { task.task_newtask {
filter = "pid < 128" filter = "pid < 128"
@ -87,16 +92,21 @@ def test_11():
'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':
"hist:keys=func.sym,lat:vals=lat:sort=lat",
'ftrace.event.initcall': False, 'ftrace.event.initcall': False,
'ftrace.event.initcall.initcall_start': False, 'ftrace.event.initcall.initcall_start': False,
'ftrace.event.initcall.initcall_start.actions': "hist:keys=func:ts0=common_timestamp.usecs", 'ftrace.event.initcall.initcall_start.actions':
"hist:keys=func:ts0=common_timestamp.usecs",
'ftrace.event.initcall.initcall_finish': False, 'ftrace.event.initcall.initcall_finish': False,
'ftrace.event.initcall.initcall_finish.actions': "hist:keys=func:lat=common_timestamp.usecs-$ts0:onmatch(initcall.initcall_start).initcall_latency(func,$lat)" 'ftrace.event.initcall.initcall_finish.actions':
"hist:keys=func:lat=common_timestamp.usecs-$ts0:onmatch(" +
"initcall.initcall_start).initcall_latency(func,$lat)"
} }
assert loads_xbc(i) == d assert loads_xbc(i) == d
def test_12(): def test_12():
'Another example of a possible configuration.'
i = '''ftrace.event.synthetic.initcall_latency { i = '''ftrace.event.synthetic.initcall_latency {
fields = "unsigned long func", "u64 lat" fields = "unsigned long func", "u64 lat"
hist { hist {
@ -130,7 +140,8 @@ def test_12():
'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':
"lat=common_timestamp.usecs-$ts0",
'ftrace.event.synthetic.initcall_latency.hist.to.onmatch': ['func', '$lat'], '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.keys': ['func.sym', 'lat'],
'ftrace.event.synthetic.initcall_latency.hist.vals': 'lat', 'ftrace.event.synthetic.initcall_latency.hist.vals': 'lat',