From dd0e227216e247d2040cdd40bf7397702880cdc4 Mon Sep 17 00:00:00 2001 From: Ulf Magnusson Date: Mon, 9 Oct 2017 23:05:00 +0200 Subject: Kconfiglib 2 backup WIP --- examples/allnoconfig.py | 87 +- examples/allnoconfig_simpler.py | 47 +- examples/allyesconfig.py | 133 +- examples/defconfig.py | 13 +- examples/defconfig_oldconfig.py | 12 +- examples/eval_expr.py | 20 +- examples/help_grep.py | 109 +- examples/print_refs.py | 13 - examples/print_sym_info.py | 56 +- examples/print_tree.py | 80 +- examples/print_undefined.py | 15 - kconfiglib.py | 5221 +++++++++++++++++++-------------------- makefile.patch | 2 +- tests/Kchoice | 26 +- tests/Kdefconfig_existent | 1 + tests/Keval | 1 + tests/Klocation | 79 +- tests/Klocation_included | 43 +- tests/Kmisc | 2 +- tests/Kmodifiable | 50 - tests/Kprompt | 77 - tests/Krelation | 2 + tests/Krepr | 61 + tests/Kstr | 76 + tests/Ktext | 145 -- tests/Kvisibility | 161 +- testsuite.py | 2371 +++++++----------- 27 files changed, 4155 insertions(+), 4748 deletions(-) delete mode 100644 examples/print_refs.py delete mode 100644 examples/print_undefined.py delete mode 100644 tests/Kmodifiable delete mode 100644 tests/Kprompt create mode 100644 tests/Krepr create mode 100644 tests/Kstr delete mode 100644 tests/Ktext diff --git a/examples/allnoconfig.py b/examples/allnoconfig.py index 9423352..edc473d 100644 --- a/examples/allnoconfig.py +++ b/examples/allnoconfig.py @@ -1,40 +1,57 @@ -# Works like allnoconfig. Automatically verified by the testsuite to generate -# identical output to 'make allnoconfig' for all ARCHes. The looping is done in -# case setting one symbol to "n" allows other symbols to be set to "n" (due to -# dependencies). +# Works like 'make allnoconfig'. Verified by the test suite to generate +# identical output to 'make allnoconfig' for all ARCHes. +# +# See allnoconfig_simpler.py for a much simpler version. This version +# demonstrates some tree walking and value processing. +# +# Usage: +# +# $ make [ARCH=] scriptconfig SCRIPT=Kconfiglib/examples/allnoconfig.py -import kconfiglib +from kconfiglib import Config, Symbol, tri_less import sys -conf = kconfiglib.Config(sys.argv[1]) - -# Do an initial pass to give allnoconfig_y symbols the user value 'y'. It might -# be possible to handle this through "successive raising" similarly to the -# "successive lowering" below too, but keep it simple. -for sym in conf: - if sym.get_type() in (kconfiglib.BOOL, kconfiglib.TRISTATE) and \ - sym.is_allnoconfig_y(): - sym.set_user_value('y') - -done = False -while not done: - done = True - - for sym in conf: - # Choices take care of themselves for allnoconfig, so we only need to - # worry about non-choice symbols - if not sym.is_choice_symbol() and not sym.is_allnoconfig_y(): - # If we can assign a value to the symbol (where "n", "m" and "y" - # are ordered from lowest to highest), then assign the lowest - # value. lower_bound() returns None for symbols whose values cannot - # (currently) be changed, as well as for non-bool/tristate symbols. - lower_bound = sym.get_lower_bound() - if lower_bound is not None and \ - kconfiglib.tri_less(lower_bound, sym.get_value()): - - sym.set_user_value(lower_bound) - # We just changed the value of some symbol. As this may affect - # other symbols, keep going. - done = False +def do_allnoconfig(node): + global no_changes + + # Walk the tree of menu nodes. You can imagine this as going down/into menu + # entries in the menuconfig interface, setting each to 'n' (or the lowest + # assignable value). + + while node is not None: + if isinstance(node.item, Symbol): + sym = node.item + + # Is the symbol a non-choice symbol that can be set to a lower + # value than its current value? + if sym.choice is None and sym.assignable and \ + tri_less(sym.assignable[0], sym.value): + + # Yup, lower it + sym.set_value(sym.assignable[0]) + no_changes = False + + # Recursively lower children + if node.list is not None: + do_allnoconfig(node.list) + + node = node.next + +conf = Config(sys.argv[1]) + +while 1: + # For tricky dependencies involving '!', setting later symbols to 'n' might + # actually raise the value of earlier symbols. To be super safe, we do + # additional passes until a pass no longer changes the value of any symbol. + # + # This isn't actually needed for any ARCH in the kernel as of 4.14. A + # single pass gives the correct result. + no_changes = True + + do_allnoconfig(conf.top_menu) + + # Did the pass change any symbols? + if no_changes: + break conf.write_config(".config") diff --git a/examples/allnoconfig_simpler.py b/examples/allnoconfig_simpler.py index f3cfe9a..e732669 100644 --- a/examples/allnoconfig_simpler.py +++ b/examples/allnoconfig_simpler.py @@ -1,28 +1,35 @@ # This is a simpler version of allnoconfig.py, corresponding to how the C -# implementation does it. Setting a user value that's not in the assignable -# range of the symbol (between get_lower_bound() and get_upper_bound(), or, -# equivalently, not in get_assignable_values()) is OK; the value will simply -# get truncated downwards or upwards as determined by the visibility and -# selects. +# implementation does it. Verified by the test suite to produce identical +# output to 'make allnoconfig' for all ARCHes. +# +# Usage: +# +# $ make [ARCH=] scriptconfig SCRIPT=Kconfiglib/examples/allnoconfig_simpler.py +# +# Implementation/performance note +# =============================== +# +# Kconfiglib immediately invalidates (flags for recalculation) all (possibly) +# dependent symbols when a value is assigned to a symbol, which slows this down +# a bit (due to tons of redundant invalidation), but makes any assignment +# pattern safe ("just works"). Config.load_config() instead invalidates all +# symbols up front, making it much faster. If you really need to eke out +# performance, look at how load_config() does things (which involves internal +# APIs that don't invalidate symbols). This has been fast enough for all cases +# I've seen so far though (around 3 seconds for this particular script on my +# Core i7 2600K, including the initial Kconfig parsing). -# This version is a bit slower compared allnoconfig.py since Kconfiglib -# invalidates all dependent symbols for each set_user_value() call. This does not -# happen for load_config(), which instead invalidates all symbols once after -# the configuration has been loaded. This is OK for load_config() since nearly -# all symbols will tend to be affected anyway. - -import kconfiglib +from kconfiglib import Config, BOOL, TRISTATE import sys -conf = kconfiglib.Config(sys.argv[1]) +conf = Config(sys.argv[1]) -# Avoid warnings printed by Kconfiglib when assigning a user value with -# set_user_value() to a symbol that has no prompt (such assignments never have -# an effect) -conf.set_print_warnings(False) +# Avoid warnings printed by Kconfiglib when assigning a value to a symbol that +# has no prompt. Such assignments never have an effect. +conf.disable_warnings() -for sym in conf: - if sym.get_type() in (kconfiglib.BOOL, kconfiglib.TRISTATE): - sym.set_user_value("y" if sym.is_allnoconfig_y() else "n") +for sym in conf.defined_syms: + if sym.type in (BOOL, TRISTATE): + sym.set_value("y" if sym.is_allnoconfig_y else "n") conf.write_config(".config") diff --git a/examples/allyesconfig.py b/examples/allyesconfig.py index 906343c..4847c05 100644 --- a/examples/allyesconfig.py +++ b/examples/allyesconfig.py @@ -1,65 +1,112 @@ -# Works like allyesconfig. This is a bit more involved than allnoconfig as we -# need to handle choices in two different modes: +# Works like 'make allyesconfig'. Verified by the test suite to generate +# identical output to 'make allyesconfig' for all ARCHES. # -# "y": One symbol is "y", the rest are "n". -# "m": Any number of symbols are "m", the rest are "n". +# This could be implemented as a straightforward tree walk just like +# allnoconfig.py (or even simpler like allnoconfig_simpler.py), but do it a bit +# differently (roundabout) just to demonstrate some other possibilities. # -# Only tristate choices can be in "m" mode. It is safe since the code for two -# conflicting options will appear as separate modules instead of simultaneously -# in the kernel. +# allyesconfig is a bit more involved than allnoconfig as we need to handle +# choices in two different modes: # -# If a choice can be in "y" mode, it will be. If it can only be in "m" mode -# (due to dependencies), then all the options will be set to "m". +# y: One symbol is "y", the rest are "n" +# m: Any number of symbols are "m", the rest are "n" # -# The looping is in case setting one symbol to "y" (or "m") allows the value of -# other symbols to be raised. +# Only tristate choices can be in "m" mode. No "m" mode choices seem to appear +# for allyesconfig on the kernel Kconfigs as of 4.14, but we still handle it. +# +# Usage: +# +# $ make [ARCH=] scriptconfig SCRIPT=Kconfiglib/examples/allyesconfig.py -import kconfiglib +from kconfiglib import Config, Choice, tri_less import sys -conf = kconfiglib.Config(sys.argv[1]) +conf = Config(sys.argv[1]) + +# Collect all the choices in the configuration. Demonstrates how the menu node +# tree can be walked iteratively by using the parent pointers. + +choices = [] +node = conf.top_menu + +while 1: + if isinstance(node.item, Choice): + choices.append(node.item) + + # Iterative tree walking by using parent pointers. + # + # Recursing on next pointers can blow the Python stack. Recursing on child + # pointers is safe (as is done in the other examples). This gives a + # template for how you can avoid recursing on both. The same logic is found + # in the C implementation. + + if node.list is not None: + # Jump to child node if available + node = node.list + + elif node.next is not None: + # Otherwise, jump to next node if available + node = node.next -# Get a list of all symbols that are not in choices -non_choice_syms = [sym for sym in conf.get_symbols() if - not sym.is_choice_symbol()] + else: + # Otherwise, look for parents with next nodes to jump to + while node.parent is not None: + node = node.parent + if node.next is not None: + node = node.next + break + else: + # No parents with next nodes, all nodes visited + break -done = False -while not done: - done = True +# Collect all symbols that are not in choices +non_choice_syms = [sym for sym in conf.defined_syms if sym.choice is None] + +while 1: + no_changes = True # Handle symbols outside of choices for sym in non_choice_syms: - upper_bound = sym.get_upper_bound() - - # See corresponding comment for allnoconfig implementation - if upper_bound is not None and \ - kconfiglib.tri_less(sym.get_value(), upper_bound): - sym.set_user_value(upper_bound) - done = False + # See allnoconfig example. [-1] gives the last (highest) assignable + # value. + if sym.assignable and tri_less(sym.value, sym.assignable[-1]): + sym.set_value(sym.assignable[-1]) + no_changes = False - # Handle symbols within choices + # Handle choices - for choice in conf.get_choices(): + for choice in choices: + # Handle a choice whose visibility allows it to be in "y" mode - # Handle choices whose visibility allow them to be in "y" mode + if choice.visibility == "y": + selection = choice.default_selection - if choice.get_visibility() == "y": - selection = choice.get_selection_from_defaults() + # Does the choice have a default selection that we haven't already + # selected? if selection is not None and \ - selection is not choice.get_user_selection(): - selection.set_user_value("y") - done = False + selection is not choice.user_value: - # Handle choices whose visibility only allow them to be in "m" mode. + # Yup, select it + selection.set_value("y") + no_changes = False + + # Handle a choice whose visibility only allows it to be in "m" mode. # This might happen if a choice depends on a symbol that can only be - # "m" for example. - - elif choice.get_visibility() == "m": - for sym in choice.get_symbols(): - if sym.get_value() != "m" and \ - sym.get_upper_bound() != "n": - sym.set_user_value("m") - done = False + # "m", for example. + + elif choice.visibility == "m": + for sym in choice.symbols: + + # Does the choice have a symbol that can be "m" that we haven't + # already set to "m"? + if sym.user_value != "m" and "m" in sym.assignable: + + # Yup, set it + sym.set_value("m") + no_changes = False + + if no_changes: + break conf.write_config(".config") diff --git a/examples/defconfig.py b/examples/defconfig.py index 3e958e2..ce2bf6e 100644 --- a/examples/defconfig.py +++ b/examples/defconfig.py @@ -1,4 +1,8 @@ # Works like entering "make menuconfig" and immediately saving and exiting +# +# Usage: +# +# $ make [ARCH=] scriptconfig SCRIPT=Kconfiglib/examples/allyesconfig.py import kconfiglib import os @@ -7,11 +11,12 @@ import sys conf = kconfiglib.Config(sys.argv[1]) if os.path.exists(".config"): + print("using existing .config") conf.load_config(".config") else: - defconfig = conf.get_defconfig_filename() - if defconfig is not None: - print("Using" + defconfig) - conf.load_config(defconfig) + if conf.defconfig_filename is not None: + print("using " + conf.defconfig_filename) + conf.load_config(conf.defconfig_filename) conf.write_config(".config") +print("configuration written to .config") diff --git a/examples/defconfig_oldconfig.py b/examples/defconfig_oldconfig.py index 9a85440..98173e7 100644 --- a/examples/defconfig_oldconfig.py +++ b/examples/defconfig_oldconfig.py @@ -7,6 +7,10 @@ # yes n | make oldconfig # # This came up in https://github.com/ulfalizer/Kconfiglib/issues/15. +# +# Usage: +# +# $ make [ARCH=] scriptconfig SCRIPT=Kconfiglib/examples/defconfig_oldconfig.py import kconfiglib import sys @@ -19,15 +23,15 @@ conf.write_config(".config") # Mirrors the first oldconfig conf.load_config(".config") -conf["ETHERNET"].set_user_value('n') +conf.syms["ETHERNET"].set_value('n') conf.write_config(".config") # Mirrors the second oldconfig conf.load_config(".config") -conf["ETHERNET"].set_user_value('y') +conf.syms["ETHERNET"].set_value('y') for s in conf: - if s.get_user_value() is None and 'n' in s.get_assignable_values(): - s.set_user_value('n') + if s.user_value is None and 'n' in s.assignable: + s.set_value('n') # Write the final configuration conf.write_config(".config") diff --git a/examples/eval_expr.py b/examples/eval_expr.py index edb33e6..a907f35 100644 --- a/examples/eval_expr.py +++ b/examples/eval_expr.py @@ -1,8 +1,22 @@ -# Evaluates an expression in the context of a configuration. (Here we could -# load a .config as well.) +# Evaluates an expression (e.g. "X86_64 || (X86_32 && X86_LOCAL_APIC)") in the +# context of a configuration. Note that this always yields a tristate value (n, +# m, or y). +# +# Usage: +# +# $ make [ARCH=] scriptconfig SCRIPT=Kconfiglib/examples/eval_expr.py SCRIPT_ARG= import kconfiglib import sys +if len(sys.argv) < 3: + print('Pass symbol name (without "CONFIG_" prefix) with SCRIPT_ARG=NAME') + sys.exit(1) + conf = kconfiglib.Config(sys.argv[1]) -print(conf.eval("(TRACE_IRQFLAGS_SUPPORT || PPC32) && STACKTRACE_SUPPORT")) + +# Enable modules so that 'm' doesn't get demoted to 'n' +conf.syms["MODULES"].set_value("y") + +print("the expression '{}' evaluates to {}" + .format(sys.argv[2], conf.eval_string(sys.argv[2]))) diff --git a/examples/help_grep.py b/examples/help_grep.py index 61ac936..fed1731 100644 --- a/examples/help_grep.py +++ b/examples/help_grep.py @@ -1,49 +1,72 @@ -# Does a case-insensitive search for a string in the help texts for symbols and -# choices and the titles of menus and comments. Prints the matching items -# together with their locations and the matching text. Used like +# Does a case-insensitive search for a regular expression in the help texts of +# symbols and choices and the prompts of menus and comments. Prints the +# matching items together with their locations and the matching text. # -# $ make [ARCH=] scriptconfig SCRIPT=Kconfiglib/examples/help_grep.py SCRIPT_ARG= +# Usage: +# +# $ make [ARCH=] scriptconfig SCRIPT=Kconfiglib/examples/help_grep.py SCRIPT_ARG= +# +# Shortened example output for SCRIPT_ARG=general: +# +# menu "General setup" +# location: init/Kconfig:39 +# +# config SYSVIPC +# bool +# prompt "System V IPC" +# help +# ... +# exchange information. It is generally considered to be a good thing, +# ... +# +# location: init/Kconfig:233 +# +# config BSD_PROCESS_ACCT +# bool +# prompt "BSD Process Accounting" if MULTIUSER +# help +# ... +# information. This is generally a good idea, so say Y. +# +# location: init/Kconfig:403 +# +# ... + -import kconfiglib +from kconfiglib import Config, Symbol, Choice, MENU, COMMENT +import re import sys if len(sys.argv) < 3: - print('Pass search string with SCRIPT_ARG="search string"') + print('Pass the regex with SCRIPT_ARG=regex') sys.exit(1) -search_string = sys.argv[2].lower() - -conf = kconfiglib.Config(sys.argv[1]) - -for item in conf.get_symbols() + \ - conf.get_choices() + conf.get_menus() + conf.get_comments(): - if item.is_symbol() or item.is_choice(): - text = item.get_help() - elif item.is_menu(): - text = item.get_title() - else: - # Comment - text = item.get_text() - - # Case-insensitive search - if text is not None and search_string in text.lower(): - if item.is_symbol() or item.is_choice(): - # Indent lines in help text. (There might be a nicer way. :) - text = "\n".join([" " + s for s in text.splitlines()]) - - # Don't worry about symbols/choices defined in multiple locations to - # keep things simple - fname, linenr = item.get_def_locations()[0] - if item.is_symbol(): - print("config {0} at {1}:{2}:\n{3}" - .format(item.get_name(), fname, linenr, text)) - elif item.is_choice(): - print("choice at {0}:{1}:\n{2}".format(fname, linenr, text)) - - else: - # Menu or comment - fname, linenr = item.get_location() - if item.is_menu(): - print('menu "{0}" at {1}:{2}'.format(text, fname, linenr)) - else: - # Comment - print('comment "{0}" at {1}:{2}'.format(text, fname, linenr)) + +search = re.compile(sys.argv[2], re.IGNORECASE).search + +def search_tree(node): + while node is not None: + match = False + + if isinstance(node.item, (Symbol, Choice)) and \ + node.help is not None and search(node.help): + print(node.item) + match = True + + elif node.item == MENU and search(node.prompt[0]): + print('menu "{}"'.format(node.prompt[0])) + match = True + + elif node.item == COMMENT and search(node.prompt[0]): + print('comment "{}"'.format(node.prompt[0])) + match = True + + if match: + print("location: {}:{}\n".format(node.filename, node.linenr)) + + if node.list is not None: + search_tree(node.list) + + node = node.next + +conf = Config(sys.argv[1]) +search_tree(conf.top_menu) diff --git a/examples/print_refs.py b/examples/print_refs.py deleted file mode 100644 index ea62223..0000000 --- a/examples/print_refs.py +++ /dev/null @@ -1,13 +0,0 @@ -# Prints the names of all symbols that reference a particular symbol. (There's -# also a method get_selected_symbols() for determining just selection -# relations.) - -import kconfiglib -import sys - -conf = kconfiglib.Config(sys.argv[1]) - -x86 = conf["X86"] -for sym in conf: - if x86 in sym.get_referenced_symbols(): - print(sym.get_name()) diff --git a/examples/print_sym_info.py b/examples/print_sym_info.py index c913358..2c1b0f0 100644 --- a/examples/print_sym_info.py +++ b/examples/print_sym_info.py @@ -1,18 +1,52 @@ -# Loads a Kconfig and a .config and prints information about a symbol. +# Loads a Kconfig and a .config and prints a symbol. +# +# Usage: +# +# $ make [ARCH=] scriptconfig SCRIPT=Kconfiglib/examples/print_sym_info.py SCRIPT_ARG= +# +# Example output for SCRIPT_ARG=modules: +# +# config MODULES +# bool +# prompt "Enable loadable module support" +# option modules +# help +# Kernel modules are small pieces of compiled code which can +# be inserted in the running kernel, rather than being +# permanently built into the kernel. You use the "modprobe" +# tool to add (and sometimes remove) them. If you say Y here, +# many parts of the kernel can be built as modules (by +# answering M instead of Y where indicated): this is most +# useful for infrequently used options which are not required +# for booting. For more information, see the man pages for +# modprobe, lsmod, modinfo, insmod and rmmod. +# +# If you say Y here, you will need to run "make +# modules_install" to put the modules under /lib/modules/ +# where modprobe can find them (you may need to be root to do +# this). +# +# If unsure, say Y. +# +# value = n +# visibility = y +# currently assignable values: n, y +# defined at init/Kconfig:1678 import kconfiglib import sys -# Create a Config object representing a Kconfig configuration. (Any number of -# these can be created -- the library has no global state.) +if len(sys.argv) < 3: + print('Pass symbol name (without "CONFIG_" prefix) with SCRIPT_ARG=') + sys.exit(1) + conf = kconfiglib.Config(sys.argv[1]) +sym = conf.syms[sys.argv[2]] -# Load values from a .config file. 'srctree' is an environment variable set by -# the Linux makefiles to the top-level directory of the kernel tree. It needs -# to be used here for the script to work with alternative build directories -# (specified e.g. with O=). -conf.load_config("$srctree/arch/x86/configs/i386_defconfig") +print(sym) +print("value = " + sym.value) +print("visibility = " + sym.visibility) +print("currently assignable values: " + ", ".join(sym.assignable)) -# Print some information about a symbol. (The Config class implements -# __getitem__() to provide a handy syntax for getting symbols.) -print(conf["SERIAL_UARTLITE_CONSOLE"]) +for node in sym.nodes: + print("defined at {}:{}".format(node.filename, node.linenr)) diff --git a/examples/print_tree.py b/examples/print_tree.py index 1405ed5..da795c9 100644 --- a/examples/print_tree.py +++ b/examples/print_tree.py @@ -1,23 +1,67 @@ -# Prints a tree of all items in the configuration +# Prints the menu tree of the configuration. Dependencies between symbols can +# sometimes implicitly alter the menu structure (see kconfig-language.txt), and +# that's implemented too. +# +# Usage: +# +# $ make [ARCH=] scriptconfig SCRIPT=Kconfiglib/examples/print_tree.py +# +# Example output: +# +# ... +# config HAVE_KERNEL_LZO +# config HAVE_KERNEL_LZ4 +# choice +# config KERNEL_GZIP +# config KERNEL_BZIP2 +# config KERNEL_LZMA +# config KERNEL_XZ +# config KERNEL_LZO +# config KERNEL_LZ4 +# config DEFAULT_HOSTNAME +# config SWAP +# config SYSVIPC +# config SYSVIPC_SYSCTL +# config POSIX_MQUEUE +# config POSIX_MQUEUE_SYSCTL +# config CROSS_MEMORY_ATTACH +# config FHANDLE +# config USELIB +# config AUDIT +# config HAVE_ARCH_AUDITSYSCALL +# config AUDITSYSCALL +# config AUDIT_WATCH +# config AUDIT_TREE +# menu "IRQ subsystem" +# config MAY_HAVE_SPARSE_IRQ +# config GENERIC_IRQ_LEGACY +# config GENERIC_IRQ_PROBE +# ... -import kconfiglib +from kconfiglib import Config, Symbol, Choice, MENU, COMMENT import sys -def print_with_indent(s, indent): +def indent_print(s, indent): print((" " * indent) + s) -def print_items(items, indent): - for item in items: - if item.is_symbol(): - print_with_indent("config {0}".format(item.get_name()), indent) - elif item.is_menu(): - print_with_indent('menu "{0}"'.format(item.get_title()), indent) - print_items(item.get_items(), indent + 2) - elif item.is_choice(): - print_with_indent('choice', indent) - print_items(item.get_items(), indent + 2) - elif item.is_comment(): - print_with_indent('comment "{0}"'.format(item.get_text()), indent) - -conf = kconfiglib.Config(sys.argv[1]) -print_items(conf.get_top_level_items(), 0) +def print_items(node, indent): + while node is not None: + if isinstance(node.item, Symbol): + indent_print("config " + node.item.name, indent) + + elif isinstance(node.item, Choice): + indent_print("choice", indent) + + elif node.item == MENU: + indent_print('menu "{0}"'.format(node.prompt[0]), indent) + + elif node.item == COMMENT: + indent_print('comment "{0}"'.format(node.prompt[0]), indent) + + if node.list is not None: + print_items(node.list, indent + 2) + + node = node.next + +conf = Config(sys.argv[1]) +print_items(conf.top_menu, 0) diff --git a/examples/print_undefined.py b/examples/print_undefined.py deleted file mode 100644 index fb8120b..0000000 --- a/examples/print_undefined.py +++ /dev/null @@ -1,15 +0,0 @@ -# Prints the names of all symbols that are referenced but never defined in the -# current configuration together with the locations where they are referenced. -# Integers being included in the list is not a bug, as these need to be treated -# as symbols per the design of Kconfig. - -import kconfiglib -import sys - -conf = kconfiglib.Config(sys.argv[1]) - -for sym in conf.get_symbols(): - if not sym.is_defined(): - print(sym.get_name()) - for (filename, linenr) in sym.get_ref_locations(): - print(" {0}:{1}".format(filename, linenr)) diff --git a/kconfiglib.py b/kconfiglib.py index 186c931..e4d0b58 100644 --- a/kconfiglib.py +++ b/kconfiglib.py @@ -1,71 +1,73 @@ -# This is Kconfiglib, a Python library for scripting, debugging, and extracting -# information from Kconfig-based configuration systems. To view the -# documentation, run -# -# $ pydoc kconfiglib -# -# or, if you prefer HTML, -# -# $ pydoc -w kconfiglib -# -# The examples/ subdirectory contains examples, to be run with e.g. -# -# $ make scriptconfig SCRIPT=Kconfiglib/examples/print_tree.py -# -# Look in testsuite.py for the test suite. - """ -Kconfiglib is a Python library for scripting and extracting information from -Kconfig-based configuration systems. Features include the following: +Kconfiglib is a Python 2/3 library for scripting and extracting information +from Kconfig-based configuration systems. Features include the following: + + - Programmatic getting and setting of symbol values - - Symbol values and properties can be looked up and values assigned - programmatically. - - .config files can be read and written. - - Expressions can be evaluated in the context of a Kconfig configuration. - - Relations between symbols can be quickly determined, such as finding all - symbols that reference a particular symbol. - - Highly compatible with the scripts/kconfig/*conf utilities. The test suite - automatically compares outputs between Kconfiglib and the C implementation - for a large number of cases. + - Reading/writing of .config files -For the Linux kernel, scripts are run using + - Expression inspection and evaluation. All expressions are exposed and use a + simple format that can be processed manually if needed. - $ make scriptconfig [ARCH=] SCRIPT= [SCRIPT_ARG=] + - Menu tree inspection. The underlying menu tree is exposed, including + submenus created implicitly by symbols depending on preceding symbols. This + can be used e.g. to implement menuconfig-like functionality. -Using the 'scriptconfig' target ensures that required environment variables -(SRCARCH, ARCH, srctree, KERNELVERSION, etc.) are set up correctly. + - Highly compatible with the standard Kconfig C tools: The test suite compares + outputs between Kconfiglib and the C tools on real-world kernel Kconfig and + defconfig files for a large number of cases (by diffing generated .configs). -Scripts receive the name of the Kconfig file to load in sys.argv[1]. As of -Linux 4.1.0-rc5, this is always "Kconfig" from the kernel top-level directory. -If an argument is provided with SCRIPT_ARG, it appears as sys.argv[2]. + - Pretty speedy by pure Python standards: Parses the x86 Kconfigs in about a + second on a Core i7 2600K (with a warm file cache). For long-running jobs, + PyPy gives a nice speedup. -To get an interactive Python prompt with Kconfiglib preloaded and a Config -object 'c' created, run +For the Linux kernel, a handy interface is provided by the +scripts/kconfig/Makefile patch. For experimentation, you can use the +iscriptconfig target, which gives an interactive Python prompt where the +configuration for ARCH has been loaded: - $ make iscriptconfig [ARCH=] + $ make [ARCH=] iscriptconfig -Kconfiglib supports both Python 2 and Python 3. For (i)scriptconfig, the Python -interpreter to use can be passed in PYTHONCMD, which defaults to 'python'. PyPy -works well too, and might give a nice speedup for long-running jobs. +To run a script, use the scriptconfig target: -The examples/ directory contains short example scripts, which can be run with -e.g. + $ make [ARCH=] scriptconfig SCRIPT= [SCRIPT_ARG=] - $ make scriptconfig SCRIPT=Kconfiglib/examples/print_tree.py +See the examples/ subdirectory for example scripts. -or - $ make scriptconfig SCRIPT=Kconfiglib/examples/help_grep.py SCRIPT_ARG=kernel +The Makefile patch is used to pick up the ARCH, SRCARCH, and KERNELVERSION +environment variables (and any future environment variables that might get +used). If you want to run Kconfiglib without the Makefile patch, the following +will probably work in practice (it's what the test suite does in 'speedy' mode, +except it tests all ARCHes): -testsuite.py contains the test suite. See the top of the script for how to run -it. + $ ARCH=x86 SRCARCH=x86 KERNELVERSION=`make kernelversion` python script.py -Credits: Written by Ulf "Ulfalizer" Magnusson +ARCH and SRCARCH (the arch/ subdirectory) might differ in some cases. Search +for "Additional ARCH settings for" in the top-level Makefile to see the +possible variations. The value of KERNELVERSION doesn't seem to matter as of +Linux 4.14. -Send bug reports, suggestions and other feedback to ulfalizer a.t Google's -email service. Don't wrestle with internal APIs. Tell me what you need and I -might add it in a safe way as a client API instead.""" +Kconfiglib will warn if you forget to set some environment variable that's +referenced in the configuration (via 'option env="ENV_VAR"'). + +When using scriptconfig, scripts receive the name of the Kconfig file to load +in sys.argv[1]. As far as I can tell, this is always "Kconfig" from the kernel +top-level directory as of Linux 4.14. If an argument is provided with +SCRIPT_ARG, it appears as sys.argv[2]. + + +Kconfiglib supports both Python 2 and Python 3 (and PyPy). For (i)scriptconfig, +the Python interpreter to use can be passed in PYTHONCMD, which defaults to +"python". + + +Send bug reports, suggestions, and questions to ulfalizer a.t Google's email +service (or open a ticket on the GitHub page). +""" + +import errno import os import platform import re @@ -87,801 +89,925 @@ import sys # class Config(object): - - """Represents a Kconfig configuration, e.g. for i386 or ARM. This is the - set of symbols and other items appearing in the configuration together with - their values. Creating any number of Config objects -- including for - different architectures -- is safe; Kconfiglib has no global state.""" + """ + Represents a Kconfig configuration, e.g. for x86 or ARM. This is the set of + symbols, choices, and menu nodes appearing in the configuration. Creating + any number of Config objects (including for different architectures) is + safe. Kconfiglib doesn't keep any global state. + + The following attributes are available on Config instances. They should be + viewed as read-only, and some are implemented through @property magic. + Modifying symbols is fine, but not the 'syms' dictionary itself. + + syms: + A dictionary with all symbols in the configuration. The key is the name + of the symbol, so that e.g. conf.syms["MODULES"] returns the MODULES + symbol. Symbols that are referenced in expressions but never defined are + included as well. + + defined_syms: + A list of all defined symbols, in the same order as they appear in the + Kconfig files. Provided as a convenience (and also used internally). The + defined symbols are those whose 'nodes' attribute is non-empty. + + named_choices: + A dictionary like 'syms' for named choices (choice FOO). This is mostly + for completeness. I've never seen named choices being used. + + top_menu: + The menu node (see the MenuNode class) of the top-level menu. Acts as the + root of the menu tree. + + mainmenu_text: + The prompt (title) of the top_menu menu, with Kconfig variable references + ("$FOO") expanded. Defaults to "Linux Kernel Configuration" (like in the + C tools). Can be changed with the 'mainmenu' statement (see + kconfig-language.txt). + + defconfig_filename: + The filename given by the 'option defconfig_list' symbol. This is the + first existing file with a satisfied condition among the 'default' + properties of the symbol. If a file is not found at the given path, it is + also looked up relative to $srctree if set ($srctree/foo/defconfig is + looked up if foo/defconfig is not found). + + Has the value None if either no defconfig_list symbol exists, or if it + has no 'default' with a satisfied dependency that points to an existing + file. + + References to Kconfig symbols ("$FOO") are expanded in 'default' + properties. + + Setting 'option defconfig_list' on multiple symbols ignores symbols past + the first one. + + Do print(c.syms["DEFCONFIG_LIST"]) on a kernel configuration to see an + example of a defconfig_list symbol. + + Something to look out for is that scripts/kconfig/Makefile might use the + --defconfig= option when calling the C tools of e.g. 'make + defconfig'. This option overrides the 'option defconfig_list' symbol, + meaning defconfig_filename might not match what 'make defconfig' would + use. + + srctree: + The value of the $srctree environment variable when the configuration was + loaded, or None if $srctree wasn't set. Kconfig and .config files are + looked up relative to $srctree if they are not found in the base path + (unless absolute paths are specified). This is to support out-of-tree + builds. The C tools use this variable in the same way. + + Changing $srctree after loading the configuration has no effect. Only the + value when the configuration is loaded matters. This avoids surprises if + multiple configurations are loaded with different values for $srctree. + + config_prefix: + The value of the $CONFIG_ environment variable when the configuration was + loaded. This is the prefix used (and expected) in .config files. Defaults + to "CONFIG_". Used in the same way in the C tools. + + Like for srctree, only the value of $CONFIG_ when the configuration is + loaded matters. + """ + + __slots__ = ( + "_choices", + "_print_undef_assign", + "_print_warnings", + "_set_re", + "_unset_re", + "config_prefix", + "defconfig_list", + "defined_syms", + "modules", + "named_choices", + "srctree", + "syms", + "top_menu", + ) # # Public interface # - def __init__(self, filename="Kconfig", base_dir=None, print_warnings=True, - print_undef_assign=False): - """Creates a new Config object, representing a Kconfig configuration. - Raises Kconfig_Syntax_Error on syntax errors. - - filename (default: "Kconfig"): The base Kconfig file of the - configuration. For the Linux kernel, you'll probably want "Kconfig" - from the top-level directory, as environment variables will make - sure the right Kconfig is included from there - (arch//Kconfig). If you are using Kconfiglib via 'make - scriptconfig', the filename of the base base Kconfig file will be in - sys.argv[1]. - - base_dir (default: None): The base directory relative to which 'source' - statements within Kconfig files will work. For the Linux kernel this - should be the top-level directory of the kernel tree. $-references - to existing environment variables will be expanded. - - If None (the default), the environment variable 'srctree' will be - used if set, and the current directory otherwise. 'srctree' is set - by the Linux makefiles to the top-level kernel directory. A default - of "." would not work with an alternative build directory. - - print_warnings (default: True): Set to True if warnings related to this - configuration should be printed to stderr. This can be changed later - with Config.set_print_warnings(). It is provided as a constructor - argument since warnings might be generated during parsing. - - print_undef_assign (default: False): Set to True if informational - messages related to assignments to undefined symbols should be - printed to stderr for this configuration. Can be changed later with - Config.set_print_undef_assign().""" - - # The set of all symbols, indexed by name (a string) - self._syms = {} - - # The set of all defined symbols in the configuration in the order they - # appear in the Kconfig files. This excludes the special symbols n, m, - # and y as well as symbols that are referenced but never defined. - self._defined_syms = [] - - # The set of all named choices (yes, choices can have names), indexed - # by name (a string) - self._named_choices = {} - - # Lists containing all choices, menus, and comments in the - # configuration + def __init__(self, filename="Kconfig", warn=True): + """ + Creates a new Config object by parsing Kconfig files. Raises + KconfigSyntaxError on syntax errors. Note that Kconfig files are not + the same as .config files (which store configuration symbol values). + + filename (default: "Kconfig"): + The base Kconfig file. For the Linux kernel, you'll want "Kconfig" + from the top-level directory, as environment variables will make sure + the right Kconfig is included from there + (arch//Kconfig). If you are using Kconfiglib via 'make + scriptconfig', the filename of the base base Kconfig file will be in + sys.argv[1] (always "Kconfig" in practice). + + The $srctree environment variable is used if set (see the class + documentation). + + warn (default: True): + True if warnings related to this configuration should be printed to + stderr. This can be changed later with + Config.enable/disable_warnings(). It is provided as a constructor + argument since warnings might be generated during parsing. + """ + + self.syms = {} + self.defined_syms = [] + self.named_choices = {} + + # Used for quickly invalidating all choices self._choices = [] - self._menus = [] - self._comments = [] - - def register_special_symbol(type_, name, val): - sym = Symbol() - sym._is_special = True - sym._is_defined = True - sym._config = self - sym._name = name - sym._type = type_ - sym._cached_val = val - self._syms[name] = sym - return sym - - # The special symbols n, m and y, used as shorthand for "n", "m" and - # "y" - self._n = register_special_symbol(TRISTATE, "n", "n") - self._m = register_special_symbol(TRISTATE, "m", "m") - self._y = register_special_symbol(TRISTATE, "y", "y") - # DEFCONFIG_LIST uses this - register_special_symbol(STRING, "UNAME_RELEASE", platform.uname()[2]) + + # Predefined symbol. DEFCONFIG_LIST has been seen using this. + uname_sym = Symbol() + uname_sym._type = STRING + uname_sym.name = "UNAME_RELEASE" + uname_sym.config = self + uname_sym.defaults.append((platform.uname()[2], None)) + # env_var doubles as the SYMBOL_AUTO flag from the C implementation, so + # just set it to something. The naming breaks a bit here, but it's + # pretty obscure. + uname_sym.env_var = "" + self.syms["UNAME_RELEASE"] = uname_sym # The symbol with "option defconfig_list" set, containing a list of # default .config files - self._defconfig_sym = None - - # See Symbol.get_(src)arch() - self._arch = os.environ.get("ARCH") - self._srcarch = os.environ.get("SRCARCH") + self.defconfig_list = None - # If you set CONFIG_ in the environment, Kconfig will prefix all - # symbols with its value when saving the configuration, instead of - # using the default, "CONFIG_". - self._config_prefix = os.environ.get("CONFIG_") - if self._config_prefix is None: - self._config_prefix = "CONFIG_" + self.config_prefix = os.environ.get("CONFIG_") + if self.config_prefix is None: + self.config_prefix = "CONFIG_" # Regular expressions for parsing .config files self._set_re = re.compile(r"{}(\w+)=(.*)" - .format(self._config_prefix)) + .format(self.config_prefix)) self._unset_re = re.compile(r"# {}(\w+) is not set" - .format(self._config_prefix)) + .format(self.config_prefix)) - self._kconfig_filename = filename + self.srctree = os.environ.get("srctree") - # See Config.__init__(). We need this for get_defconfig_filename(). - self._srctree = os.environ.get("srctree") - - if base_dir is None: - self._base_dir = "." if self._srctree is None else self._srctree - else: - self._base_dir = os.path.expandvars(base_dir) + self._print_warnings = warn + self._print_undef_assign = False - # The 'mainmenu' text - self._mainmenu_text = None + self.top_menu = MenuNode() + self.top_menu.config = self + self.top_menu.item = MENU + self.top_menu.visibility = None + self.top_menu.prompt = ("Linux Kernel Configuration", None) + self.top_menu.parent = None + self.top_menu.dep = None + self.top_menu.filename = filename + self.top_menu.linenr = 1 - # The filename of the most recently loaded .config file - self._config_filename = None - # The textual header of the most recently loaded .config, uncommented - self._config_header = None + # We hardcode MODULES for backwards compatibility. Proper support via + # 'option modules' wouldn't be that tricky to add with backwards + # compatibility either though. + self.modules = self._lookup_sym("MODULES") - self._print_warnings = print_warnings - self._print_undef_assign = print_undef_assign + # Parse the Kconfig files + self._parse_block(_FileFeed(self._open(filename), filename), + None, self.top_menu, None, None, self.top_menu) - # When parsing properties, we stop on the first (non-empty) - # non-property line. _end_line and _end_line_tokens hold that line and - # its tokens so that we don't have to re-tokenize the line later. This - # isn't just an optimization: We record references to symbols during - # tokenization, so tokenizing twice would cause double registration. - # - # self._end_line doubles as a flag where None means we don't have a - # cached tokenized line. - self._end_line = None - # self.end_line_tokens is set later during parsing + self.top_menu.list = self.top_menu.next + self.top_menu.next = None - # Parse the Kconfig files - self._top_block = [] - self._parse_file(filename, None, None, None, self._top_block) + _finalize_tree(self.top_menu) # Build Symbol._direct_dependents for all symbols self._build_dep() - def get_arch(self): - """Returns the value the environment variable ARCH had at the time the - Config instance was created, or None if ARCH was not set. For the - kernel, this corresponds to the architecture being built for, with - values such as "i386" or "mips".""" - return self._arch - - def get_srcarch(self): - """Returns the value the environment variable SRCARCH had at the time - the Config instance was created, or None if SRCARCH was not set. For - the kernel, this corresponds to the particular arch/ subdirectory - containing architecture-specific code.""" - return self._srcarch - - def get_srctree(self): - """Returns the value the environment variable 'srctree' had at the time - the Config instance was created, or None if 'srctree' was not defined. - This variable points to the source directory and is used when building - in a separate directory.""" - return self._srctree - - def get_base_dir(self): - """Returns the base directory relative to which 'source' statements - will work, passed as an argument to Config.__init__().""" - return self._base_dir - - def get_kconfig_filename(self): - """Returns the name of the (base) kconfig file this configuration was - loaded from.""" - return self._kconfig_filename - - def get_config_filename(self): - """Returns the filename of the most recently loaded configuration file, - or None if no configuration has been loaded.""" - return self._config_filename - - def get_config_header(self): - """Returns the (uncommented) textual header of the .config file most - recently loaded with load_config(). Returns None if no .config file has - been loaded or if the most recently loaded .config file has no header. - - The header consists of all lines up to but not including the first line - that either (1) does not begin with "#", or (2) matches - "# CONFIG_FOO is not set".""" - return self._config_header - - def get_mainmenu_text(self): - """Returns the text of the 'mainmenu' statement (with $-references to - symbols replaced by symbol values), or None if the configuration has no - 'mainmenu' statement.""" - return None if self._mainmenu_text is None else \ - self._expand_sym_refs(self._mainmenu_text) - - def get_defconfig_filename(self): - """Returns the name of the defconfig file, which is the first existing - file in the list given in a symbol having 'option defconfig_list' set. - $-references to symbols will be expanded ("$FOO bar" -> "foo bar" if - FOO has the value "foo"). Returns None in case of no defconfig file. - Setting 'option defconfig_list' on multiple symbols ignores the symbols - past the first one (and prints a warning). - - If the environment variable 'srctree' was set when the Config was - created, each defconfig specified with a relative path will be - searched for in $srcdir if it is not found at the specified path (i.e., - if foo/defconfig is not found, $srctree/foo/defconfig will be looked - up). - - WARNING: A wart here is that scripts/kconfig/Makefile sometimes uses - the --defconfig= option when calling the C implementation of - e.g. 'make defconfig'. This option overrides the 'option - defconfig_list' symbol, meaning the result from - get_defconfig_filename() might not match what 'make defconfig' would - use. That probably ought to be worked around somehow, so that this - function always gives the "expected" result.""" - if self._defconfig_sym is None: + @property + def mainmenu_text(self): + """ + See the class documentation. + """ + return self._expand_sym_refs(self.top_menu.prompt[0]) + + @property + def defconfig_filename(self): + """ + See the class documentation. + """ + if self.defconfig_list is None: return None - for filename, cond_expr in self._defconfig_sym._def_exprs: - if self._eval_expr(cond_expr) != "n": + for filename, cond_expr in self.defconfig_list.defaults: + if eval_expr(cond_expr) != "n": filename = self._expand_sym_refs(filename) - if os.access(filename, os.R_OK): - return filename - # defconfig not found. If the path is a relative path and - # $srctree is set, we also look in $srctree. - if not os.path.isabs(filename) and self._srctree is not None: - filename = os.path.join(self._srctree, filename) - if os.access(filename, os.R_OK): - return filename + try: + with self._open(filename) as f: + return f.name + except IOError: + continue return None - def get_symbol(self, name): - """Returns the symbol with name 'name', or None if no such symbol - appears in the configuration. An alternative shorthand is conf[name], - where conf is a Config instance, though that will instead raise - KeyError if the symbol does not exist.""" - return self._syms.get(name) - - def __getitem__(self, name): - """Returns the symbol with name 'name'. Raises KeyError if the symbol - does not appear in the configuration.""" - return self._syms[name] - - def get_symbols(self, all_symbols=True): - """Returns a list of symbols from the configuration. An alternative for - iterating over all defined symbols (in the order of definition) is - - for sym in config: - ... - - which relies on Config implementing __iter__() and is equivalent to - - for sym in config.get_symbols(False): - ... - - all_symbols (default: True): If True, all symbols -- including special - and undefined symbols -- will be included in the result, in an - undefined order. If False, only symbols actually defined and not - merely referred to in the configuration will be included in the - result, and will appear in the order that they are defined within - the Kconfig configuration files.""" - return list(self._syms.values()) if all_symbols else \ - self._defined_syms - - def __iter__(self): - """Convenience function for iterating over the set of all defined - symbols in the configuration, used like - - for sym in conf: - ... - - The iteration happens in the order of definition within the Kconfig - configuration files. Symbols only referred to but not defined will not - be included, nor will the special symbols n, m, and y. If you want to - include such symbols as well, see config.get_symbols().""" - return iter(self._defined_syms) - - def get_choices(self): - """Returns a list containing all choice statements in the - configuration, in the order they appear in the Kconfig files.""" - return self._choices - - def get_menus(self): - """Returns a list containing all menus in the configuration, in the - order they appear in the Kconfig files.""" - return self._menus - - def get_comments(self): - """Returns a list containing all comments in the configuration, in the - order they appear in the Kconfig files.""" - return self._comments - - def get_top_level_items(self): - """Returns a list containing the items (symbols, menus, choices, and - comments) at the top level of the configuration -- that is, all items - that do not appear within a menu or choice. The items appear in the - same order as within the configuration.""" - return self._top_block - def load_config(self, filename, replace=True): - """Loads symbol values from a file in the familiar .config format. - Equivalent to calling Symbol.set_user_value() to set each of the - values. + """ + Loads symbol values from a file in the .config format. Equivalent to + calling Symbol.set_value() to set each of the values. "# CONFIG_FOO is not set" within a .config file is treated specially - and sets the user value of FOO to 'n'. The C implementation works the - same way. - - filename: The .config file to load. $-references to existing - environment variables will be expanded. For scripts to work even when - an alternative build directory is used with the Linux kernel, you - need to refer to the top-level kernel directory with "$srctree". - - replace (default: True): True if the configuration should replace the - old configuration; False if it should add to it.""" - - # Put this first so that a missing file doesn't screw up our state - filename = os.path.expandvars(filename) - line_feeder = _FileFeed(filename) - - self._config_filename = filename - - # - # Read header - # - - if not self._is_header_line(line_feeder.peek_next()): - self._config_header = None - else: - # Kinda inefficient, but this is an unlikely hotspot - self._config_header = "" - while self._is_header_line(line_feeder.peek_next()): - self._config_header += line_feeder.get_next()[1:] - # Makes c.write_config(".config", c.get_config_header()) preserve - # the header exactly. We also handle weird cases like a .config - # file with just "# foo" and no trailing newline in it (though we - # would never generate that ourselves), hence the slight - # awkwardness. - if self._config_header.endswith("\n"): - self._config_header = self._config_header[:-1] - - # - # Read assignments. Hotspot for some workloads. - # + and sets the user value of FOO to 'n'. The C tools work the same way. - if replace: - # This invalidates all symbols as a side effect - self.unset_user_values() - else: - self._invalidate_all() - - # Small optimization - set_re_match = self._set_re.match - unset_re_match = self._unset_re.match - - while 1: - line = line_feeder.get_next() - if line is None: - return + filename: + The .config file to load. The $srctree variable is used if set (see + the class documentation). - line = line.rstrip() + replace (default: True): True if all existing user values should + be cleared before loading the .config. + """ - set_match = set_re_match(line) - if set_match: - name, val = set_match.groups() - if name not in self._syms: - self._warn_undef_assign_load( - name, val, line_feeder.filename, line_feeder.linenr) - continue + with self._open(filename) as f: + if replace: + # Invalidates all symbols as a side effect + self.unset_values() + else: + self._invalidate_all() + + # Small optimizations + set_re_match = self._set_re.match + unset_re_match = self._unset_re.match + syms = self.syms + + for linenr, line in enumerate(f, 1): + # The C tools ignore trailing whitespace + line = line.rstrip() + + set_match = set_re_match(line) + if set_match: + name, val = set_match.groups() + if name not in syms: + self._warn_undef_assign_load(name, val, filename, + linenr) + continue - sym = self._syms[name] + sym = syms[name] + + if sym._type == STRING and val.startswith('"'): + if len(val) < 2 or val[-1] != '"': + self._warn("malformed string literal", filename, + linenr) + continue + # Strip quotes and remove escapings. The unescaping + # procedure should be safe since " can only appear as + # \" inside the string. + val = val[1:-1].replace('\\"', '"') \ + .replace("\\\\", "\\") + + if sym.choice is not None: + mode = sym.choice.user_value + if mode is not None and mode != val: + self._warn("assignment to {} changes mode of " + 'containing choice from "{}" to "{}".' + .format(name, val, mode), + filename, linenr) - if sym._type == STRING and val.startswith('"'): - if len(val) < 2 or val[-1] != '"': - self._warn("malformed string literal", - line_feeder.filename, - line_feeder.linenr) + else: + unset_match = unset_re_match(line) + if not unset_match: continue - # Strip quotes and remove escapings. The unescaping - # procedure should be safe since " can only appear as \" - # inside the string. - val = val[1:-1].replace('\\"', '"') \ - .replace("\\\\", "\\") - - if sym._is_choice_sym: - user_mode = sym._parent._user_mode - if user_mode is not None and user_mode != val: - self._warn("assignment to {} changes mode of " - 'containing choice from "{}" to "{}".' - .format(name, val, user_mode), - line_feeder.filename, - line_feeder.linenr) - - else: - unset_match = unset_re_match(line) - if not unset_match: - continue - name = unset_match.group(1) - if name not in self._syms: - self._warn_undef_assign_load( - name, val, line_feeder.filename, line_feeder.linenr) - continue + name = unset_match.group(1) + if name not in syms: + self._warn_undef_assign_load(name, "n", filename, + linenr) + continue - sym = self._syms[name] - val = "n" + sym = syms[name] + val = "n" - # Done parsing the assignment. Set the value. + # Done parsing the assignment. Set the value. - if sym._user_val is not None: - self._warn('{} set more than once. Old value: "{}", new ' - 'value: "{}".' - .format(name, sym._user_val, val), - line_feeder.filename, line_feeder.linenr) + if sym.user_value is not None: + self._warn('{} set more than once. Old value: "{}", new ' + 'value: "{}".' + .format(name, sym.user_value, val), + filename, linenr) - sym._set_user_value_no_invalidate(val, True) + sym._set_value_no_invalidate(val, True) - def write_config(self, filename, header=None): - """Writes out symbol values in the familiar .config format. + def write_config(self, filename, + header="# Generated by Kconfiglib (https://github.com/ulfalizer/Kconfiglib)\n"): + """ + Writes out symbol values in .config format. - Kconfiglib makes sure the format matches what the C implementation - would generate, down to whitespace. This eases testing. + Kconfiglib makes sure the format matches what the C tools would + generate, down to whitespace. This eases testing. filename: The filename under which to save the configuration. - header (default: None): A textual header that will appear at the - beginning of the file, with each line commented out automatically. - Does not need to include a trailing newline. None means no - header.""" - - # Symbol._already_written is set to True when _add_config_strings() is - # called on a symbol, so that symbols defined in multiple locations - # only get one .config entry. We reset it prior to writing out a new - # .config. It only needs to be reset for defined symbols, because - # undefined symbols will never have _add_config_strings() called on - # them (because they do not appear in the block structure rooted at - # _top_block). - # - # The C implementation reuses _write_to_conf for this, but we cache - # _write_to_conf together with the value and don't invalidate cached - # values when writing .config files, so that won't work. - for sym in self._defined_syms: - sym._already_written = False - - # Build configuration. Avoiding string concatenation is worthwhile at - # least for PyPy. - config_strings = [] - add_fn = config_strings.append - for item in self._top_block: - item._add_config_strings(add_fn) - + header (default: "# Generated by Kconfiglib (https://github.com/ulfalizer/Kconfiglib)\n"): + Text that will be inserted verbatim at the beginning of the file. + You would usually want each line to start with '#' to make it a + comment, and include a final terminating newline. + """ with open(filename, "w") as f: - # Write header - if header is not None: - f.writelines(["#" + line - for line in (header + "\n").splitlines(True)]) - # Write configuration - f.writelines(config_strings) + f.write(header) + f.writelines(self._get_config_strings()) - def eval(self, s): - """Returns the value of the expression 's' -- where 's' is represented - as a string -- in the context of the configuration. Raises - Kconfig_Syntax_Error if syntax errors are detected in 's'. + def eval_string(self, s): + """ + Returns the value of the expression 's', represented as a string, in + the context of the configuration. Raises KconfigSyntaxError if syntax + errors are detected in 's'. - For example, if FOO and BAR are tristate symbols at least one of which - has the value "y", then config.eval("y && (FOO || BAR)") => "y" + As an example, if FOO and BAR are tristate symbols at least one of + which has the value "y", then config.eval_string("y && (FOO || BAR)") + returns "y". This function always yields a tristate value. To get the value of - non-bool, non-tristate symbols, use Symbol.get_value(). + non-bool, non-tristate symbols, use Symbol.value. The result of this function is consistent with how evaluation works for - conditional expressions in the configuration as well as in the C - implementation. "m" and m are rewritten as '"m" && MODULES' and 'm && - MODULES', respectively, and a result of "m" will get promoted to "y" if - we're running without modules. - - Syntax checking is somewhat lax, partly to be compatible with lax - parsing in the C implementation.""" - return self._eval_expr(self._parse_expr(self._tokenize(s, True), - None, # Current symbol/choice - s, - None, # filename - None, # linenr - True)) # transform_m - - def unset_user_values(self): - """Resets the values of all symbols, as if Config.load_config() or - Symbol.set_user_value() had never been called.""" - - # set_user_value() already rejects undefined symbols, and they don't + conditional ('if ...') expressions in the configuration (as well as in + the C tools). m is rewritten to 'm && MODULES'. + """ + return eval_expr(self._parse_expr(self._tokenize(s, True), + s, + None, # filename + None, # linenr + True)) # transform_m + + def unset_values(self): + """ + Resets the user values of all symbols, as if Config.load_config() or + Symbol.set_value() had never been called. + """ + + # set_value() already rejects undefined symbols, and they don't # need to be invalidated (because their value never changes), so we can # just iterate over defined symbols. - for sym in self._defined_syms: - # We're iterating over all symbols already, so no need for symbols - # to invalidate their dependent symbols - sym._unset_user_value_no_recursive_invalidate() + for sym in self.defined_syms: + # We're iterating over all symbols, so no need for symbols to + # invalidate their dependent symbols + sym.user_value = None + sym._invalidate() + + for choice in self._choices: + choice.user_value = choice.user_selection = None + choice._invalidate() - def set_print_warnings(self, print_warnings): - """Determines whether warnings related to this configuration (for - things like attempting to assign illegal values to symbols with - Symbol.set_user_value()) should be printed to stderr. + def enable_warnings(self): + """ + See Config.__init__(). + """ + self._print_warnings = True - print_warnings: True if warnings should be printed.""" - self._print_warnings = print_warnings + def disable_warnings(self): + """ + See Config.__init__(). + """ + self._print_warnings = False - def set_print_undef_assign(self, print_undef_assign): - """Determines whether informational messages related to assignments to - undefined symbols should be printed to stderr for this configuration. + def enable_undef_warnings(self): + """ + Enables printing of warnings to stderr for assignments to undefined + symbols. Disabled by default since it tends to be spammy for Kernel + configurations (and mostly suggests cleanups). + """ + self._print_undef_assign = True - print_undef_assign: If True, such messages will be printed.""" - self._print_undef_assign = print_undef_assign + def disable_undef_warnings(self): + """ + See enable_undef_assign(). + """ + self._print_undef_assign = False - def __str__(self): - """Returns a string containing various information about the Config.""" - return _lines("Configuration", - "File : " + - self._kconfig_filename, - "Base directory : " + - self._base_dir, - "Value of $ARCH at creation time : " + - ("(not set)" - if self._arch is None - else self._arch), - "Value of $SRCARCH at creation time : " + - ("(not set)" - if self._srcarch is None - else self._srcarch), - "Value of $srctree at creation time : " + - ("(not set)" - if self._srctree is None - else self._srctree), - "Most recently loaded .config : " + - ("(no .config loaded)" - if self._config_filename is None - else self._config_filename), - "Print warnings : " + - str(self._print_warnings), - "Print assignments to undefined symbols : " + - str(self._print_undef_assign)) + def __repr__(self): + """ + Prints some general information when a Config object is evaluated. + """ + fields = ( + "configuration with {} symbols".format(len(self.syms)), + 'main menu prompt "{}"'.format(self.mainmenu_text), + "srctree not set" if self.srctree is None else + 'srctree "{}"'.format(self.srctree), + 'config symbol prefix "{}"'.format(self.config_prefix), + "warnings " + ("enabled" if self._print_warnings else "disabled"), + "undef. symbol assignment warnings " + + ("enabled" if self._print_undef_assign else "disabled") + ) + + return "<{}>".format(", ".join(fields)) # # Private methods # # - # Kconfig parsing + # File reading # - def _parse_file(self, filename, parent, deps, visible_if_deps, block): - """Parses the Kconfig file 'filename'. Appends the Items in the file - (and any file it sources) to the list passed in the 'block' parameter. - See _parse_block() for the meaning of the parameters.""" - self._parse_block(_FileFeed(filename), None, parent, deps, - visible_if_deps, block) - - def _parse_block(self, line_feeder, end_marker, parent, deps, - visible_if_deps, block): - """Parses a block, which is the contents of either a file or an if, - menu, or choice statement. Appends the Items to the list passed in the - 'block' parameter. + def _open(self, filename): + """ + First tries to open 'filename', then '$srctree/filename' if $srctree + was set when the configuration was loaded. + """ + try: + return open(filename) + except IOError as e: + if not os.path.isabs(filename) and self.srctree is not None: + filename = os.path.join(self.srctree, filename) + try: + return open(filename) + except IOError as e2: + # This is needed for Python 3, because e2 is deleted after + # the try block: + # + # https://docs.python.org/3/reference/compound_stmts.html#the-try-statement + e = e2 + + raise IOError( + 'Could not open "{}" ({}: {}). Perhaps the $srctree ' + "environment variable (which was {}) is set incorrectly. Note " + "that the current value of $srctree is saved when the Config " + "instance is created (for consistency and to cleanly " + "separate instances)." + .format(filename, errno.errorcode[e.errno], e.strerror, + "unset" if self.srctree is None else + '"{}"'.format(self.srctree))) - line_feeder: A _FileFeed instance feeding lines from a file. The - Kconfig language is line-based in practice. + # + # Kconfig parsing + # - end_marker: The token that ends the block, e.g. _T_ENDIF ("endif") for - ifs. None for files. + def _tokenize(self, s, for_eval, filename=None, linenr=None): + """ + Returns a _Feed instance containing tokens derived from the string 's'. + Registers any new symbols encountered (via _lookup_sym()). - parent: The enclosing menu or choice, or None if we're at the top - level. + Tries to be reasonably speedy by processing chunks of text via regexes + and string operations where possible. This is a hotspot during parsing. - deps: Dependencies from enclosing menus, choices and ifs. + for_eval: + True when parsing an expression for a call to Config.eval_string(), + in which case we should not treat the first token specially nor + register new symbols. + """ - visible_if_deps (default: None): 'visible if' dependencies from - enclosing menus. + # Tricky implementation detail: While parsing a token, 'token' refers + # to the previous token. See _NOT_REF for why this is needed. - block: The list to add items to.""" + if for_eval: + token = None + tokens = [] - while 1: - # See the _end_line description in Config.__init__() - if self._end_line is not None: - line = self._end_line - tokens = self._end_line_tokens - self._end_line = None - else: - line = line_feeder.get_next() - if line is None: - if end_marker is not None: - raise Kconfig_Syntax_Error("Unexpected end of file " + - line_feeder.filename) - return + # The current index in the string being tokenized + i = 0 - tokens = self._tokenize(line, False, line_feeder.filename, - line_feeder.linenr) + else: + # See comment at _initial_token_re_match definition + initial_token_match = _initial_token_re_match(s) + if not initial_token_match: + return None - t0 = tokens.get_next() - if t0 is None: - continue + keyword = _get_keyword(initial_token_match.group(1)) + if keyword == _T_HELP: + # Avoid junk after "help", e.g. "---", being registered as a + # symbol + return _Feed((_T_HELP,)) + if keyword is None: + # We expect a keyword as the first token + _tokenization_error(s, filename, linenr) - # Cases are ordered roughly by frequency, which speeds things up a - # bit + token = keyword + tokens = [keyword] + # The current index in the string being tokenized + i = initial_token_match.end() - # This also handles 'menuconfig'. See the comment in the token - # definitions. - if t0 == _T_CONFIG: - # The tokenizer will automatically allocate a new Symbol object - # for any new names it encounters, so we don't need to worry - # about that here. - sym = tokens.get_next() + # Main tokenization loop (for tokens past the first one) + while i < len(s): + # Test for an identifier/keyword first. This is the most common + # case. + id_keyword_match = _id_keyword_re_match(s, i) + if id_keyword_match: + # We have an identifier or keyword - # Symbols defined in multiple places get the parent of their - # first definition. However, for symbols whose parents are - # choice statements, the choice statement takes precedence. - if not sym._is_defined or isinstance(parent, Choice): - sym._parent = parent - sym._is_defined = True + # Jump past it + i = id_keyword_match.end() - self._parse_properties(line_feeder, sym, deps, visible_if_deps) + # Check what it is. lookup_sym() will take care of allocating + # new symbols for us the first time we see them. Note that + # 'token' still refers to the previous token. - self._defined_syms.append(sym) - block.append(sym) + name = id_keyword_match.group(1) + keyword = _get_keyword(name) + if keyword is not None: + # It's a keyword + token = keyword - elif t0 == _T_SOURCE: - kconfig_file = tokens.get_next() - exp_kconfig_file = self._expand_sym_refs(kconfig_file) + elif token not in _STRING_LEX: + # It's a symbol + if name in ("n", "m", "y"): + # Always represent n, m, y as strings (constant + # symbols). This simplifies the expression logic. + token = name + else: + token = self._lookup_sym(name, for_eval) - # Hack: Avoid passing on a "./" prefix in the common case of - # 'base_dir' defaulting to ".", just to give less awkward - # results from e.g. get_def/ref_locations(). Maybe this could - # be handled in a nicer way. - if self._base_dir == ".": - filename = exp_kconfig_file else: - filename = os.path.join(self._base_dir, exp_kconfig_file) - - if not os.path.exists(filename): - raise IOError( - '{}:{}: sourced file "{}" (expands to "{}") not ' - "found. Perhaps base_dir (argument to " - 'Config.__init__(), currently "{}") is set to the ' - "the wrong value. Also note that e.g. $FOO in a " - "'source' statement does not refer to the " - "environment variable FOO, but rather to the Kconfig " - "symbol FOO (which would commonly have " - "'option env=\"FOO\"' in its definition)." - .format(line_feeder.filename, line_feeder.linenr, - kconfig_file, exp_kconfig_file, - self._base_dir)) - - # Add items to the same block - self._parse_file(filename, parent, deps, visible_if_deps, - block) + # It's a case of missing quotes. For example, the + # following is accepted: + # + # menu unquoted_title + # + # config A + # tristate unquoted_prompt + # + # endmenu + token = name - elif t0 == end_marker: - # We have reached the end of the block - return + else: + # Not an identifier/keyword - elif t0 == _T_IF: - # If statements are treated as syntactic sugar for adding - # dependencies to enclosed items and do not have an explicit - # object representation. - - dep_expr = self._parse_expr(tokens, None, line, - line_feeder.filename, - line_feeder.linenr, True) - # Add items to the same block - self._parse_block(line_feeder, _T_ENDIF, parent, - _make_and(dep_expr, deps), - visible_if_deps, block) + # Note: _id_keyword_match and _initial_token_match strip + # trailing whitespace, making it safe to assume s[i] is the + # start of a token here. We manually strip trailing whitespace + # below as well. + # + # An old version stripped whitespace in this spot instead, but + # that leads to some redundancy and would cause + # _id_keyword_match to be tried against just "\n" fairly often + # (because file.readlines() keeps newlines). - elif t0 == _T_COMMENT: - comment = Comment() - comment._config = self - comment._parent = parent - comment._filename = line_feeder.filename - comment._linenr = line_feeder.linenr - comment._text = tokens.get_next() + c = s[i] + i += 1 - self._parse_properties(line_feeder, comment, deps, - visible_if_deps) + if c in "\"'": + # String literal/constant symbol + if "\\" not in s: + # Fast path: If the string contains no backslashes, we + # can just find the matching quote. + end = s.find(c, i) + if end == -1: + _tokenization_error(s, filename, linenr) + token = s[i:end] + i = end + 1 + else: + # Slow path: This could probably be sped up, but it's a + # very unusual case anyway. + quote = c + val = "" + while 1: + if i >= len(s): + _tokenization_error(s, filename, linenr) + c = s[i] + if c == quote: + break + if c == "\\": + if i + 1 >= len(s): + _tokenization_error(s, filename, linenr) + val += s[i + 1] + i += 2 + else: + val += c + i += 1 + i += 1 + token = val - self._comments.append(comment) - block.append(comment) + elif c == "&": + # Invalid characters are ignored + if i >= len(s) or s[i] != "&": continue + token = _T_AND + i += 1 - elif t0 == _T_MENU: - menu = Menu() - menu._config = self - menu._parent = parent - menu._filename = line_feeder.filename - menu._linenr = line_feeder.linenr - menu._title = tokens.get_next() - - self._parse_properties(line_feeder, menu, deps, - visible_if_deps) - - # This needs to go before _parse_block() so that we get the - # proper menu ordering in the case of nested menus - self._menus.append(menu) - # Parse contents and put Items in menu._block - self._parse_block(line_feeder, _T_ENDMENU, menu, - menu._menu_dep, - _make_and(visible_if_deps, - menu._visible_if_expr), - menu._block) - - block.append(menu) + elif c == "|": + # Invalid characters are ignored + if i >= len(s) or s[i] != "|": continue + token = _T_OR + i += 1 - elif t0 == _T_CHOICE: - name = tokens.get_next() - if name is None: - choice = Choice() - self._choices.append(choice) - else: - # Named choice - choice = self._named_choices.get(name) - if choice is None: - choice = Choice() - choice._name = name - self._named_choices[name] = choice - self._choices.append(choice) + elif c == "!": + if i < len(s) and s[i] == "=": + token = _T_UNEQUAL + i += 1 + else: + token = _T_NOT - choice._config = self - choice._parent = parent + elif c == "=": + token = _T_EQUAL - choice._def_locations.append((line_feeder.filename, - line_feeder.linenr)) + elif c == "(": + token = _T_OPEN_PAREN - self._parse_properties(line_feeder, choice, deps, - visible_if_deps) + elif c == ")": + token = _T_CLOSE_PAREN - # Parse contents and put Items in choice._block - self._parse_block(line_feeder, _T_ENDCHOICE, choice, deps, - visible_if_deps, choice._block) + elif c == "#": break # Comment - choice._determine_actual_symbols() + # Very rare + elif c == "<": + if i < len(s) and s[i] == "=": + token = _T_LESS_EQUAL + i += 1 + else: + token = _T_LESS - # If no type is specified for the choice, its type is that of - # the first choice item with a specified type - if choice._type == UNKNOWN: - for item in choice._actual_symbols: - if item._type != UNKNOWN: - choice._type = item._type - break + # Very rare + elif c == ">": + if i < len(s) and s[i] == "=": + token = _T_GREATER_EQUAL + i += 1 + else: + token = _T_GREATER + + else: + # Invalid characters are ignored + continue + + # Skip trailing whitespace + while i < len(s) and s[i].isspace(): + i += 1 + + tokens.append(token) + + return _Feed(tokens) - # Each choice item of UNKNOWN type gets the type of the choice - for item in choice._actual_symbols: - if item._type == UNKNOWN: - item._type = choice._type + def _parse_block(self, line_feeder, end_marker, parent, visible_if_deps, + prev_line, prev_node): + """ + Parses a block, which is the contents of either a file or an if, menu, + or choice statement. + + line_feeder: + A _FileFeed instance feeding lines from a file. The Kconfig language + is line-based in practice. + + end_marker: + The token that ends the block, e.g. _T_ENDIF ("endif") for ifs. None + for files. + + parent: + The parent menu node, corresponding to e.g. a menu or Choice. Can + also be a Symbol, due to automatic submenu creation from + dependencies. + + visible_if_deps: + 'visible if' dependencies from enclosing menus. Propagated to Symbol + and Choice prompts. + + prev_line: + A "cached" (line, tokens) tuple from having parsed a line earlier + that we realized belonged to a different construct. + + prev_node: + The previous menu node. New nodes will be added after this one (by + modifying its 'next' pointer). - block.append(choice) + Through a trick, prev_node is also used to parse a list of children + (for a menu or Choice): After parsing the children, the 'next' + pointer is assigned to the 'list' pointer to "tilt up" the children + above the node. + + + Returns the final menu node in the block (or prev_node if the block is + empty). This allows for easy chaining. + """ + + while 1: + if prev_line is not None: + line, tokens = prev_line + else: + line = line_feeder.next() + if line is None: + if end_marker is not None: + raise KconfigSyntaxError("Unexpected end of file " + + line_feeder.filename) + + # We have reached the end of the file. Terminate the final + # node and return it. + prev_node.next = None + return prev_node + + tokens = self._tokenize(line, False, line_feeder.filename, + line_feeder.linenr) + if tokens is None: + continue + + t0 = tokens.next() + + # Cases are ordered roughly by frequency, which speeds things up a + # bit + + if t0 in (_T_CONFIG, _T_MENUCONFIG): + # The tokenizer will automatically allocate a new Symbol object + # for any new names it encounters, so we don't need to worry + # about that here. + sym = tokens.next() + + node = MenuNode() + node.config = self + node.item = sym + node.help = None + node.list = None + node.parent = parent + node.filename = line_feeder.filename + node.linenr = line_feeder.linenr + node.is_menuconfig = (t0 == _T_MENUCONFIG) + + prev_line = self._parse_properties(line_feeder, node, + visible_if_deps) + + sym.nodes.append(node) + self.defined_syms.append(sym) + + # Tricky Python semantics: This assign prev_node.next before + # prev_node + prev_node.next = prev_node = node + + elif t0 == _T_SOURCE: + kconfig_file = tokens.next() + exp_kconfig_file = self._expand_sym_refs(kconfig_file) + + try: + f = self._open(exp_kconfig_file) + except IOError as e: + # Extend the error message a bit in this case + raise IOError( + "{}:{}: {} Also note that e.g. $FOO in a 'source' " + "statement does not refer to the environment " + "variable FOO, but rather to the Kconfig Symbol FOO " + "(which would commonly have 'option env=\"FOO\"' in " + "its definition)." + .format(line_feeder.filename, line_feeder.linenr, + e.message)) + + prev_node = self._parse_block(_FileFeed(f, exp_kconfig_file), + None, parent, visible_if_deps, + None, prev_node) + prev_line = None + + elif t0 == end_marker: + # We have reached the end of the block. Terminate the final + # node and return it. + prev_node.next = None + return prev_node + + elif t0 == _T_IF: + node = MenuNode() + node.item = None + node.prompt = None + node.parent = parent + node.filename = line_feeder.filename + node.linenr = line_feeder.linenr + node.dep = \ + _make_and(parent.dep, + self._parse_expr(tokens, line, + line_feeder.filename, + line_feeder.linenr, True)) + + self._parse_block(line_feeder, _T_ENDIF, node, visible_if_deps, + None, node) + node.list = node.next + + prev_line = None + + prev_node.next = prev_node = node + + elif t0 == _T_MENU: + node = MenuNode() + node.config = self + node.item = MENU + node.visibility = None + node.parent = parent + node.filename = line_feeder.filename + node.linenr = line_feeder.linenr + + prev_line = self._parse_properties(line_feeder, node, + visible_if_deps) + node.prompt = (tokens.next(), node.dep) + + self._parse_block(line_feeder, _T_ENDMENU, node, + _make_and(visible_if_deps, node.visibility), + prev_line, node) + node.list = node.next + + prev_line = None + + prev_node.next = prev_node = node + + elif t0 == _T_COMMENT: + node = MenuNode() + node.config = self + node.item = COMMENT + node.list = None + node.parent = parent + node.filename = line_feeder.filename + node.linenr = line_feeder.linenr + + prev_line = self._parse_properties(line_feeder, node, + visible_if_deps) + node.prompt = (tokens.next(), node.dep) + + prev_node.next = prev_node = node + + elif t0 == _T_CHOICE: + name = tokens.next() + if name is None: + choice = Choice() + self._choices.append(choice) + else: + # Named choice + choice = self.named_choices.get(name) + if choice is None: + choice = Choice() + self._choices.append(choice) + choice.name = name + self.named_choices[name] = choice + + choice.config = self + + node = MenuNode() + node.config = self + node.item = choice + node.help = None + node.parent = parent + node.filename = line_feeder.filename + node.linenr = line_feeder.linenr + + prev_line = self._parse_properties(line_feeder, node, + visible_if_deps) + self._parse_block(line_feeder, _T_ENDCHOICE, node, + visible_if_deps, prev_line, node) + node.list = node.next + + prev_line = None + + choice.nodes.append(node) + + prev_node.next = prev_node = node elif t0 == _T_MAINMENU: - text = tokens.get_next() - if self._mainmenu_text is not None: - self._warn("overriding 'mainmenu' text. " - 'Old value: "{}", new value: "{}".' - .format(self._mainmenu_text, text), - line_feeder.filename, line_feeder.linenr) - self._mainmenu_text = text + self.top_menu.prompt = (tokens.next(), None) + self.top_menu.filename = line_feeder.filename + self.top_menu.linenr = line_feeder.linenr else: _parse_error(line, "unrecognized construct", line_feeder.filename, line_feeder.linenr) - def _parse_cond(self, tokens, stmt, line, filename, linenr): - """Parses an optional 'if ' construct and returns the parsed - , or None if the next token is not _T_IF.""" - return self._parse_expr(tokens, stmt, line, filename, linenr, True) \ + def _parse_cond(self, tokens, line, filename, linenr): + """ + Parses an optional 'if ' construct and returns the parsed , + or None if the next token is not _T_IF + """ + return self._parse_expr(tokens, line, filename, linenr, True) \ if tokens.check(_T_IF) else None - def _parse_val_and_cond(self, tokens, stmt, line, filename, linenr): - """Parses ' if ' constructs, where the 'if' part is - optional. Returns a tuple containing the parsed expressions, with - None as the second element if the 'if' part is missing.""" - return (self._parse_expr(tokens, stmt, line, filename, linenr, False), - self._parse_cond(tokens, stmt, line, filename, linenr)) - - def _parse_properties(self, line_feeder, stmt, deps, visible_if_deps): - """Parsing of properties for symbols, menus, choices, and comments. - Takes care of propagating dependencies from enclosing menus and ifs.""" - - # In case the symbol is defined in multiple locations, we need to - # remember what prompts, defaults, selects, implies, and ranges are new - # for this definition, as "depends on" should only apply to the local - # definition. - new_prompt = None - new_def_exprs = [] - new_selects = [] - new_implies = [] - new_ranges = [] - - # Dependencies from 'depends on' statements - depends_on_expr = None + def _parse_val_and_cond(self, tokens, line, filename, linenr): + """ + Parses ' if ' constructs, where the 'if' part is + optional. Returns a tuple containing the parsed expressions, with None + as the second element if the 'if' part is missing. + """ + return (self._parse_expr(tokens, line, filename, linenr, False), + self._parse_cond(tokens, line, filename, linenr)) + + def _parse_properties(self, line_feeder, node, visible_if_deps): + """ + Parses properties for symbols, menus, choices, and comments. Also takes + care of propagating dependencies from the menu node to the properties + of the item (this mirrors the inner working of the C tools). + + line_feeder: + A _FileFeed instance feeding lines from a file. The Kconfig language + is line-based in practice. + + node: + The menu node we're parsing properties on. Some properties (prompts, + help texts, 'depends on') apply to the Menu node, while the others + apply to the contained item. + + visible_if_deps: + 'visible if' dependencies from enclosing menus. Propagated to Symbol + and Choice prompts. + + Stops when finding a line that isn't part of the properties, and + returns a (line, tokens) tuple for it so it can be reused. + """ + + # New properties encountered at this location. A local 'depends on' + # only applies to these, in case a symbol is defined in multiple + # locations. + prompt = None + defaults = [] + selects = [] + implies = [] + ranges = [] + + # Menu node dependency from 'depends on'. Will get propagated to the + # properties above. + node.dep = None + + # The cached (line, tokens) tuple that we return + last_line = None while 1: - line = line_feeder.get_next() + line = line_feeder.next() if line is None: break @@ -889,138 +1015,147 @@ class Config(object): linenr = line_feeder.linenr tokens = self._tokenize(line, False, filename, linenr) - - t0 = tokens.get_next() - if t0 is None: + if tokens is None: continue - # Cases are ordered roughly by frequency, which speeds things up a - # bit + t0 = tokens.next() if t0 == _T_DEPENDS: if not tokens.check(_T_ON): _parse_error(line, 'expected "on" after "depends"', filename, linenr) - depends_on_expr = \ - _make_and(depends_on_expr, - self._parse_expr(tokens, stmt, line, filename, + node.dep = \ + _make_and(node.dep, + self._parse_expr(tokens, line, filename, linenr, True)) elif t0 == _T_HELP: # Find first non-blank (not all-space) line and get its # indentation - line = line_feeder.next_nonblank() + + while 1: + line = line_feeder.next_no_join() + if line is None or not line.isspace(): + break + if line is None: - stmt._help = "" + node.help = "" break + indent = _indentation(line) if indent == 0: # If the first non-empty lines has zero indent, there is no # help text - stmt._help = "" - line_feeder.unget() + node.help = "" + line_feeder.linenr -= 1 break # The help text goes on till the first non-empty line with less # indent - help_lines = [_deindent(line, indent)] + + help_lines = [_deindent(line, indent).rstrip()] while 1: - line = line_feeder.get_next() + line = line_feeder.next_no_join() if line is None or \ (not line.isspace() and _indentation(line) < indent): - stmt._help = "".join(help_lines) + node.help = "\n".join(help_lines).rstrip() + "\n" break - help_lines.append(_deindent(line, indent)) + help_lines.append(_deindent(line, indent).rstrip()) if line is None: break - line_feeder.unget() + line_feeder.linenr -= 1 elif t0 == _T_SELECT: - if not isinstance(stmt, Symbol): + if not isinstance(node.item, Symbol): _parse_error(line, "only symbols can select", filename, linenr) - new_selects.append( - (tokens.get_next(), - self._parse_cond(tokens, stmt, line, filename, linenr))) + # HACK: We always represent n/m/y using the constant symbol + # "n"/"m"/"y" forms, but that causes a crash if a Kconfig file + # does e.g. 'select n' (which is meaningless and probably stems + # from a misunderstanding). Seen in U-Boot. Just skip the + # select. + target = tokens.next() + if target not in ("n", "m", "y"): + selects.append( + (target, + self._parse_cond(tokens, line, filename, linenr))) elif t0 == _T_IMPLY: - if not isinstance(stmt, Symbol): + if not isinstance(node.item, Symbol): _parse_error(line, "only symbols can imply", filename, linenr) - new_implies.append( - (tokens.get_next(), - self._parse_cond(tokens, stmt, line, filename, linenr))) + # See above + target = tokens.next() + if target not in ("n", "m", "y"): + implies.append( + (target, + self._parse_cond(tokens, line, filename, linenr))) elif t0 in (_T_BOOL, _T_TRISTATE, _T_INT, _T_HEX, _T_STRING): - stmt._type = _TOKEN_TO_TYPE[t0] - if tokens.peek_next() is not None: - new_prompt = self._parse_val_and_cond(tokens, stmt, line, - filename, linenr) + node.item._type = _TOKEN_TO_TYPE[t0] + if tokens.peek() is not None: + prompt = self._parse_val_and_cond(tokens, line, + filename, linenr) elif t0 == _T_DEFAULT: - new_def_exprs.append( - self._parse_val_and_cond( - tokens, stmt, line, filename, linenr)) + defaults.append( + self._parse_val_and_cond(tokens, line, filename, linenr)) elif t0 in (_T_DEF_BOOL, _T_DEF_TRISTATE): - stmt._type = _TOKEN_TO_TYPE[t0] - if tokens.peek_next() is not None: - new_def_exprs.append( - self._parse_val_and_cond(tokens, stmt, line, filename, + node.item._type = _TOKEN_TO_TYPE[t0] + if tokens.peek() is not None: + defaults.append( + self._parse_val_and_cond(tokens, line, filename, linenr)) elif t0 == _T_PROMPT: # 'prompt' properties override each other within a single # definition of a symbol, but additional prompts can be added - # by defining the symbol multiple times; hence 'new_prompt' - # instead of 'prompt'. - new_prompt = self._parse_val_and_cond(tokens, stmt, line, - filename, linenr) + # by defining the symbol multiple times + prompt = self._parse_val_and_cond(tokens, line, filename, + linenr) elif t0 == _T_RANGE: - new_ranges.append( - (tokens.get_next(), - tokens.get_next(), - self._parse_cond(tokens, stmt, line, filename, linenr))) + ranges.append( + (tokens.next(), + tokens.next(), + self._parse_cond(tokens, line, filename, linenr))) elif t0 == _T_OPTION: if tokens.check(_T_ENV) and tokens.check(_T_EQUAL): - env_var = tokens.get_next() + env_var = tokens.next() - stmt._is_special = True - stmt._is_from_env = True + node.item.env_var = env_var if env_var not in os.environ: - self._warn("the symbol {} references the non-existent " - "environment variable {} and will get the " - "empty string as its value. If you're " - "using Kconfiglib via " - "'make (i)scriptconfig', it should have " - "set up the environment correctly for you. " - "If you still got this message, that " - "might be an error, and you should email " - "ulfalizer a.t Google's email service.""" - .format(stmt._name, env_var), + self._warn("the symbol {0} references the " + "non-existent environment variable {1} " + "(meaning the 'option env=\"{1}\"' will " + "have no effect). If you're using " + "Kconfiglib via 'make (i)scriptconfig', it " + "should have set up the environment " + "correctly for you. If you still got this " + "message, that might be an error, and you " + "should email ulfalizer a.t Google's email " + "service.".format(node.item.name, env_var), filename, linenr) - - stmt._cached_val = "" else: - stmt._cached_val = os.environ[env_var] + defaults.append((os.environ[env_var], None)) elif tokens.check(_T_DEFCONFIG_LIST): - if self._defconfig_sym is None: - self._defconfig_sym = stmt + if self.defconfig_list is None: + self.defconfig_list = node.item else: self._warn("'option defconfig_list' set on multiple " "symbols ({0} and {1}). Only {0} will be " "used." - .format(self._defconfig_sym._name, - stmt._name)) + .format(self.defconfig_list.name, + node.item.name)) elif tokens.check(_T_MODULES): # To reduce warning spam, only warn if 'option modules' is @@ -1028,7 +1163,7 @@ class Config(object): # safe. I haven't run into any projects that make use # modules besides the kernel yet, and there it's likely to # keep being called "MODULES". - if stmt._name != "MODULES": + if node.item is not self.modules: self._warn("the 'modules' option is not supported. " "Let me know if this is a problem for you; " "it shouldn't be that hard to implement. " @@ -1040,12 +1175,13 @@ class Config(object): filename, linenr) elif tokens.check(_T_ALLNOCONFIG_Y): - if not isinstance(stmt, Symbol): + if not isinstance(node.item, Symbol): _parse_error(line, "the 'allnoconfig_y' option is only " "valid for symbols", filename, linenr) - stmt._allnoconfig_y = True + + node.item.is_allnoconfig_y = True else: _parse_error(line, "unrecognized option", filename, linenr) @@ -1054,114 +1190,87 @@ class Config(object): if not tokens.check(_T_IF): _parse_error(line, 'expected "if" after "visible"', filename, linenr) - if not isinstance(stmt, Menu): - _parse_error(line, - "'visible if' is only valid for menus", - filename, linenr) - stmt._visible_if_expr = \ - _make_and(stmt._visible_if_expr, - self._parse_expr(tokens, stmt, line, filename, - linenr, True)) + node.visibility = \ + _make_and(node.visibility, + self._parse_expr(tokens, line, filename, linenr, + True)) elif t0 == _T_OPTIONAL: - if not isinstance(stmt, Choice): + if not isinstance(node.item, Choice): _parse_error(line, '"optional" is only valid for choices', filename, linenr) - stmt._optional = True + + node.item.is_optional = True else: - # See the _end_line description in Config.__init__() - self._end_line = line - tokens.unget_all() - self._end_line_tokens = tokens + tokens.i = 0 + last_line = (line, tokens) break # Done parsing properties. Now add the new - # prompts/defaults/selects/implies/ranges, with dependencies - # propagated. - - # Save original dependencies from enclosing menus and ifs - stmt._deps_from_containing = deps - - # The parent deps + the 'depends on' deps. This is also used to - # implicitly create menus when a symbol depends on the previous symbol, - # hence the name. In the C implementation, it's the dependency of a - # menu "node". - stmt._menu_dep = _make_and(deps, depends_on_expr) - - if isinstance(stmt, (Menu, Comment)): - # For display purposes - stmt._orig_deps = depends_on_expr - else: - # Symbol or Choice - - if isinstance(stmt, Symbol): - stmt._direct_deps = _make_or(stmt._direct_deps, stmt._menu_dep) - - # Propagate dependencies to prompts - if new_prompt is not None: - prompt, cond_expr = new_prompt - - # Propagate 'visible if' and 'depends on' - cond_expr = _make_and(_make_and(cond_expr, visible_if_deps), - depends_on_expr) - - # Version without parent dependencies, for display - stmt._orig_prompts.append((prompt, cond_expr)) - - # This is what we actually use for evaluation - stmt._prompts.append((prompt, _make_and(cond_expr, deps))) - - # Propagate dependencies to defaults - for val_expr, cond_expr in new_def_exprs: - # Version without parent dependencies, for display - stmt._orig_def_exprs.append( - (val_expr, _make_and(cond_expr, depends_on_expr))) - - # This is what we actually use for evaluation - stmt._def_exprs.append( - (val_expr, _make_and(cond_expr, stmt._menu_dep))) + # prompts/defaults/selects/implies/ranges properties, with dependencies + # from node.dep propagated. + + # First propagate parent dependencies to node.dep + node.dep = _make_and(node.dep, node.parent.dep) + + if isinstance(node.item, (Symbol, Choice)): + if isinstance(node.item, Symbol): + node.item.direct_deps = \ + _make_or(node.item.direct_deps, node.dep) + + # Set the prompt, with dependencies propagated + if prompt is not None: + node.prompt = (prompt[0], + _make_and(_make_and(prompt[1], node.dep), + visible_if_deps)) + else: + node.prompt = None - # Propagate dependencies to ranges - for low, high, cond_expr in new_ranges: - # Version without parent dependencies, for display - stmt._orig_ranges.append( - (low, high, _make_and(cond_expr, depends_on_expr))) + # Add the new defaults, with dependencies propagated + for val_expr, cond_expr in defaults: + node.item.defaults.append( + (val_expr, _make_and(cond_expr, node.dep))) - # This is what we actually use for evaluation - stmt._ranges.append( - (low, high, _make_and(cond_expr, stmt._menu_dep))) + # Add the new ranges, with dependencies propagated + for low, high, cond_expr in ranges: + node.item.ranges.append( + (low, high, _make_and(cond_expr, node.dep))) # Handle selects - for target, cond_expr in new_selects: - # Used for display - stmt._orig_selects.append( - (target, _make_and(cond_expr, depends_on_expr))) + for target, cond_expr in selects: + # Only stored for convenience. Not used during evaluation. + node.item.selects.append( + (target, _make_and(cond_expr, node.dep))) # Modify the dependencies of the selected symbol - target._rev_dep = \ - _make_or(target._rev_dep, - _make_and(stmt, _make_and(cond_expr, - stmt._menu_dep))) + target.rev_dep = \ + _make_or(target.rev_dep, + _make_and(node.item, + _make_and(cond_expr, node.dep))) # Handle implies - for target, cond_expr in new_implies: - # Used for display - stmt._orig_implies.append( - (target, _make_and(cond_expr, depends_on_expr))) + for target, cond_expr in implies: + # Only stored for convenience. Not used during evaluation. + node.item.implies.append( + (target, _make_and(cond_expr, node.dep))) # Modify the dependencies of the implied symbol - target._weak_rev_dep = \ - _make_or(target._weak_rev_dep, - _make_and(stmt, _make_and(cond_expr, - stmt._menu_dep))) - - def _parse_expr(self, feed, cur_item, line, filename, linenr, transform_m): - """Parses an expression from the tokens in 'feed' using a simple - top-down approach. The result has the form + target.weak_rev_dep = \ + _make_or(target.weak_rev_dep, + _make_and(node.item, + _make_and(cond_expr, node.dep))) + + # Return cached non-property line + return last_line + + def _parse_expr(self, feed, line, filename, linenr, transform_m): + """ + Parses an expression from the tokens in 'feed' using a simple top-down + approach. The result has the form '( )' where is e.g. kconfiglib._AND. If there is only one operand (i.e., no && or ||), then the operand is returned directly. This also goes for subexpressions. @@ -1170,22 +1279,24 @@ class Config(object): structure (_AND, A, (_AND, B, (_OR, (_NOT, C), (_EQUAL, D, 3)))), with the Symbol objects stored directly in the expression. - feed: _Feed instance containing the tokens for the expression. - - cur_item: The item (Symbol, Choice, Menu, or Comment) currently being - parsed, or None if we're not parsing an item. Used for recording - references to symbols. + feed: + _Feed instance containing the tokens for the expression. - line: The line containing the expression being parsed. + line: + The line containing the expression being parsed. - filename: The file containing the expression. None when using - Config.eval(). + filename: + The file containing the expression. None when using + Config.eval_string(). - linenr: The line number containing the expression. None when using - Config.eval(). + linenr: + The line number containing the expression. None when using + Config.eval_string(). - transform_m (default: False): Determines if 'm' should be rewritten to - 'm && MODULES'. See the Config.eval() docstring.""" + transform_m: + True if 'm' should be rewritten to 'm && MODULES'. See + the Config.eval_string() documentation. + """ # Grammar: # @@ -1211,8 +1322,8 @@ class Config(object): # we end up allocating a ton of lists instead of reusing expressions, # which is bad. - and_expr = self._parse_and_expr(feed, cur_item, line, filename, - linenr, transform_m) + and_expr = self._parse_and_expr(feed, line, filename, linenr, + transform_m) # Return 'and_expr' directly if we have a "single-operand" OR. # Otherwise, parse the expression on the right and make an _OR node. @@ -1220,54 +1331,47 @@ class Config(object): # (_OR, A, (_OR, B, (_OR, C, D))). return and_expr \ if not feed.check(_T_OR) else \ - (_OR, and_expr, self._parse_expr(feed, cur_item, line, filename, - linenr, transform_m)) - - def _parse_and_expr(self, feed, cur_item, line, filename, linenr, - transform_m): + (_OR, and_expr, self._parse_expr(feed, line, filename, linenr, + transform_m)) - factor = self._parse_factor(feed, cur_item, line, filename, linenr, - transform_m) + def _parse_and_expr(self, feed, line, filename, linenr, transform_m): + factor = self._parse_factor(feed, line, filename, linenr, transform_m) # Return 'factor' directly if we have a "single-operand" AND. # Otherwise, parse the right operand and make an _AND node. This turns # A && B && C && D into (_AND, A, (_AND, B, (_AND, C, D))). return factor \ if not feed.check(_T_AND) else \ - (_AND, factor, self._parse_and_expr(feed, cur_item, line, - filename, linenr, - transform_m)) + (_AND, factor, self._parse_and_expr(feed, line, filename, + linenr, transform_m)) - def _parse_factor(self, feed, cur_item, line, filename, linenr, - transform_m): - token = feed.get_next() + def _parse_factor(self, feed, line, filename, linenr, transform_m): + token = feed.next() if isinstance(token, (Symbol, str)): # Plain symbol or relation - next_token = feed.peek_next() - if next_token not in _TOKEN_TO_RELATION: + next_token = feed.peek() + if next_token not in _TOKEN_TO_REL: # Plain symbol # For conditional expressions ('depends on ', # '... if ', etc.), "m" and m are rewritten to # "m" && MODULES. - if transform_m and (token is self._m or token == "m"): - return (_AND, "m", self._lookup_sym("MODULES")) + if transform_m and token == "m": + return (_AND, "m", self.modules) return token # Relation - return (_TOKEN_TO_RELATION[feed.get_next()], - token, - feed.get_next()) + return (_TOKEN_TO_REL[feed.next()], token, feed.next()) if token == _T_NOT: - return (_NOT, self._parse_factor(feed, cur_item, line, filename, - linenr, transform_m)) + return (_NOT, self._parse_factor(feed, line, filename, linenr, + transform_m)) if token == _T_OPEN_PAREN: - expr_parse = self._parse_expr(feed, cur_item, line, filename, + expr_parse = self._parse_expr(feed, line, filename, linenr, transform_m) if not feed.check(_T_CLOSE_PAREN): _parse_error(line, "missing end parenthesis", filename, linenr) @@ -1275,338 +1379,104 @@ class Config(object): _parse_error(line, "malformed expression", filename, linenr) - def _tokenize(self, s, for_eval, filename=None, linenr=None): - """Returns a _Feed instance containing tokens derived from the string - 's'. Registers any new symbols encountered (via _lookup_sym()). - - Tries to be reasonably speedy by processing chunks of text via regexes - and string operations where possible. This is a hotspot during parsing. - - for_eval: True when parsing an expression for a call to Config.eval(), - in which case we should not treat the first token specially nor - register new symbols.""" - - # Tricky implementation detail: While parsing a token, 'token' refers - # to the previous token. See _NOT_REF for why this is needed. - - if for_eval: - token = None - tokens = [] - - # The current index in the string being tokenized - i = 0 - - else: - # See comment at _initial_token_re_match definition - initial_token_match = _initial_token_re_match(s) - if not initial_token_match: - return _Feed(()) - - keyword = _get_keyword(initial_token_match.group(1)) - if keyword == _T_HELP: - # Avoid junk after "help", e.g. "---", being registered as a - # symbol - return _Feed((_T_HELP,)) - if keyword is None: - # We expect a keyword as the first token - _tokenization_error(s, filename, linenr) - - token = keyword - tokens = [keyword] - # The current index in the string being tokenized - i = initial_token_match.end() - - # Main tokenization loop (for tokens past the first one) - while i < len(s): - # Test for an identifier/keyword first. This is the most common - # case. - id_keyword_match = _id_keyword_re_match(s, i) - if id_keyword_match: - # We have an identifier or keyword - - # Jump past it - i = id_keyword_match.end() - - # Check what it is. lookup_sym() will take care of allocating - # new symbols for us the first time we see them. Note that - # 'token' still refers to the previous token. - - name = id_keyword_match.group(1) - keyword = _get_keyword(name) - if keyword is not None: - # It's a keyword - token = keyword - - elif token not in _NOT_REF: - # It's a symbol reference - token = self._lookup_sym(name, for_eval) - token._ref_locations.append((filename, linenr)) - - elif token == _T_CONFIG: - # It's a symbol definition - token = self._lookup_sym(name, for_eval) - token._def_locations.append((filename, linenr)) - - else: - # It's a case of missing quotes. For example, the - # following is accepted: - # - # menu unquoted_title - # - # config A - # tristate unquoted_prompt - # - # endmenu - token = name - - else: - # Not an identifier/keyword - - # Note: _id_keyword_match and _initial_token_match strip - # trailing whitespace, making it safe to assume s[i] is the - # start of a token here. We manually strip trailing whitespace - # below as well. - # - # An old version stripped whitespace in this spot instead, but - # that leads to some redundancy and would cause - # _id_keyword_match to be tried against just "\n" fairly often - # (because file.readlines() keeps newlines). - - c = s[i] - i += 1 - - if c in "\"'": - # String literal/constant symbol - if "\\" not in s: - # Fast path: If the string contains no backslashes, we - # can just find the matching quote. - end = s.find(c, i) - if end == -1: - _tokenization_error(s, filename, linenr) - token = s[i:end] - i = end + 1 - else: - # Slow path: This could probably be sped up, but it's a - # very unusual case anyway. - quote = c - val = "" - while 1: - if i >= len(s): - _tokenization_error(s, filename, linenr) - c = s[i] - if c == quote: - break - if c == "\\": - if i + 1 >= len(s): - _tokenization_error(s, filename, linenr) - val += s[i + 1] - i += 2 - else: - val += c - i += 1 - i += 1 - token = val - - elif c == "&": - # Invalid characters are ignored - if i >= len(s) or s[i] != "&": continue - token = _T_AND - i += 1 - - elif c == "|": - # Invalid characters are ignored - if i >= len(s) or s[i] != "|": continue - token = _T_OR - i += 1 - - elif c == "!": - if i < len(s) and s[i] == "=": - token = _T_UNEQUAL - i += 1 - else: - token = _T_NOT - - elif c == "=": - token = _T_EQUAL - - elif c == "(": - token = _T_OPEN_PAREN - - elif c == ")": - token = _T_CLOSE_PAREN - - elif c == "#": break # Comment - - # Very rare - elif c == "<": - if i < len(s) and s[i] == "=": - token = _T_LESS_EQUAL - i += 1 - else: - token = _T_LESS - - # Very rare - elif c == ">": - if i < len(s) and s[i] == "=": - token = _T_GREATER_EQUAL - i += 1 - else: - token = _T_GREATER - - else: - # Invalid characters are ignored - continue - - # Skip trailing whitespace - while i < len(s) and s[i].isspace(): - i += 1 - - tokens.append(token) - - return _Feed(tokens) + # + # Symbol lookup + # def _lookup_sym(self, name, for_eval=False): - """Fetches the symbol 'name' from the symbol table, creating and + """ + Fetches the symbol 'name' from the symbol table, creating and registering it if it does not exist. If 'for_eval' is True, the symbol - won't be added to the symbol table if it does not exist -- this is for - Config.eval().""" - if name in self._syms: - return self._syms[name] - - new_sym = Symbol() - new_sym._config = self - new_sym._name = name + won't be added to the symbol table if it does not exist. This is for + Config.eval_string(). + """ + if name in self.syms: + return self.syms[name] + + sym = Symbol() + sym.config = self + sym.name = name if for_eval: self._warn("no symbol {} in configuration".format(name)) else: - self._syms[name] = new_sym - return new_sym + self.syms[name] = sym + return sym # - # Expression evaluation + # .config generation # - def _eval_expr(self, expr): - """Evaluates an expression to "n", "m", or "y".""" - - # Handles e.g. an "x if y" condition where the "if y" part is missing. - if expr is None: - return "y" - - res = self._eval_expr_rec(expr) - if res == "m": - # Promote "m" to "y" if we're running without modules. - # - # Internally, "m" is often rewritten to "m" && MODULES by both the - # C implementation and Kconfiglib, which takes care of cases where - # "m" should be demoted to "n" instead. - modules_sym = self._syms.get("MODULES") - if modules_sym is None or modules_sym.get_value() != "y": - return "y" - return res + def _get_config_strings(self): + """ + Returns a list containing all .config strings for the configuration. + """ - def _eval_expr_rec(self, expr): - if isinstance(expr, Symbol): - # Non-bool/tristate symbols are always "n" in a tristate sense, - # regardless of their value - return expr.get_value() if expr._type in (BOOL, TRISTATE) else "n" + config_strings = [] + add_fn = config_strings.append - if isinstance(expr, str): - return expr if expr in ("m", "y") else "n" + node = self.top_menu.list + if node is None: + # Empty configuration + return config_strings + + # Symbol._already_written is set to True when a symbol config string is + # fetched, so that symbols defined in multiple locations only get one + # .config entry. We reset it prior to writing out a new .config. It + # only needs to be reset for defined symbols, because undefined symbols + # will never be written out (because they do not appear structure + # rooted at Config.top_menu). + # + # The C tools reuse _write_to_conf for this, but we cache + # _write_to_conf together with the value and don't invalidate cached + # values when writing .config files, so that won't work. + for sym in self.defined_syms: + sym._already_written = False - if expr[0] == _AND: - ev1 = self._eval_expr_rec(expr[1]) - if ev1 == "n": - return "n" - ev2 = self._eval_expr_rec(expr[2]) - return ev2 if ev1 == "y" else \ - "m" if ev2 != "n" else \ - "n" - - if expr[0] == _OR: - ev1 = self._eval_expr_rec(expr[1]) - if ev1 == "y": - return "y" - ev2 = self._eval_expr_rec(expr[2]) - return ev2 if ev1 == "n" else \ - "y" if ev2 == "y" else \ - "m" - - if expr[0] == _NOT: - ev = self._eval_expr_rec(expr[1]) - return "n" if ev == "y" else \ - "y" if ev == "n" else \ - "m" - - if expr[0] in _RELATIONS: - # Implements <, <=, >, >= comparisons as well. These were added to - # kconfig in 31847b67 (kconfig: allow use of relations other than - # (in)equality). - - # This mirrors the C implementation pretty closely. Perhaps there's - # a more pythonic way to structure this. - - oper, op1, op2 = expr - op1_type, op1_str = _type_and_val(op1) - op2_type, op2_str = _type_and_val(op2) - - # If both operands are strings... - if op1_type == STRING and op2_type == STRING: - # ...then compare them lexicographically - comp = _strcmp(op1_str, op2_str) + while 1: + if isinstance(node.item, Symbol): + sym = node.item + if not sym._already_written: + config_string = sym.config_string + if config_string is not None: + add_fn(config_string) + sym._already_written = True + + elif (node.item == MENU and eval_expr(node.dep) != "n" and + eval_expr(node.visibility) != "n") or \ + (node.item == COMMENT and eval_expr(node.dep) != "n"): + add_fn("\n#\n# {}\n#\n".format(node.prompt[0])) + + # Iterative tree walk using parent pointers + + if node.list is not None: + node = node.list + elif node.next is not None: + node = node.next else: - # Otherwise, try to compare them as numbers - try: - comp = int(op1_str, _TYPE_TO_BASE[op1_type]) - \ - int(op2_str, _TYPE_TO_BASE[op2_type]) - except ValueError: - # They're not both valid numbers. If the comparison is - # anything but = or !=, return 'n'. Otherwise, reuse - # _strcmp() to check for (in)equality. - if oper not in (_EQUAL, _UNEQUAL): - return "n" - comp = _strcmp(op1_str, op2_str) - - if oper == _EQUAL: res = comp == 0 - elif oper == _UNEQUAL: res = comp != 0 - elif oper == _LESS: res = comp < 0 - elif oper == _LESS_EQUAL: res = comp <= 0 - elif oper == _GREATER: res = comp > 0 - elif oper == _GREATER_EQUAL: res = comp >= 0 - - return "y" if res else "n" - - _internal_error("Internal error while evaluating expression: " - "unknown operation {}.".format(expr[0])) - - def _eval_min(self, e1, e2): - """Returns the minimum value of the two expressions. Equates None with - 'y'.""" - e1_eval = self._eval_expr(e1) - e2_eval = self._eval_expr(e2) - return e1_eval if tri_less(e1_eval, e2_eval) else e2_eval - - def _eval_max(self, e1, e2): - """Returns the maximum value of the two expressions. Equates None with - 'y'.""" - e1_eval = self._eval_expr(e1) - e2_eval = self._eval_expr(e2) - return e1_eval if tri_greater(e1_eval, e2_eval) else e2_eval + while node.parent is not None: + node = node.parent + if node.next is not None: + node = node.next + break + else: + return config_strings # # Dependency tracking (for caching and invalidation) # def _build_dep(self): - """Populates the Symbol._direct_dependents sets, linking the symbol to - the symbols that immediately depend on it in the sense that changing - the value of the symbol might affect the values of those other symbols. + """ + Populates the Symbol._direct_dependents sets, linking the symbol to the + symbols that immediately depend on it in the sense that changing the + value of the symbol might affect the values of those other symbols. This is used for caching/invalidation purposes. The calculated sets might be larger than necessary as we don't do any complicated analysis - of the expressions.""" + of the expressions. + """ # Adds 'sym' as a directly dependent symbol to all symbols that appear - # in the expression 'e' + # in the expression 'expr' def add_expr_deps(expr, sym): res = [] _expr_syms(expr, res) @@ -1615,11 +1485,11 @@ class Config(object): # The directly dependent symbols of a symbol S are: # - # - Any symbols whose prompts, default values, _rev_dep (select - # condition), _weak_rev_dep (imply condition) or ranges depend on S + # - Any symbols whose prompts, default values, rev_dep (select + # condition), weak_rev_dep (imply condition), or ranges depend on S # # - Any symbol that has S as a direct dependency (has S in - # _direct_deps). This is needed to get invalidation right for + # direct_deps). This is needed to get invalidation right for # 'imply'. # # - Any symbols that belong to the same choice statement as S @@ -1634,307 +1504,62 @@ class Config(object): # change their value (they always evaluate to their name), so it's not # a true dependency. - for sym in self._defined_syms: - for _, e in sym._prompts: - add_expr_deps(e, sym) + for sym in self.defined_syms: + for node in sym.nodes: + if node.prompt is not None: + add_expr_deps(node.prompt[1], sym) - for v, e in sym._def_exprs: - add_expr_deps(v, sym) - add_expr_deps(e, sym) + for value, cond in sym.defaults: + add_expr_deps(value, sym) + add_expr_deps(cond, sym) - add_expr_deps(sym._rev_dep, sym) - add_expr_deps(sym._weak_rev_dep, sym) + add_expr_deps(sym.rev_dep, sym) + add_expr_deps(sym.weak_rev_dep, sym) - for l, u, e in sym._ranges: + for l, u, e in sym.ranges: add_expr_deps(l, sym) add_expr_deps(u, sym) add_expr_deps(e, sym) - add_expr_deps(sym._direct_deps, sym) + add_expr_deps(sym.direct_deps, sym) - if sym._is_choice_sym: - choice = sym._parent - for _, e in choice._prompts: + if sym.choice is not None: + for node in sym.choice.nodes: + if node.prompt is not None: + add_expr_deps(node.prompt[1], sym) + for _, e in sym.choice.defaults: add_expr_deps(e, sym) - for _, e in choice._def_exprs: - add_expr_deps(e, sym) - - def _eq_to_sym(self, eq): - """_expr_depends_on() helper. For (in)equalities of the form sym = y/m - or sym != n, returns sym. For other (in)equalities, returns None.""" - relation, left, right = eq - - def transform_y_m_n(item): - if item is self._y: return "y" - if item is self._m: return "m" - if item is self._n: return "n" - return item - - left = transform_y_m_n(left) - right = transform_y_m_n(right) - - # Make sure the symbol (if any) appears to the left - if not isinstance(left, Symbol): - left, right = right, left - if not isinstance(left, Symbol): - return None - if (relation == _EQUAL and right in ("m", "y")) or \ - (relation == _UNEQUAL and right == "n"): - return left - return None - - def _expr_depends_on(self, expr, sym): - """Reimplementation of expr_depends_symbol() from mconf.c. Used to - determine if a submenu should be implicitly created, which influences - what items inside choice statements are considered choice items.""" - if expr is None: - return False - - def rec(expr): - if isinstance(expr, str): - return False - if isinstance(expr, Symbol): - return expr is sym - - if expr[0] in (_EQUAL, _UNEQUAL): - return self._eq_to_sym(expr) is sym - if expr[0] == _AND: - return rec(expr[1]) or rec(expr[2]) - return False - - return rec(expr) def _invalidate_all(self): # Undefined symbols never change value and don't need to be # invalidated, so we can just iterate over defined symbols - for sym in self._defined_syms: + for sym in self.defined_syms: sym._invalidate() + for choice in self._choices: + choice._invalidate() + # # Printing and misc. # def _expand_sym_refs(self, s): - """Expands $-references to symbols in 's' to symbol values, or to the - empty string for undefined symbols.""" + """ + Expands $-references to symbols in 's' to symbol values, or to the + empty string for undefined symbols. + """ while 1: sym_ref_match = _sym_ref_re_search(s) if sym_ref_match is None: return s - sym = self._syms.get(sym_ref_match.group(1)) + sym = self.syms.get(sym_ref_match.group(1)) s = s[:sym_ref_match.start()] + \ - (sym.get_value() if sym is not None else "") + \ + (sym.value if sym is not None else "") + \ s[sym_ref_match.end():] - def _expr_val_str(self, expr, no_value_str="(none)", - get_val_instead_of_eval=False): - """Printing helper. Returns a string with 'expr' and its value. - - no_value_str: String to return when 'expr' is missing (None). - - get_val_instead_of_eval: Assume 'expr' is a symbol or string (constant - symbol) and get its value directly instead of evaluating it to a - tristate value.""" - - if expr is None: - return no_value_str - - if get_val_instead_of_eval: - if isinstance(expr, str): - return _expr_to_str(expr) - val = expr.get_value() - else: - val = self._eval_expr(expr) - - return "{} (value: {})".format(_expr_to_str(expr), _expr_to_str(val)) - - def _get_sym_or_choice_str(self, sc): - """Symbols and choices have many properties in common, so we factor out - common __str__() stuff here. "sc" is short for "symbol or choice".""" - - # As we deal a lot with string representations here, use some - # convenient shorthand: - s = _expr_to_str - - # - # Common symbol/choice properties - # - - user_val_str = "(no user value)" if sc._user_val is None else \ - s(sc._user_val) - - # Build prompts string - if not sc._prompts: - prompts_str = " (no prompts)" - else: - prompts_str_rows = [] - for prompt, cond_expr in sc._orig_prompts: - prompts_str_rows.append( - ' "{}"'.format(prompt) - if cond_expr is None else - ' "{}" if {}'.format(prompt, - self._expr_val_str(cond_expr))) - prompts_str = "\n".join(prompts_str_rows) - - # Build locations string - locations_str = "(no locations)" \ - if not sc._def_locations else \ - " ".join(["{}:{}".format(filename, linenr) - for filename, linenr in sc._def_locations]) - - # Build additional-dependencies-from-menus-and-ifs string - additional_deps_str = " " + \ - self._expr_val_str(sc._deps_from_containing, - "(no additional dependencies)") - - # - # Symbol-specific stuff - # - - if isinstance(sc, Symbol): - # Build ranges string - if isinstance(sc, Symbol): - if not sc._orig_ranges: - ranges_str = " (no ranges)" - else: - ranges_str_rows = [] - for l, u, cond_expr in sc._orig_ranges: - ranges_str_rows.append( - " [{}, {}]".format(s(l), s(u)) - if cond_expr is None else - " [{}, {}] if {}" - .format(s(l), s(u), self._expr_val_str(cond_expr))) - ranges_str = "\n".join(ranges_str_rows) - - # Build default values string - if not sc._orig_def_exprs: - defaults_str = " (no default values)" - else: - defaults_str_rows = [] - for val_expr, cond_expr in sc._orig_def_exprs: - row_str = " " + self._expr_val_str(val_expr, "(none)", - sc._type == STRING) - defaults_str_rows.append(row_str) - defaults_str_rows.append(" Condition: " + - self._expr_val_str(cond_expr)) - defaults_str = "\n".join(defaults_str_rows) - - # Build selects string - if not sc._orig_selects: - selects_str = " (no selects)" - else: - selects_str_rows = [] - for target, cond_expr in sc._orig_selects: - selects_str_rows.append( - " " + target._name - if cond_expr is None else - " {} if {}".format(target._name, - self._expr_val_str(cond_expr))) - selects_str = "\n".join(selects_str_rows) - - # Build implies string - if not sc._orig_implies: - implies_str = " (no implies)" - else: - implies_str_rows = [] - for target, cond_expr in sc._orig_implies: - implies_str_rows.append( - " " + target._name - if cond_expr is None else - " {} if {}".format(target._name, - self._expr_val_str(cond_expr))) - implies_str = "\n".join(implies_str_rows) - - res = _lines("Symbol " + - ("(no name)" if sc._name is None else sc._name), - "Type : " + _TYPENAME[sc._type], - "Value : " + s(sc.get_value()), - "User value : " + user_val_str, - "Visibility : " + s(_get_visibility(sc)), - "Is choice item : " + str(sc._is_choice_sym), - "Is defined : " + str(sc._is_defined), - "Is from env. : " + str(sc._is_from_env), - "Is special : " + str(sc._is_special), - "") - if sc._ranges: - res += _lines("Ranges:", ranges_str + "\n") - res += _lines("Prompts:", - prompts_str, - "Default values:", - defaults_str, - "Selects:", - selects_str, - "Implies:", - implies_str, - "Reverse (select-related) dependencies:", - " (no reverse dependencies)" - if sc._rev_dep == "n" - else " " + self._expr_val_str(sc._rev_dep), - "Weak reverse (imply-related) dependencies:", - " (no weak reverse dependencies)" - if sc._weak_rev_dep == "n" - else " " + self._expr_val_str(sc._weak_rev_dep), - "Additional dependencies from enclosing menus " - "and ifs:", - additional_deps_str, - "Locations: " + locations_str) - - return res - - # - # Choice-specific stuff - # - - # Build selected symbol string - sel = sc.get_selection() - sel_str = "(no selection)" if sel is None else sel._name - - # Build default values string - if not sc._def_exprs: - defaults_str = " (no default values)" - else: - defaults_str_rows = [] - for sym, cond_expr in sc._orig_def_exprs: - defaults_str_rows.append( - " " + sym._name - if cond_expr is None else - " {} if {}".format(sym._name, - self._expr_val_str(cond_expr))) - defaults_str = "\n".join(defaults_str_rows) - - # Build contained symbols string - names = [sym._name for sym in sc._actual_symbols] - syms_string = " ".join(names) if names else "(empty)" - - return _lines("Choice", - "Name (for named choices): " + - ("(no name)" if sc._name is None else sc._name), - "Type : " + _TYPENAME[sc._type], - "Selected symbol : " + sel_str, - "User value : " + user_val_str, - "Mode : " + s(sc.get_mode()), - "Visibility : " + s(_get_visibility(sc)), - "Optional : " + str(sc._optional), - "Prompts:", - prompts_str, - "Defaults:", - defaults_str, - "Choice symbols:", - " " + syms_string, - "Additional dependencies from enclosing menus and ifs:", - additional_deps_str, - "Locations: " + locations_str) - - def _is_header_line(self, line): - """Returns True is the line could be part of the initial header in a - .config file (which is really just another comment, but can be handy - for storing metadata).""" - return line is not None and line.startswith("#") and \ - not self._unset_re.match(line) - # # Warnings # @@ -1945,136 +1570,228 @@ class Config(object): _stderr_msg("warning: " + msg, filename, linenr) def _warn_undef_assign(self, msg, filename=None, linenr=None): - """For printing warnings for assignments to undefined variables. We - treat this is a separate category of warnings to avoid spamming lots of - warnings.""" + """ + See the class documentation. + """ if self._print_undef_assign: _stderr_msg("warning: " + msg, filename, linenr) def _warn_undef_assign_load(self, name, val, filename, linenr): - """Special version for load_config().""" + """ + Special version for load_config(). + """ self._warn_undef_assign( 'attempt to assign the value "{}" to the undefined symbol {}' \ .format(val, name), filename, linenr) -class Item(object): - - """Base class for symbols and other Kconfig constructs. Subclasses are - Symbol, Choice, Menu, and Comment.""" - - def is_symbol(self): - """Returns True if the item is a symbol. Short for - isinstance(item, kconfiglib.Symbol).""" - return isinstance(self, Symbol) - - def is_choice(self): - """Returns True if the item is a choice. Short for - isinstance(item, kconfiglib.Choice).""" - return isinstance(self, Choice) - - def is_menu(self): - """Returns True if the item is a menu. Short for - isinstance(item, kconfiglib.Menu).""" - return isinstance(self, Menu) - - def is_comment(self): - """Returns True if the item is a comment. Short for - isinstance(item, kconfiglib.Comment).""" - return isinstance(self, Comment) - -class Symbol(Item): - - """Represents a configuration symbol - e.g. FOO for - - config FOO - ...""" +class Symbol(object): + """ + Represents a configuration symbol: + + (menu)config FOO + ... + + The following attributes are available on Symbol instances. They should be + viewed as read-only, and some are implemented through @property magic (but + are still efficient to access due to internal caching). + + (Note: Prompts and help texts are stored in the Symbol's MenuNode(s) rather + than the Symbol itself. This matches the C tools.) + + name: + The name of the symbol, e.g. "FOO" for 'config FOO'. + + type: + The type of the symbol. One of BOOL, TRISTATE, STRING, INT, HEX, UNKNOWN. + UNKNOWN is for undefined symbols and symbols defined without a type. + + When running without modules (CONFIG_MODULES=n), TRISTATE symbols + magically change type to BOOL. This also happens for symbols within + choices in "y" mode. This matches the C tools, and makes sense for + menuconfig-like functionality. (Check the implementation of the property + if you need to get the original type.) + + value: + The current value of the symbol. Automatically recalculated as + dependencies change. + + assignable: + A string containing the tristate values that can be assigned to the + symbol, ordered from lowest (n) to highest (y). This corresponds to the + selections available in the 'menuconfig' interface. The assignable + values are calculated from the Symbol's visibility and selects/implies. + + Returns the empty string for non-BOOL/TRISTATE and symbols with + visibility "n". The other possible values are "ny", "nmy", "my", "m", + and "y". A "m" or "y" result means the symbol is visible but "locked" to + that particular value (through a select, perhaps in combination with a + prompt dependency). menuconfig seems to represent this as -M- and -*-, + respectively. + + Some handy 'assignable' idioms: + + # Is the symbol assignable (visible)? + if sym.assignable: + # What's the highest value it can be assigned? [-1] in Python + # gives the last element. + sym_high = sym.assignable[-1] + + # The lowest? + sym_low = sym.assignable[0] + + # Can the symbol be assigned the value "m"? + if "m" in sym.assignable: + ... + + visibility: + The visibility of the symbol's prompt(s): one of "n", "m", or "y". This + acts as an upper bound on the values the user can set for the symbol (via + Symbol.set_value() or a .config file). User values higher than the + visibility are truncated down to the visibility. + + If the visibility is "n", the user value is ignored, and the symbol is + not visible in e.g. the menuconfig interface. The visibility of symbols + without prompts is always "n". Symbols with "n" visibility can only get a + non-"n" value through a default, select, or imply. + + Note that 'depends on' and parent dependencies (including 'visible if' + dependencies) are propagated to the prompt dependencies. Additional + dependencies can be specified with e.g. 'bool "foo" if ". + + user_value: + The value assigned with Symbol.set_value(), or None if no value has been + assigned. This won't necessarily match 'value' even if set, as + dependencies and prompt visibility take precedence. + + Note that you should use Symbol.set_value() to change this value. + Properties are always read-only. + + config_string: + The .config assignment string that would get written out for the symbol + by Config.write_config(). None if no .config assignment would get written + out. In general, visible symbols, symbols with (active) defaults, and + selected symbols get written out. + + nodes: + A list of MenuNode's for this symbol. For most symbols, this list will + contain a single MenuNode. Undefined symbols get an empty list, and + symbols defined in multiple locations get one node for each location. + + choice: + Holds the parent Choice for choice symbols, and None for non-choice + symbols. Doubles as a flag for whether a symbol is a choice symbol. + + defaults: + List of (default, cond) tuples for the symbol's 'default's. For example, + 'default A && B if C || D' is represented as ((AND, A, B), (OR, C, D)). + If there is no condition, 'cond' is None. + + Note that 'depends on' and parent dependencies are propagated to + 'default' conditions. + + selects: + List of (symbol, cond) tuples for the symbol's 'select's. For example, + 'select A if B' is represented as (A, B). If there is no condition, + 'cond' is None. + + Note that 'depends on' and parent dependencies are propagated to 'select' + conditions. + + implies: + List of (symbol, cond) tuples for the symbol's 'imply's. For example, + 'imply A if B' is represented as (A, B). If there is no condition, 'cond' + is None. + + Note that 'depends on' and parent dependencies are propagated to 'imply' + conditions. + + ranges: + List of (low, high, cond) tuples for the symbol's 'range's. For example, + 'range 1 2 if A' is represented as (1, 2, A). If there is no condition, + 'cond' is None. + + Note that 'depends on' and parent dependencies are propagated to 'range' + conditions. + + Gotcha: Integers are represented as Symbols too. Undefined symbols get + their name as their value, so this works out. The C tools work the same + way. + + rev_dep: + Reverse dependency expression from being 'select'ed by other symbols. + Multiple selections get ORed together. A condition on a select is ANDed + with the selecting symbol. For example, if A has 'select FOO' and B has + 'select FOO if C', then FOO's rev_dep will be '(OR, A, (AND, B, C))'. + + weak_rev_dep: + Like rev_dep, for imply. + + direct_deps: + The 'depends on' dependencies. If a symbol is defined in multiple + locations, the dependencies at each location are ORed together. + + env_var: + If the Symbol is set from the environment via 'option env="FOO"', this + contains the name ("FOO") of the environment variable. None for symbols + that aren't set from the environment. + + Internally, this is only used to print the symbol. The value of the + environment variable is looked up once when the configuration is parsed. + + is_allnoconfig_y: + True if the symbol has 'option allnoconfig_y' set on it. This has no + effect internally, but can be checked by scripts. + + config: + The Config instance this symbol is from. + """ + + __slots__ = ( + "_already_written", + "_cached_assignable", + "_cached_deps", + "_cached_val", + "_cached_vis", + "_direct_dependents", + "_type", + "_write_to_conf", + "choice", + "config", + "defaults", + "direct_deps", + "env_var", + "implies", + "is_allnoconfig_y", + "name", + "nodes", + "ranges", + "rev_dep", + "selects", + "user_value", + "weak_rev_dep", + ) # # Public interface - # - - def get_config(self): - """Returns the Config instance this symbol is from.""" - return self._config - - def get_name(self): - """Returns the name of the symbol.""" - return self._name - - def get_type(self): - """Returns the type of the symbol: one of UNKNOWN, BOOL, TRISTATE, - STRING, HEX, or INT. These are defined at the top level of the module, - so you'd do something like - - if sym.get_type() == kconfiglib.STRING: - ...""" - return self._type - - def get_prompts(self): - """Returns a list of prompts defined for the symbol, in the order they - appear in the configuration files. Returns the empty list for symbols - with no prompt. - - This list will have a single entry for the vast majority of symbols - having prompts, but having multiple prompts for a single symbol is - possible through having multiple 'config' entries for it.""" - return [prompt for prompt, _ in self._orig_prompts] - - def get_help(self): - """Returns the help text of the symbol, or None if the symbol has no - help text.""" - return self._help - - def get_parent(self): - """Returns the menu or choice statement that contains the symbol, or - None if the symbol is at the top level. Note that if statements are - treated as syntactic and do not have an explicit class - representation.""" - return self._parent - - def get_def_locations(self): - """Returns a list of (filename, linenr) tuples, where filename (string) - and linenr (int) represent a location where the symbol is defined. For - the vast majority of symbols this list will only contain one element. - For the following Kconfig, FOO would get two entries: the lines marked - with *. - - config FOO * - bool "foo prompt 1" - - config FOO * - bool "foo prompt 2" - """ - return self._def_locations - - def get_ref_locations(self): - """Returns a list of (filename, linenr) tuples, where filename (string) - and linenr (int) represent a location where the symbol is referenced in - the configuration. For example, the lines marked by * would be included - for FOO below: - - config A - bool - default BAR || FOO * + # - config B - tristate - depends on FOO * - default m if FOO * + @property + def type(self): + """ + See the class documentation. + """ - if FOO * - config A - bool "A" - endif + if self._type == TRISTATE and \ + ((self.choice is not None and self.choice.value == "y") or + self.config.modules.value == "n"): + return BOOL + return self._type - config FOO (definition not included) - bool + @property + def value(self): + """ + See the class documentation. """ - return self._ref_locations - - def get_value(self): - """Calculate and return the value of the symbol. See also - Symbol.set_user_value().""" if self._cached_val is not None: return self._cached_val @@ -2083,48 +1800,48 @@ class Symbol(Item): # value. This is why things like "FOO = bar" work for seeing if FOO has # the value "bar". if self._type == UNKNOWN: - self._cached_val = self._name - return self._name + self._cached_val = self.name + return self.name # This will hold the value at the end of the function val = _DEFAULT_VALUE[self._type] - vis = _get_visibility(self) + vis = self.visibility if self._type in (BOOL, TRISTATE): - if not self._is_choice_sym: + if self.choice is None: self._write_to_conf = (vis != "n") - if vis != "n" and self._user_val is not None: + if vis != "n" and self.user_value is not None: # If the symbol is visible and has a user value, we use # that - val = self._config._eval_min(self._user_val, vis) + val = _eval_min(self.user_value, vis) else: # Otherwise, we look at defaults and weak reverse # dependencies (implies) - for def_expr, cond_expr in self._def_exprs: - cond_val = self._config._eval_expr(cond_expr) + for default, cond in self.defaults: + cond_val = eval_expr(cond) if cond_val != "n": self._write_to_conf = True - val = self._config._eval_min(def_expr, cond_val) + val = _eval_min(default, cond_val) break # Weak reverse dependencies are only considered if our # direct dependencies are met - if self._config._eval_expr(self._direct_deps) != "n": + if eval_expr(self.direct_deps) != "n": weak_rev_dep_val = \ - self._config._eval_expr(self._weak_rev_dep) + eval_expr(self.weak_rev_dep) if weak_rev_dep_val != "n": self._write_to_conf = True - val = self._config._eval_max(val, weak_rev_dep_val) + val = _eval_max(val, weak_rev_dep_val) # Reverse (select-related) dependencies take precedence - rev_dep_val = self._config._eval_expr(self._rev_dep) + rev_dep_val = eval_expr(self.rev_dep) if rev_dep_val != "n": self._write_to_conf = True - val = self._config._eval_max(val, rev_dep_val) + val = _eval_max(val, rev_dep_val) else: # (bool/tristate) symbol in choice. See _get_visibility() for @@ -2134,33 +1851,31 @@ class Symbol(Item): self._write_to_conf = False if vis != "n": - choice = self._parent - mode = choice.get_mode() + mode = self.choice.value if mode != "n": self._write_to_conf = True if mode == "y": - val = "y" if choice.get_selection() is self \ - else "n" - elif self._user_val in ("m", "y"): - # mode == "m" here + val = "y" if self.choice.selection is self else "n" + elif self.user_value in ("m", "y"): + # mode == "m" and self.user_value is not None or + # "n" val = "m" - # We need to promote "m" to "y" in two circumstances: + # "m" is promoted to "y" in two circumstances: # 1) If our type is boolean - # 2) If our _weak_rev_dep (from IMPLY) is "y" + # 2) If our weak_rev_dep (from IMPLY) is "y" if val == "m" and \ - (self._type == BOOL or - self._config._eval_expr(self._weak_rev_dep) == "y"): + (self.type == BOOL or eval_expr(self.weak_rev_dep) == "y"): val = "y" elif self._type in (INT, HEX): base = _TYPE_TO_BASE[self._type] # Check if a range is in effect - for low_expr, high_expr, cond_expr in self._ranges: - if self._config._eval_expr(cond_expr) != "n": + for low_expr, high_expr, cond_expr in self.ranges: + if eval_expr(cond_expr) != "n": has_active_range = True low_str = _str_val(low_expr) @@ -2177,21 +1892,21 @@ class Symbol(Item): self._write_to_conf = (vis != "n") - if vis != "n" and self._user_val is not None and \ - _is_base_n(self._user_val, base) and \ + if vis != "n" and self.user_value is not None and \ + _is_base_n(self.user_value, base) and \ (not has_active_range or - low <= int(self._user_val, base) <= high): + low <= int(self.user_value, base) <= high): # If the user value is well-formed and satisfies range # contraints, it is stored in exactly the same form as # specified in the assignment (with or without "0x", etc.) - val = self._user_val + val = self.user_value else: # No user value or invalid user value. Look at defaults. - for val_expr, cond_expr in self._def_exprs: - if self._config._eval_expr(cond_expr) != "n": + for val_expr, cond_expr in self.defaults: + if eval_expr(cond_expr) != "n": self._write_to_conf = True # Similarly to above, well-formed defaults are @@ -2227,11 +1942,11 @@ class Symbol(Item): elif self._type == STRING: self._write_to_conf = (vis != "n") - if vis != "n" and self._user_val is not None: - val = self._user_val + if vis != "n" and self.user_value is not None: + val = self.user_value else: - for val_expr, cond_expr in self._def_exprs: - if self._config._eval_expr(cond_expr) != "n": + for val_expr, cond_expr in self.defaults: + if eval_expr(cond_expr) != "n": self._write_to_conf = True val = _str_val(val_expr) break @@ -2239,480 +1954,319 @@ class Symbol(Item): self._cached_val = val return val - def get_user_value(self): - """Returns the value assigned to the symbol in a .config or via - Symbol.set_user_value() (provided the value was valid for the type of - the symbol). Returns None in case of no user value.""" - return self._user_val + @property + def assignable(self): + """ + See the class documentation. + """ + if self._cached_assignable is not None: + return self._cached_assignable - def get_upper_bound(self): - """For string/hex/int symbols and for bool and tristate symbols that - cannot be modified (see is_modifiable()), returns None. + self._cached_assignable = self._get_assignable() + return self._cached_assignable - Otherwise, returns the highest value the symbol can be set to with - Symbol.set_user_value() (that will not be truncated): one of "m" or - "y", arranged from lowest to highest. This corresponds to the highest - value the symbol could be given in e.g. the 'make menuconfig' - interface. + @property + def visibility(self): + """ + See the class documentation. + """ + if self._cached_vis is not None: + return self._cached_vis - See also the tri_less*() and tri_greater*() functions, which could come - in handy.""" - if self._type not in (BOOL, TRISTATE): - return None + self._cached_vis = _get_visibility(self) + return self._cached_vis - # Fast path for the common case - if self._rev_dep == "n": - vis = _get_visibility(self) - return vis if vis != "n" else None + @property + def config_string(self): + """ + See the class documentation. + """ - rev_dep_val = self._config._eval_expr(self._rev_dep) - # A bool selected to "m" gets promoted to "y", pinning it - if rev_dep_val == "m" and self._type == BOOL: + if self.env_var is not None: + # Variables with 'option env' never get written out. This + # corresponds to the SYMBOL_AUTO flag in the C implementation. return None - vis = _get_visibility(self) - return vis if tri_greater(vis, rev_dep_val) else None - - def get_lower_bound(self): - """For string/hex/int symbols and for bool and tristate symbols that - cannot be modified (see is_modifiable()), returns None. - - Otherwise, returns the lowest value the symbol can be set to with - Symbol.set_user_value() (that will not be truncated): one of "n" or - "m", arranged from lowest to highest. This corresponds to the lowest - value the symbol could be given in e.g. the 'make menuconfig' - interface. - See also the tri_less*() and tri_greater*() functions, which could come - in handy.""" - if self._type not in (BOOL, TRISTATE): - return None - rev_dep_val = self._config._eval_expr(self._rev_dep) - # A bool selected to "m" gets promoted to "y", pinning it - if rev_dep_val == "m" and self._type == BOOL: + # Note: _write_to_conf is determined when the value is calculated + val = self.value + if not self._write_to_conf: return None - return rev_dep_val if tri_greater(_get_visibility(self), rev_dep_val) \ - else None - def get_assignable_values(self): - """For string/hex/int symbols and for bool and tristate symbols that - cannot be modified (see is_modifiable()), returns the empty list. + if self._type in (BOOL, TRISTATE): + return "{}{}={}\n" \ + .format(self.config.config_prefix, self.name, val) \ + if val != "n" else \ + "# {}{} is not set\n" \ + .format(self.config.config_prefix, self.name) + + if self._type in (INT, HEX): + return "{}{}={}\n" \ + .format(self.config.config_prefix, self.name, val) - Otherwise, returns a list containing the user values that can be - assigned to the symbol (that won't be truncated). Usage example: + if self._type == STRING: + # Escape \ and " + return '{}{}="{}"\n' \ + .format(self.config.config_prefix, self.name, + val.replace("\\", "\\\\").replace('"', '\\"')) - if "m" in sym.get_assignable_values(): - sym.set_user_value("m") + _internal_error("Internal error while creating .config: unknown " + 'type "{}".'.format(self._type)) - This is basically a more convenient interface to - get_lower/upper_bound() when wanting to test if a particular tristate - value can be assigned.""" - if self._type not in (BOOL, TRISTATE): - return [] - rev_dep_val = self._config._eval_expr(self._rev_dep) - # A bool selected to "m" gets promoted to "y", pinning it - if rev_dep_val == "m" and self._type == BOOL: - return [] - res = ["n", "m", "y"][_TRI_TO_INT[rev_dep_val] : - _TRI_TO_INT[_get_visibility(self)] + 1] - return res if len(res) > 1 else [] - - def get_visibility(self): - """Returns the visibility of the symbol: one of "n", "m" or "y". For - bool and tristate symbols, this is an upper bound on the value users - can set for the symbol. For other types of symbols, a visibility of "n" - means the user value will be ignored. A visibility of "n" corresponds - to not being visible in the 'make *config' interfaces. - - Example (assuming we're running with modules enabled -- i.e., MODULES - set to 'y'): - - # Assume this has been assigned 'n' - config N_SYM - tristate "N_SYM" - - # Assume this has been assigned 'm' - config M_SYM - tristate "M_SYM" - - # Has visibility 'n' - config A - tristate "A" - depends on N_SYM - - # Has visibility 'm' - config B - tristate "B" - depends on M_SYM - - # Has visibility 'y' - config C - tristate "C" - - # Has no prompt, and hence visibility 'n' - config D - tristate - - Having visibility be tri-valued ensures that e.g. a symbol cannot be - set to "y" by the user if it depends on a symbol with value "m", which - wouldn't be safe. - - You should probably look at get_lower/upper_bound(), - get_assignable_values() and is_modifiable() before using this.""" - return _get_visibility(self) - - def get_referenced_symbols(self, refs_from_enclosing=False): - """Returns the set() of all symbols referenced by this item. For - example, the symbol defined by - - config FOO - bool - prompt "foo" if A && B - default C if D - depends on E - select F if G - - references the symbols A through G. - - refs_from_enclosing (default: False): If True, the symbols referenced - by enclosing menus and ifs will be included in the result.""" - res = [] - - for _, cond_expr in self._orig_prompts: - _expr_syms(cond_expr, res) - for val_expr, cond_expr in self._orig_def_exprs: - _expr_syms(val_expr, res) - _expr_syms(cond_expr, res) - for sym, cond_expr in self._orig_selects: - res.append(sym) - _expr_syms(cond_expr, res) - for sym, cond_expr in self._orig_implies: - res.append(sym) - _expr_syms(cond_expr, res) - for low, high, cond_expr in self._orig_ranges: - res.append(low) - res.append(high) - _expr_syms(cond_expr, res) - - if refs_from_enclosing: - _expr_syms(self._deps_from_containing, res) - - # Remove duplicates and return - return set(res) - - def get_selected_symbols(self): - """Returns the set() of all symbols X for which this symbol has a - 'select X' or 'select X if Y' (regardless of whether Y is satisfied or - not). This is a subset of the symbols returned by - get_referenced_symbols().""" - return {sym for sym, _ in self._orig_selects} - - def get_implied_symbols(self): - """Returns the set() of all symbols X for which this symbol has an - 'imply X' or 'imply X if Y' (regardless of whether Y is satisfied or - not). This is a subset of the symbols returned by - get_referenced_symbols().""" - return {sym for sym, _ in self._orig_implies} - - def set_user_value(self, v): - """Sets the user value of the symbol. + def set_value(self, value): + """ + Sets the user value of the symbol. Equal in effect to assigning the value to the symbol within a .config - file. Use get_lower/upper_bound() or get_assignable_values() to find - the range of currently assignable values for bool and tristate symbols; - setting values outside this range will cause the user value to differ - from the result of Symbol.get_value() (be truncated). Values that are - invalid for the type (such as a_bool.set_user_value("foo")) are - ignored, and a warning is emitted if an attempt is made to assign such - a value. - - For any type of symbol, is_modifiable() can be used to check if a user - value will currently have any effect on the symbol, as determined by - its visibility and range of assignable values. Any value that is valid - for the type (bool, tristate, etc.) will end up being reflected in - get_user_value() though, and might have an effect later if conditions - change. To get rid of the user value, use unset_user_value(). - - Any symbols dependent on the symbol are (recursively) invalidated, so - things will just work with regards to dependencies. - - v: The user value to give to the symbol.""" - self._set_user_value_no_invalidate(v, False) - - if self._name == "MODULES": + file. Use the 'assignable' attribute to check which values can + currently be assigned. Setting values outside 'assignable' will cause + Symbol.user_value to differ from Symbol.value (be truncated down or + up). Values that are invalid for the type (such as "foo" or "m" for a + BOOL) are ignored (and won't be stored in Symbol.user_value). A warning + is printed for attempts to assign invalid values. + + The values of other symbols that depend on this symbol are + automatically recalculated to reflect the new value. + + value: + The user value to give to the symbol. + """ + self._set_value_no_invalidate(value, False) + + if self is self.config.modules: # Changing MODULES has wide-ranging effects - self._config._invalidate_all() - return + self.config._invalidate_all() + else: + self._rec_invalidate() - self._invalidate() - self._invalidate_dependent() - - def unset_user_value(self): - """Resets the user value of the symbol, as if the symbol had never - gotten a user value via Config.load_config() or - Symbol.set_user_value().""" - self._unset_user_value_no_recursive_invalidate() - self._invalidate_dependent() - - def is_modifiable(self): - """Returns True if the value of the symbol could be modified by calling - Symbol.set_user_value(). - - For bools and tristates, this corresponds to the symbol being visible - in the 'make menuconfig' interface and not already being pinned to a - specific value (e.g. because it is selected by another symbol). - - For strings and numbers, this corresponds to just being visible. (See - Symbol.get_visibility().)""" - if self._is_special: - return False - if self._type in (BOOL, TRISTATE): - rev_dep_val = self._config._eval_expr(self._rev_dep) - # A bool selected to "m" gets promoted to "y", pinning it - if rev_dep_val == "m" and self._type == BOOL: - return False - return tri_greater(_get_visibility(self), rev_dep_val) - return _get_visibility(self) != "n" - - def is_defined(self): - """Returns False if the symbol is referred to in the Kconfig but never - actually defined.""" - return self._is_defined - - def is_special(self): - """Returns True if the symbol is one of the special symbols n, m, y, or - UNAME_RELEASE, or gets its value from the environment.""" - return self._is_special - - def is_from_environment(self): - """Returns True if the symbol gets its value from the environment.""" - return self._is_from_env - - def has_ranges(self): - """Returns True if the symbol is of type INT or HEX and has ranges that - limit what values it can take on.""" - return bool(self._ranges) - - def is_choice_symbol(self): - """Returns True if the symbol is in a choice statement and is an actual - choice symbol (see Choice.get_symbols()).""" - return self._is_choice_sym - - def is_choice_selection(self): - """Returns True if the symbol is contained in a choice statement and is - the selected item. Equivalent to - - sym.is_choice_symbol() and sym.get_parent().get_selection() is sym""" - return self._is_choice_sym and self._parent.get_selection() is self - - def is_allnoconfig_y(self): - """Returns True if the symbol has the 'allnoconfig_y' option set.""" - return self._allnoconfig_y + def unset_value(self): + """ + Resets the user value of the symbol, as if the symbol had never gotten + a user value via Config.load_config() or Symbol.set_value(). + """ + self.user_value = None + self._rec_invalidate() def __str__(self): - """Returns a string containing various information about the symbol.""" - return self._config._get_sym_or_choice_str(self) + """ + Returns a string representation of the symbol, matching the Kconfig + format. As a convenience, prompts and help texts are also printed, even + though they really belong to the symbol's menu nodes and not to the + symbol itself. + + The output is designed so that feeding it back to a Kconfig parser + redefines the symbol as is. This also works for symbols defined in + multiple locations, where all the definitions are output. + + An empty string is returned for undefined symbols. + """ + return _sym_choice_str(self) + + def __repr__(self): + """ + Prints some information about the symbol (including its name, value, + and visibility) when it is evaluated. + """ + fields = [ + "symbol " + self.name, + _TYPENAME[self.type], + 'value "{}"'.format(self.value), + "visibility {}".format(self.visibility) + ] + + if self.user_value is not None: + fields.append('user value "{}"'.format(self.user_value)) + + if self.choice is not None: + fields.append("choice symbol") + + if self.is_allnoconfig_y: + fields.append("allnoconfig_y") + + if self is self.config.defconfig_list: + fields.append("is the defconfig_list symbol") + + if self.env_var is not None: + fields.append("from environment variable " + self.env_var) + + if self is self.config.modules: + fields.append("is the modules symbol") + + fields.append("direct deps " + eval_expr(self.direct_deps)) + + fields.append("{} menu node{}" + .format(len(self.nodes), + "" if len(self.nodes) == 1 else "s")) + + return "<{}>".format(", ".join(fields)) # # Private methods # def __init__(self): - """Symbol constructor -- not intended to be called directly by - Kconfiglib clients.""" + """ + Symbol constructor -- not intended to be called directly by Kconfiglib + clients. + """ # These attributes are always set on the instance from outside and # don't need defaults: - # _config - # _name + # config + # name # _already_written self._type = UNKNOWN - self._prompts = [] - self._def_exprs = [] # 'default' properties - self._ranges = [] # 'range' properties (for int and hex) - self._help = None # Help text - self._rev_dep = "n" # Reverse (select-related) dependencies - self._weak_rev_dep = "n" # Weak reverse (imply-related) dependencies - self._parent = None - - self._user_val = None # Value set by user - - # Prompts, default values, ranges, selects, and implies without any - # dependencies from parents propagated to them - self._orig_prompts = [] - self._orig_def_exprs = [] - self._orig_selects = [] - self._orig_implies = [] - self._orig_ranges = [] - - # Dependencies inherited from containing menus and ifs - self._deps_from_containing = None - - # See comment in _parse_properties() - self._menu_dep = None - - # The direct dependencies (inherited + 'depends on', with OR if a - # symbol is defined in multiple locations). This is needed for 'imply' - # support. - self._direct_deps = "n" - - # See Symbol.get_ref/def_locations(). - self._def_locations = [] - self._ref_locations = [] + self.defaults = [] + self.selects = [] + self.implies = [] + self.ranges = [] + self.rev_dep = "n" + self.weak_rev_dep = "n" + + self.nodes = [] + + self.user_value = None + + self.direct_deps = "n" # Populated in Config._build_dep() after parsing. Links the symbol to # the symbols that immediately depend on it (in a caching/invalidation - # sense). The total set of dependent symbols for the symbol (the - # transitive closure) is calculated on an as-needed basis in - # _get_dependent(). + # sense). The total set of dependent symbols for the symbol is + # calculated as needed in _get_dependent(). self._direct_dependents = set() # Cached values # Caches the calculated value self._cached_val = None - # Caches the visibility, which acts as an upper bound on the value - self._cached_visibility = None + # Caches the visibility + self._cached_vis = None # Caches the total list of dependent symbols. Calculated in # _get_dependent(). self._cached_deps = None + # Caches the 'assignable' attribute + self._cached_assignable = None # Flags - # Does the symbol have an entry in the Kconfig file? - self._is_defined = False - # Should the symbol get an entry in .config? + self.env_var = None + + self.choice = None + self.is_allnoconfig_y = False + + # Should the symbol get an entry in .config? Calculated along with the + # value. self._write_to_conf = False - # This is set to True for "actual" choice symbols; see - # Choice._determine_actual_symbols(). - self._is_choice_sym = False - # Does the symbol get its value in some special way, e.g. from the - # environment or by being one of the special symbols n, m, and y? If - # so, the value is stored in self._cached_val, which is never - # invalidated. - self._is_special = False - # Does the symbol get its value from the environment? - self._is_from_env = False - # Does the symbol have the 'allnoconfig_y' option set? - self._allnoconfig_y = False - def _invalidate(self): - if self._is_special: - # Special symbols never change value and keep their value in - # _cached_val - return + def _get_assignable(self): + """ + Worker function for the 'assignable' attribute. + """ - if self._is_choice_sym: - self._parent._invalidate() + if self._type not in (BOOL, TRISTATE): + return "" - self._cached_val = None - self._cached_visibility = None + vis = self.visibility - def _invalidate_dependent(self): - for sym in self._get_dependent(): - sym._invalidate() + if vis == "n": + return "" - def _set_user_value_no_invalidate(self, v, suppress_load_warnings): - """Like set_user_value(), but does not invalidate any symbols. + rev_dep_val = eval_expr(self.rev_dep) - suppress_load_warnings: some warnings are annoying when loading a - .config that can be helpful when manually invoking set_user_value(). - This flag is set to True to suppress such warnings. + if vis == "y": + if rev_dep_val == "n": + if self.type == BOOL or eval_expr(self.weak_rev_dep) == "y": + return "ny" + return "nmy" - Perhaps this could be made optional for load_config() instead.""" + if rev_dep_val == "y": + return "y" - if self._is_special: - if self._is_from_env: - self._config._warn('attempt to assign the value "{}" to the ' - 'symbol {}, which gets its value from the ' - 'environment. Assignment ignored.' - .format(v, self._name)) - else: - self._config._warn('attempt to assign the value "{}" to the ' - 'special symbol {}. Assignment ignored.' - .format(v, self._name)) - return + # rev_dep_val == "m" - if not self._is_defined: - filename, linenr = self._ref_locations[0] - self._config._warn_undef_assign( - 'attempt to assign the value "{}" to {}, which is referenced ' - "at {}:{} but never defined. Assignment ignored." - .format(v, self._name, filename, linenr)) - return + if self.type == BOOL or eval_expr(self.weak_rev_dep) == "y": + return "y" + return "my" - # Check if the value is valid for our type - if not ((self._type == BOOL and v in ("n", "y") ) or - (self._type == TRISTATE and v in ("n", "m", "y")) or - (self._type == STRING ) or - (self._type == INT and _is_base_n(v, 10) ) or - (self._type == HEX and _is_base_n(v, 16) )): - self._config._warn('the value "{}" is invalid for {}, which has ' - "type {}. Assignment ignored." - .format(v, self._name, _TYPENAME[self._type])) - return + # vis == "m" + + if rev_dep_val == "n": + return "m" if eval_expr(self.weak_rev_dep) != "y" else "y" - if not self._prompts and not suppress_load_warnings: - self._config._warn('assigning "{}" to the symbol {} which lacks ' - 'prompts and thus has visibility "n". The ' - 'assignment will have no effect.' - .format(v, self._name)) + if rev_dep_val == "y": + return "y" - self._user_val = v + # vis == "m", rev_dep == "m" (rare) - if self._is_choice_sym and self._type in (BOOL, TRISTATE): - choice = self._parent - if v == "y": - choice._user_val = self - choice._user_mode = "y" - elif v == "m": - choice._user_val = None - choice._user_mode = "m" + return "m" - def _unset_user_value_no_recursive_invalidate(self): - self._invalidate() - self._user_val = None + def _set_value_no_invalidate(self, value, suppress_prompt_warning): + """ + Like set_value(), but does not invalidate any symbols. - if self._is_choice_sym: - self._parent._unset_user_value() + suppress_prompt_warning: + The warning about assigning a value to a promptless symbol gets + spammy for Linux defconfigs, so turn it off when loading .configs. + It's still helpful when manually invoking set_value(). + """ - def _add_config_strings(self, add_fn): - if self._already_written: + # Check if the value is valid for our type + if not ((self._type == BOOL and value in ("n", "y") ) or + (self._type == TRISTATE and value in ("n", "m", "y")) or + (self._type == STRING ) or + (self._type == INT and _is_base_n(value, 10) ) or + (self._type == HEX and _is_base_n(value, 16) )): + self.config._warn('the value "{}" is invalid for {}, which has ' + "type {}. Assignment ignored." + .format(value, self.name, _TYPENAME[self._type])) return - self._already_written = True + if not self.nodes: + self.config._warn_undef_assign( + 'assigning the value "{}" to the undefined symbol {} will ' + "have no effect".format(value, self.name)) - # Note: _write_to_conf is determined in get_value() - val = self.get_value() - if not self._write_to_conf: - return + if not suppress_prompt_warning: + for node in self.nodes: + if node.prompt is not None: + break + else: + self.config._warn('assigning the value "{}" to the ' + "promptless symbol {} will have no effect" + .format(value, self.name)) - if self._type in (BOOL, TRISTATE): - add_fn("# {}{} is not set\n".format(self._config._config_prefix, - self._name) - if val == "n" else - "{}{}={}\n".format(self._config._config_prefix, self._name, - val)) + self.user_value = value - elif self._type in (INT, HEX): - add_fn("{}{}={}\n".format(self._config._config_prefix, self._name, - val)) + if self.choice is not None and self._type in (BOOL, TRISTATE): + if value == "y": + self.choice.user_selection = self + self.choice.user_value = "y" + elif value == "m": + self.choice.user_value = "m" - elif self._type == STRING: - # Escape \ and " - add_fn('{}{}="{}"\n' - .format(self._config._config_prefix, self._name, - val.replace("\\", "\\\\").replace('"', '\\"'))) + def _invalidate(self): + """ + Marks the symbol as needing to be recalculated. + """ + self._cached_val = self._cached_vis = self._cached_assignable = None - else: - _internal_error("Internal error while creating .config: unknown " - 'type "{}".'.format(self._type)) + def _rec_invalidate(self): + """ + Invalidates the symbol and all symbols and choices that (possibly + indirectly) depend on it + """ + self._invalidate() + + for item in self._get_dependent(): + item._invalidate() def _get_dependent(self): - """Returns the set of symbols that should be invalidated if the value - of the symbol changes, because they might be affected by the change. - Note that this is an internal API -- it's probably of limited - usefulness to clients.""" + """ + Returns the set of symbols that should be invalidated if the value of + the symbol changes, because they might be affected by the change. Note + that this is an internal API and probably of limited usefulness to + clients. + """ if self._cached_deps is not None: return self._cached_deps @@ -2727,554 +2281,506 @@ class Symbol(Item): {sym for dep in self._direct_dependents for sym in dep._get_dependent()} - if self._is_choice_sym: + if self.choice is not None: + # Choices depend on their choice symbols + res.add(self.choice) + # Choice symbols also depend (recursively) on their siblings. The # siblings are not included in _direct_dependents to avoid # dependency loops. - for sibling in self._parent._actual_symbols: + for sibling in self.choice.syms: if sibling is not self: res.add(sibling) - res |= sibling._direct_dependents - for s in sibling._direct_dependents: - res |= s._get_dependent() - - self._cached_deps = res - return res - - def _has_auto_menu_dep_on(self, on): - """See Choice._determine_actual_symbols().""" - if not isinstance(self._parent, Choice): - _internal_error("Attempt to determine auto menu dependency for " - "symbol ouside of choice.") - - if not self._prompts: - # If we have no prompt, use the menu dependencies instead (what was - # specified with 'depends on') - return self._menu_dep is not None and \ - self._config._expr_depends_on(self._menu_dep, on) - - for _, cond_expr in self._prompts: - if self._config._expr_depends_on(cond_expr, on): - return True - - return False + # Less readable version of the following: + # + # res |= sibling._direct_dependents + # for s in sibling._direct_dependents: + # res |= s._get_dependent() + res |= sibling._direct_dependents | \ + {sym for dep in sibling._direct_dependents + for sym in dep._get_dependent()} -class Menu(Item): + # The tuple conversion sped up allnoconfig_simpler.py by 10% + self._cached_deps = tuple(res) + return self._cached_deps - """Represents a menu statement.""" +class Choice(object): + """ + Represents a choice statement: + + choice + ... + + The following attributes are available on Choice instances. They should be + viewed as read-only, and some are implemented through @property magic (but + are still efficient to access due to internal caching). + + name: + The name of the choice, e.g. "FOO" for 'choice FOO', or None if the + Choice has no name. I can't remember ever seeing named choices in + practice, but the C tools support them too. + + type: + The type of the choice. One of BOOL, TRISTATE, UNKNOWN. UNKNOWN is for + choices defined without a type where none of the contained symbols have a + type either (otherwise the choice inherits the type of the first symbol + defined with a type). + + When running without modules (CONFIG_MODULES=n), TRISTATE choices + magically change type to BOOL. This matches the C tools, and makes sense + for menuconfig-like functionality. (Check the implementation of the + property if you need to get the original type.) + + value: + The tristate value (mode) of the choice. A choice can be in one of three + modes: + + "n" - The choice is not visible and no symbols can be selected. + + "m" - Any number of symbols can be set to "m". The rest will be "n". + + "y" - One symbol will be "y" while the rest are "n". + + Only tristate choices can be in "m" mode, and the visibility of the + choice is an upper bound on the mode. + + The mode changes automatically when a value is assigned to a symbol + within the choice (this makes .config loading "just work"), and can also + be changed via Choice.set_value(). + + See the implementation note at the end for one reason why it makes sense + to call this 'value' rather than e.g. 'mode'. It also makes the Choice + and Symbol interfaces consistent. + + assignable: + See the symbol class documentation. Gives the assignable values (modes). + + visibility: + See the Symbol class documentation. Acts on the value (mode). + + selection: + The currently selected symbol. None if the Choice is not in "y" mode or + has no selected symbol (due to unsatisfied dependencies on choice + symbols). + + default_selection: + The symbol that would be selected by default, had the user not selected + any symbol. Can be None for the same reasons as 'selected'. + + user_value: + The value (mode) selected by the user (by assigning some choice symbol or + calling Choice.set_value()). This does not necessarily match Choice.value + for the same reasons that Symbol.user_value might not match Symbol.value. + + user_selection: + The symbol selected by the user (by setting it to "y"). Ignored if the + choice is not in "y" mode, but still remembered so that the choice "snaps + back" to the user selection if the mode is changed back to "y". + + syms: + List of symbols contained in the choice. + + Gotcha: If a symbol depends on a previous symbol within a choice so that + an implicit menu is created, it won't be a choice symbol, and won't be + included in 'syms'. There are real-world examples of this. + + nodes: + A list of MenuNode's for this symbol. In practice, the list will probably + always contain a single MenuNode, but it is possible to define a choice + in multiple locations by giving it a name, which adds more nodes. + + defaults: + List of (symbol, cond) tuples for the choices 'defaults's. For example, + 'default A if B && C' is represented as (A, (AND, B, C)). If there is no + condition, 'cond' is None. + + Note that 'depends on' and parent dependencies are propagated to + 'default' conditions. + + is_optional: + True if the choice has the 'optional' flag set on it. + + Implementation note: The C tools internally represent choices as a type of + symbol, with special-casing in many code paths, which is why there is a lot + of similarity to Symbol above. The value (mode) is really just a normal + symbol value, and an implicit reverse dependency forces its lower bound to + 'm' for non-optional choices. Kconfiglib uses a separate Choice class only + because it makes the code and interface less confusing (especially in a + user-facing interface). + """ + + __slots__ = ( + "_cached_assignable", + "_cached_selection", + "_cached_vis", + "_type", + "config", + "defaults", + "is_optional", + "name", + "nodes", + "syms", + "user_selection", + "user_value", + ) # # Public interface # - def get_config(self): - """Return the Config instance this menu is from.""" - return self._config - - def get_title(self): - """Returns the title text of the menu.""" - return self._title - - def get_parent(self): - """Returns the menu or choice statement that contains the menu, or - None if the menu is at the top level. Note that if statements are - treated as syntactic sugar and do not have an explicit class - representation.""" - return self._parent - - def get_location(self): - """Returns the location of the menu as a (filename, linenr) tuple, - where filename is a string and linenr an int.""" - return (self._filename, self._linenr) - - def get_items(self, recursive=False): - """Returns a list containing the items (symbols, menus, choice - statements and comments) in in the menu, in the same order that the - items appear within the menu. - - recursive (default: False): True if items contained in items within the - menu should be included recursively (preorder).""" - - if not recursive: - return self._block - - res = [] - for item in self._block: - res.append(item) - if isinstance(item, Menu): - res.extend(item.get_items(True)) - elif isinstance(item, Choice): - res.extend(item.get_items()) - return res - - def get_symbols(self, recursive=False): - """Returns a list containing the symbols in the menu, in the same order - that they appear within the menu. - - recursive (default: False): True if symbols contained in items within - the menu should be included recursively.""" - - return [item for item in self.get_items(recursive) if - isinstance(item, Symbol)] - - def get_visibility(self): - """Returns the visibility of the menu. This also affects the visibility - of subitems. See also Symbol.get_visibility().""" - return self._config._eval_expr(self._menu_dep) - - def get_visible_if_visibility(self): - """Returns the visibility the menu gets from its 'visible if' - condition. "y" if the menu has no 'visible if' condition.""" - return self._config._eval_expr(self._visible_if_expr) - - def get_referenced_symbols(self, refs_from_enclosing=False): - """See Symbol.get_referenced_symbols().""" - res = [] - - _expr_syms(self._visible_if_expr, res) - _expr_syms(self._orig_deps - if not refs_from_enclosing else - self._menu_dep, - res) - - # Remove duplicates and return - return set(res) - - def __str__(self): - """Returns a string containing various information about the menu.""" - depends_on_str = self._config._expr_val_str(self._orig_deps, - "(no dependencies)") - visible_if_str = self._config._expr_val_str(self._visible_if_expr, - "(no dependencies)") - - additional_deps_str = " " + \ - self._config._expr_val_str(self._deps_from_containing, - "(no additional dependencies)") - - return _lines("Menu", - "Title : " + self._title, - "'depends on' dependencies : " + depends_on_str, - "'visible if' dependencies : " + visible_if_str, - "Additional dependencies from enclosing menus and ifs:", - additional_deps_str, - "Location: {}:{}".format(self._filename, self._linenr)) - - # - # Private methods - # - - def __init__(self): - """Menu constructor -- not intended to be called directly by - Kconfiglib clients.""" - - # These attributes are always set on the instance from outside and - # don't need defaults: - # _config - # _parent - # _filename - # _linenr - # _title - # _deps_from_containing - # _menu_dep - - # Dependencies specified with 'visible_if' - self._visible_if_expr = None - - # Dependency expression without dependencies from enclosing menus and - # ifs propagated - self._orig_deps = None - - # Contained items - self._block = [] - - def _add_config_strings(self, add_fn): - if self._config._eval_expr(self._menu_dep) != "n" and \ - self._config._eval_expr(self._visible_if_expr) != "n": - add_fn("\n#\n# {}\n#\n".format(self._title)) - - for item in self._block: - item._add_config_strings(add_fn) - -class Choice(Item): - - """Represents a choice statement. A choice can be in one of three modes: - - "n" - The choice is not visible and no symbols can be selected. - - "m" - Any number of symbols can be set to "m". The rest will be "n". This - is safe since potentially conflicting options don't actually get - compiled into the kernel simultaneously with "m". - - "y" - One symbol will be "y" while the rest are "n". + @property + def type(self): + """Returns the type of the choice. See Symbol.type.""" + if self._type == TRISTATE and self.config.modules.value == "n": + return BOOL + return self._type - Only tristate choices can be in "m" mode, and the visibility of the choice - is an upper bound on the mode, so that e.g. a choice that depends on a - symbol with value "m" will be in "m" mode. + @property + def value(self): + """ + See the class documentation. + """ + if self.user_value is not None: + val = _eval_min(self.user_value, self.visibility) + else: + val = "n" - The mode changes automatically when a value is assigned to a symbol within - the choice. + if val == "n" and not self.is_optional: + val = "m" - See Symbol.get_visibility() too.""" + # Promote "m" to "y" for boolean choices + return "y" if val == "m" and self.type == BOOL else val - # - # Public interface - # + @property + def assignable(self): + """ + See the class documentation. + """ + if self._cached_assignable is not None: + return self._cached_assignable - def get_config(self): - """Returns the Config instance this choice is from.""" - return self._config + self._cached_assignable = self._get_assignable() + return self._cached_assignable - def get_name(self): - """For named choices, returns the name. Returns None for unnamed - choices. No named choices appear anywhere in the kernel Kconfig files - as of Linux 3.7.0-rc8.""" - return self._name + @property + def visibility(self): + """ + See the class documentation. + """ + if self._cached_vis is not None: + return self._cached_vis - def get_type(self): - """Returns the type of the choice. See Symbol.get_type().""" - return self._type + self._cached_vis = _get_visibility(self) + return self._cached_vis - def get_prompts(self): - """Returns a list of prompts defined for the choice, in the order they - appear in the configuration files. Returns the empty list for choices - with no prompt. - - This list will have a single entry for the vast majority of choices - having prompts, but having multiple prompts for a single choice is - possible through having multiple 'choice' entries for it (though I'm - not sure if that ever happens in practice).""" - return [prompt for prompt, _ in self._orig_prompts] - - def get_help(self): - """Returns the help text of the choice, or None if the choice has no - help text.""" - return self._help - - def get_parent(self): - """Returns the menu or choice statement that contains the choice, or - None if the choice is at the top level. Note that if statements are - treated as syntactic sugar and do not have an explicit class - representation.""" - return self._parent - - def get_def_locations(self): - """Returns a list of (filename, linenr) tuples, where filename (string) - and linenr (int) represent a location where the choice is defined. For - the vast majority of choices (all of them as of Linux 3.7.0-rc8) this - list will only contain one element, but its possible for named choices - to be defined in multiple locations.""" - return self._def_locations - - def get_selection(self): - """Returns the symbol selected (either by the user or through - defaults), or None if either no symbol is selected or the mode is not - "y".""" - if self._cached_selection is not None: - if self._cached_selection == _NO_SELECTION: - return None + @property + def selection(self): + """ + See the class documentation. + """ + if self._cached_selection is not _NO_CACHED_SELECTION: return self._cached_selection - if self.get_mode() != "y": - return self._cache_ret(None) + if self.value != "y": + self._cached_selection = None + return None # User choice available? - if self._user_val is not None and \ - _get_visibility(self._user_val) == "y": - return self._cache_ret(self._user_val) - - if self._optional: - return self._cache_ret(None) + if self.user_selection is not None and \ + self.user_selection.visibility == "y": + self._cached_selection = self.user_selection + return self.user_selection - return self._cache_ret(self.get_selection_from_defaults()) + # Look at defaults - def get_selection_from_defaults(self): - """Like Choice.get_selection(), but acts as if no symbol has been - selected by the user and no 'optional' flag is in effect.""" + self._cached_selection = self.default_selection + return self._cached_selection - # Does any 'default SYM [if ]' property apply? - for sym, cond_expr in self._def_exprs: - if (self._config._eval_expr(cond_expr) != "n" and + @property + def default_selection(self): + """ + See the class documentation. + """ + for sym, cond_expr in self.defaults: + if (eval_expr(cond_expr) != "n" and # Must be visible too - _get_visibility(sym) != "n"): + sym.visibility != "n"): return sym - # Otherwise, pick the first visible symbol - for sym in self._actual_symbols: - if _get_visibility(sym) != "n": + # Otherwise, pick the first visible symbol, if any + for sym in self.syms: + if sym.visibility != "n": return sym # Couldn't find a default return None - def get_user_selection(self): - """If the choice is in "y" mode and has a user-selected symbol, returns - that symbol. Otherwise, returns None.""" - return self._user_val - - def get_items(self): - """Gets all items contained in the choice in the same order as within - the configuration ("items" instead of "symbols" since choices and - comments might appear within choices. This only happens in one place as - of Linux 3.7.0-rc8, in drivers/usb/gadget/Kconfig).""" - return self._block - - def get_symbols(self): - """Returns a list containing the choice's symbols. - - A quirk (perhaps a bug) of Kconfig is that you can put items within a - choice that will not be considered members of the choice insofar as - selection is concerned. This happens for example if one symbol within a - choice 'depends on' the symbol preceding it, or if you put non-symbol - items within choices. - - As of Linux 3.7.0-rc8, this seems to be used intentionally in one - place: drivers/usb/gadget/Kconfig. - - This function returns the "proper" symbols of the choice in the order - they appear in the choice, excluding such items. If you want all items - in the choice, use get_items().""" - return self._actual_symbols - - def get_referenced_symbols(self, refs_from_enclosing=False): - """See Symbol.get_referenced_symbols().""" - res = [] - - for _, cond_expr in self._orig_prompts: - _expr_syms(cond_expr, res) - for val_expr, cond_expr in self._orig_def_exprs: - _expr_syms(val_expr, res) - _expr_syms(cond_expr, res) - - if refs_from_enclosing: - _expr_syms(self._deps_from_containing, res) - - # Remove duplicates and return - return set(res) - - def get_visibility(self): - """Returns the visibility of the choice statement: one of "n", "m" or - "y". This acts as an upper limit on the mode of the choice (though bool - choices can only have the mode "y"). See the class documentation for an - explanation of modes.""" - return _get_visibility(self) - - def get_mode(self): - """Returns the mode of the choice. See the class documentation for - an explanation of modes.""" - minimum_mode = "n" if self._optional else "m" - mode = self._user_mode if self._user_mode is not None else minimum_mode - mode = self._config._eval_min(mode, _get_visibility(self)) + def set_value(self, value): + """ + Sets the user value (mode) of the choice. Like for Symbol.set_value(), + the visibility might truncate the value. Choices without the 'optional' + attribute (is_optional) can never be in "n" mode, but "n" is still + accepted (and ignored) since it's not a malformed value. + """ + if not ((self._type == BOOL and value in ("n", "y") ) or + (self._type == TRISTATE and value in ("n", "m", "y"))): + self.config._warn('the value "{}" is invalid for the choice, ' + "which has type {}. Assignment ignored" + .format(value, _TYPENAME[self._type])) - # Promote "m" to "y" for boolean choices - if mode == "m" and self._type == BOOL: - return "y" + self.user_value = value - return mode + if self.syms: + # Hackish way to invalidate the choice and all the choice symbols + self.syms[0]._rec_invalidate() - def is_optional(self): - """Returns True if the choice has the 'optional' flag set (and so will - default to "n" mode).""" - return self._optional + def unset_value(self): + """ + Resets the user value (mode) and user selection of the Choice, as if + the user had never touched the mode or any of the choice symbols. + """ + self.user_value = self.user_selection = None + if self.syms: + # Hackish way to invalidate the choice and all the choice symbols + self.syms[0]._rec_invalidate() def __str__(self): - """Returns a string containing various information about the choice - statement.""" - return self._config._get_sym_or_choice_str(self) + """ + Returns a string containing various information about the choice + statement. + """ + return _sym_choice_str(self) + + def __repr__(self): + fields = [ + "choice" if self.name is None else "choice " + self.name, + _TYPENAME[self.type], + "mode " + self.value, + "visibility " + self.visibility] + + if self.is_optional: + fields.append("optional") + + if self.selection is not None: + fields.append("{} selected".format(self.selection.name)) + + fields.append("{} menu node{}" + .format(len(self.nodes), + "" if len(self.nodes) == 1 else "s")) + + return "<{}>".format(", ".join(fields)) + # # Private methods # def __init__(self): - """Choice constructor -- not intended to be called directly by - Kconfiglib clients.""" + """ + Choice constructor -- not intended to be called directly by Kconfiglib + clients. + """ # These attributes are always set on the instance from outside and # don't need defaults: - # _config - # _parent - # _deps_from_containing - # _actual_symbols (set in _determine_actual_symbols()) + # config - self._name = None # Yes, choices can be named + self.name = None self._type = UNKNOWN - self._prompts = [] - self._def_exprs = [] # 'default' properties - self._help = None # Help text + self.syms = [] + self.defaults = [] - self._user_val = None - self._user_mode = None + self.nodes = [] + + self.user_selection = None + self.user_value = None # The prompts and default values without any dependencies from # enclosing menus and ifs propagated - self._orig_prompts = [] - self._orig_def_exprs = [] - - # See Choice.get_def_locations() - self._def_locations = [] + self.defaults = [] # Cached values - self._cached_selection = None - self._cached_visibility = None - - self._optional = False - - # Contained items - self._block = [] - - def _determine_actual_symbols(self): - """If a symbol's visibility depends on the preceding symbol within a - choice, it is no longer viewed as a choice item. (This is quite - possibly a bug, but some things consciously use it... ugh. It stems - from automatic submenu creation.) In addition, it's possible to have - choices and comments within choices, and those shouldn't be considered - choice items either. Only drivers/usb/gadget/Kconfig seems to depend on - any of this. This method computes the "actual" items in the choice and - sets the _is_choice_sym flag on them (retrieved via - is_choice_symbol()). - - Don't let this scare you: an earlier version simply checked for a - sequence of symbols where all symbols after the first appeared in the - 'depends on' expression of the first, and that worked fine. The added - complexity is to be future-proof in the event that - drivers/usb/gadget/Kconfig turns even more sinister. It might very well - be overkilling things (especially if that file is refactored ;).""" - - self._actual_symbols = [] - - # Items might depend on each other in a tree structure, so we need a - # stack to keep track of the current tentative parent - stack = [] - - for item in self._block: - if not isinstance(item, Symbol): - stack = [] - continue - - while stack: - if item._has_auto_menu_dep_on(stack[-1]): - # The item should not be viewed as a choice item, so don't - # set item._is_choice_sym - stack.append(item) - break - else: - stack.pop() - else: - item._is_choice_sym = True - self._actual_symbols.append(item) - stack.append(item) - - def _cache_ret(self, selection): - # As None is used to indicate the lack of a cached value we can't use - # that to cache the fact that the choice has no selection. Instead, we - # use the symbolic constant _NO_SELECTION. - if selection is None: - self._cached_selection = _NO_SELECTION - else: - self._cached_selection = selection + self._cached_selection = _NO_CACHED_SELECTION + self._cached_vis = None + self._cached_assignable = None - return selection + self.is_optional = False - def _invalidate(self): - self._cached_selection = None - self._cached_visibility = None + def _get_assignable(self): + """ + Worker function for the 'assignable' attribute. + """ - def _unset_user_value(self): - self._invalidate() - self._user_val = None - self._user_mode = None + vis = self.visibility - def _add_config_strings(self, add_fn): - for item in self._block: - item._add_config_strings(add_fn) + if vis == "n": + return "" -class Comment(Item): + if vis == "y": + if not self.is_optional: + return "y" if self.type == BOOL else "my" + return "y" - """Represents a comment statement.""" + # vis == "m" - # - # Public interface - # + return "nm" if self.is_optional else "m" - def get_config(self): - """Returns the Config instance this comment is from.""" - return self._config + def _invalidate(self): + self._cached_selection = _NO_CACHED_SELECTION + self._cached_vis = self._cached_assignable = None + +class MenuNode(object): + """ + Represents a menu node in the configuration. This corresponds to an entry + in e.g. the 'make menuconfig' interface, though non-visible, + non-user-assignable symbols, choices, menus, and comments also get menu + nodes. If a symbol or choice is defined in multiple locations, it gets one + menu node for each location. + + The top-level menu node, corresponding to the implicit top-level menu, is + available in Config.top_menu. + + For symbols and choices, the menu nodes are available in the 'nodes' + attribute. Menus and comments are represented as plain menu nodes, with + their text stored in the prompt attribute (prompt[0]). This mirrors the C + implementation. + + The following attributes are available on MenuNode instances. They should + be viewed as read-only. + + item: + Either a Symbol, a Choice, or one of the constants MENU and COMMENT. + Menus and comments are represented as plain menu nodes. Ifs are + collapsed and do not appear in the final menu tree (matching the C + implementation). + + next: + The following menu node in the menu tree. None if there is no following + node. + + list: + The first child menu node in the menu tree. None if there are no + children. + + Choices and menus naturally have children, but Symbols can have + children too because of menus created automatically from dependencies + (see kconfig-language.txt). + + parent: + The parent menu node. None if there is no parent. + + prompt: + A (string, cond) tuple with the prompt for the menu node and its + condition. None if there is no prompt. Prompts are always stored in the + menu node rather than the Symbol or Choice. For menus and comments, the + prompt holds the text. + + help: + The help text for the menu node. None if there is no help text. Always + stored in the node rather than the Symbol or Choice. It is possible to + have a separate help at each location if a symbol is defined in + multiple locations. + + dep: + The 'depends on' dependencies for the menu node. None if there are no + dependencies. Parent dependencies are propagated to this attribute, and + this attribute is then in turn propagated to the properties of symbols + and choices. + + If a symbol is defined in multiple locations, only the properties + defined at each location get the corresponding MenuNode.dep propagated + to them. + + visibility: + The 'visible if' dependencies for the menu node (which must represent a + menu). None if there are no 'visible if' dependencies. 'visible if' + dependencies are recursively propagated to the prompts of symbols and + choices within the menu. + + is_menuconfig: + True if the symbol for the menu node (it must be a symbol) was defined + with 'menuconfig' rather than 'config' (at this location). This is a + hint on how to display the menu entry. It's ignored by Kconfiglib + itself. + + config: + The Config the menu node is from. + + filename/linenr: + The location where the menu node appears. + """ + + __slots__ = ( + "config", + "dep", + "filename", + "help", + "is_menuconfig", + "item", + "linenr", + "list", + "next", + "parent", + "prompt", + "visibility", + ) + + def __repr__(self): + fields = [] + + if isinstance(self.item, Symbol): + fields.append("menu node for symbol " + self.item.name) + elif isinstance(self.item, Choice): + s = "menu node for choice" + if self.item.name is not None: + s += " " + self.item.name + fields.append(s) + elif self.item == MENU: + fields.append("menu node for menu") + elif self.item == COMMENT: + fields.append("menu node for comment") + elif self.item is None: + fields.append("menu node for if (should not appear in the final " + " tree)") + else: + raise InternalError("unable to determine type in " + "MenuNode.__repr__()") - def get_text(self): - """Returns the text of the comment.""" - return self._text + fields.append("{}:{}".format(self.filename, self.linenr)) - def get_parent(self): - """Returns the menu or choice statement that contains the comment, or - None if the comment is at the top level. Note that if statements are - treated as syntactic sugar and do not have an explicit class - representation.""" - return self._parent + if self.prompt is not None: + fields.append('prompt "{}" (visibility {})' + .format(self.prompt[0], eval_expr(self.prompt[1]))) - def get_location(self): - """Returns the location of the comment as a (filename, linenr) tuple, - where filename is a string and linenr an int.""" - return (self._filename, self._linenr) + if isinstance(self.item, Symbol) and self.is_menuconfig: + fields.append("is menuconfig") - def get_visibility(self): - """Returns the visibility of the comment. See also - Symbol.get_visibility().""" - return self._config._eval_expr(self._menu_dep) + fields.append("deps " + eval_expr(self.dep)) - def get_referenced_symbols(self, refs_from_enclosing=False): - """See Symbol.get_referenced_symbols().""" - res = [] + if self.item == MENU: + fields.append("'visible if' deps " + eval_expr(self.visibility)) - _expr_syms(self._orig_deps - if not refs_from_enclosing else - self._menu_dep, - res) + if isinstance(self.item, (Symbol, Choice)) and self.help is not None: + fields.append("has help") - # Remove duplicates and return - return set(res) + if self.list is not None: + fields.append("has child") - def __str__(self): - """Returns a string containing various information about the - comment.""" - dep_str = self._config._expr_val_str(self._orig_deps, - "(no dependencies)") - - additional_deps_str = " " + \ - self._config._expr_val_str(self._deps_from_containing, - "(no additional dependencies)") - - return _lines("Comment", - "Text: " + self._text, - "Dependencies: " + dep_str, - "Additional dependencies from enclosing menus and ifs:", - additional_deps_str, - "Location: {}:{}".format(self._filename, self._linenr)) + if self.next is not None: + fields.append("has next") - # - # Private methods - # + return "<{}>".format(", ".join(fields)) - # These attributes are always set on the instance from outside and don't - # need defaults: - # _config - # _parent - # _filename - # _linenr - # _text - # _deps_from_containing - # _menu_dep - # _orig_deps - - def _add_config_strings(self, add_fn): - if self._config._eval_expr(self._menu_dep) != "n": - add_fn("\n#\n# {}\n#\n".format(self._text)) - -class Kconfig_Syntax_Error(Exception): - """Exception raised for syntax errors.""" +class KconfigSyntaxError(Exception): + """ + Exception raised for syntax errors. + """ pass -class Internal_Error(Exception): - """Exception raised for internal errors.""" +class InternalError(Exception): + """ + Exception raised for internal errors. + """ pass # @@ -3282,83 +2788,184 @@ class Internal_Error(Exception): # def tri_less(v1, v2): - """Returns True if the tristate v1 is less than the tristate v2, where "n", - "m" and "y" are ordered from lowest to highest.""" + """ + Returns True if the tristate v1 is less than the tristate v2, where "n", + "m" and "y" are ordered from lowest to highest. + """ return _TRI_TO_INT[v1] < _TRI_TO_INT[v2] def tri_less_eq(v1, v2): - """Returns True if the tristate v1 is less than or equal to the tristate - v2, where "n", "m" and "y" are ordered from lowest to highest.""" + """ + Returns True if the tristate v1 is less than or equal to the tristate v2, + where "n", "m" and "y" are ordered from lowest to highest. + """ return _TRI_TO_INT[v1] <= _TRI_TO_INT[v2] def tri_greater(v1, v2): - """Returns True if the tristate v1 is greater than the tristate v2, where - "n", "m" and "y" are ordered from lowest to highest.""" + """ + Returns True if the tristate v1 is greater than the tristate v2, where "n", + "m" and "y" are ordered from lowest to highest. + """ return _TRI_TO_INT[v1] > _TRI_TO_INT[v2] def tri_greater_eq(v1, v2): - """Returns True if the tristate v1 is greater than or equal to the tristate - v2, where "n", "m" and "y" are ordered from lowest to highest.""" + """ + Returns True if the tristate v1 is greater than or equal to the tristate + v2, where "n", "m" and "y" are ordered from lowest to highest. + """ return _TRI_TO_INT[v1] >= _TRI_TO_INT[v2] +# Expression evaluation + +def eval_expr(expr): + """ + Evaluates an expression to "n", "m", or "y". Returns "y" for None, which + makes sense as None usually indicates a missing condition. + """ + return "y" if expr is None else _eval_expr_rec(expr) + +def _eval_expr_rec(expr): + if isinstance(expr, Symbol): + # Non-bool/tristate symbols are always "n" in a tristate sense, + # regardless of their value + return expr.value if expr._type in (BOOL, TRISTATE) else "n" + + if isinstance(expr, str): + return expr if expr in ("m", "y") else "n" + + if expr[0] == _AND: + ev1 = _eval_expr_rec(expr[1]) + if ev1 == "n": + # No need to look at expr[2] + return "n" + ev2 = _eval_expr_rec(expr[2]) + return ev2 if ev1 == "y" else \ + "m" if ev2 != "n" else \ + "n" + + if expr[0] == _OR: + ev1 = _eval_expr_rec(expr[1]) + if ev1 == "y": + # No need to look at expr[2] + return "y" + ev2 = _eval_expr_rec(expr[2]) + return ev2 if ev1 == "n" else \ + "y" if ev2 == "y" else \ + "m" + + if expr[0] == _NOT: + ev = _eval_expr_rec(expr[1]) + return "n" if ev == "y" else \ + "y" if ev == "n" else \ + "m" + + if expr[0] in _RELATIONS: + # Implements <, <=, >, >= comparisons as well. These were added to + # kconfig in 31847b67 (kconfig: allow use of relations other than + # (in)equality). + + # This mirrors the C tools pretty closely. Perhaps there's a more + # pythonic way to structure this. + + oper, op1, op2 = expr + op1_type, op1_str = _type_and_val(op1) + op2_type, op2_str = _type_and_val(op2) + + # If both operands are strings... + if op1_type == STRING and op2_type == STRING: + # ...then compare them lexicographically + comp = _strcmp(op1_str, op2_str) + else: + # Otherwise, try to compare them as numbers + try: + comp = int(op1_str, _TYPE_TO_BASE[op1_type]) - \ + int(op2_str, _TYPE_TO_BASE[op2_type]) + except ValueError: + # They're not both valid numbers. If the comparison is + # anything but = or !=, return 'n'. Otherwise, reuse + # _strcmp() to check for (in)equality. + if oper not in (_EQUAL, _UNEQUAL): + return "n" + comp = _strcmp(op1_str, op2_str) + + if oper == _EQUAL: res = comp == 0 + elif oper == _UNEQUAL: res = comp != 0 + elif oper == _LESS: res = comp < 0 + elif oper == _LESS_EQUAL: res = comp <= 0 + elif oper == _GREATER: res = comp > 0 + elif oper == _GREATER_EQUAL: res = comp >= 0 + + return "y" if res else "n" + + _internal_error("Internal error while evaluating expression: " + "unknown operation {}.".format(expr[0])) + + # # Internal classes # class _Feed(object): + """ + Class for working with sequences in a stream-like fashion; handy for + tokens. + """ - """Class for working with sequences in a stream-like fashion; handy for - tokens.""" - - # This would be more helpful on the item classes, but would remove some - # flexibility - __slots__ = ['items', 'length', 'i'] + __slots__ = ( + 'i', + 'length', + 'items', + ) def __init__(self, items): self.items = items self.length = len(self.items) self.i = 0 - def get_next(self): + def next(self): if self.i >= self.length: return None item = self.items[self.i] self.i += 1 return item - def peek_next(self): + def peek(self): return None if self.i >= self.length else self.items[self.i] def check(self, token): - """Check if the next token is 'token'. If so, remove it from the token - feed and return True. Otherwise, leave it in and return False.""" + """ + Checks if the next token is 'token'. If so, removes it from the token + feed and return True. Otherwise, leaves it in and return False. + """ if self.i < self.length and self.items[self.i] == token: self.i += 1 return True return False - def unget_all(self): - self.i = 0 - class _FileFeed(object): - - """Feeds lines from a file. Keeps track of the filename and current line + """ + Feeds lines from a file. Keeps track of the filename and current line number. Joins any line ending in \\ with the following line. We need to be - careful to get the line number right in the presence of continuation - lines.""" + careful to get the line number right in the presence of continuation lines. + """ - __slots__ = ['filename', 'lines', 'length', 'linenr'] + __slots__ = ( + 'filename', + 'lines', + 'length', + 'linenr' + ) - def __init__(self, filename): + def __init__(self, file_, filename): self.filename = filename - with open(filename) as f: + with file_: # No interleaving of I/O and processing yet. Don't know if it would # help. - self.lines = f.readlines() + self.lines = file_.readlines() self.length = len(self.lines) self.linenr = 0 - def get_next(self): + def next(self): if self.linenr >= self.length: return None line = self.lines[self.linenr] @@ -3368,73 +2975,61 @@ class _FileFeed(object): self.linenr += 1 return line - def peek_next(self): - linenr = self.linenr - if linenr >= self.length: + def next_no_join(self): + if self.linenr >= self.length: return None - line = self.lines[linenr] - while line.endswith("\\\n"): - linenr += 1 - line = line[:-2] + self.lines[linenr] + line = self.lines[self.linenr] + self.linenr += 1 return line - def unget(self): - self.linenr -= 1 - while self.lines[self.linenr].endswith("\\\n"): - self.linenr -= 1 - - def next_nonblank(self): - """Removes lines up to and including the next non-blank (not all-space) - line and returns it. Returns None if there are no more non-blank - lines.""" - while 1: - line = self.get_next() - if line is None or not line.isspace(): - return line - # # Internal functions # def _get_visibility(sc): - """Symbols and Choices have a "visibility" that acts as an upper bound on - the values a user can set for them, corresponding to the visibility in e.g. + """ + Symbols and Choices have a "visibility" that acts as an upper bound on the + values a user can set for them, corresponding to the visibility in e.g. 'make menuconfig'. This function calculates the visibility for the Symbol - or Choice 'sc' -- the logic is nearly identical.""" - if sc._cached_visibility is None: - vis = "n" - for _, cond_expr in sc._prompts: - vis = sc._config._eval_max(vis, cond_expr) - - if isinstance(sc, Symbol) and sc._is_choice_sym: - choice = sc._parent - if choice._type == TRISTATE and sc._type != TRISTATE and \ - choice.get_mode() != "y": - # Non-tristate choice symbols in tristate choices depend on the - # choice being in mode "y" - vis = "n" - elif sc._type == TRISTATE and vis == "m" and \ - choice.get_mode() == "y": - # Choice symbols with visibility "m" are not visible if the - # choice has mode "y" - vis = "n" - else: - vis = sc._config._eval_min(vis, _get_visibility(choice)) - - # Promote "m" to "y" if we're dealing with a non-tristate - if vis == "m" and sc._type != TRISTATE: - vis = "y" - - sc._cached_visibility = vis + or Choice 'sc' -- the logic is nearly identical. + """ + vis = "n" + + for node in sc.nodes: + if node.prompt: + vis = _eval_max(vis, node.prompt[1]) + + if isinstance(sc, Symbol) and sc.choice is not None: + if sc.choice._type == TRISTATE and sc._type != TRISTATE and \ + sc.choice.value != "y": + # Non-tristate choice symbols in tristate choices depend on the + # choice being in mode "y" + return "n" + + if sc._type == TRISTATE and vis == "m" and sc.choice.value == "y": + # Choice symbols with visibility "m" are not visible if the + # choice has mode "y" + return "n" + + vis = _eval_min(vis, sc.choice.visibility) + + # Promote "m" to "y" if we're dealing with a non-tristate. This might lead + # to infinite recursion if something really weird is done with MODULES, but + # it's not a problem in practice. + if vis == "m" and \ + (sc._type != TRISTATE or sc.config.modules.value == "n"): + return "y" - return sc._cached_visibility + return vis def _make_and(e1, e2): - """Constructs an _AND (&&) expression. Performs trivial simplification. - Nones equate to 'y'. + """ + Constructs an _AND (&&) expression. Performs trivial simplification. Nones + equate to 'y'. Returns None if e1 == e2 == None, so that ANDing two nonexistent - expressions gives a nonexistent expression.""" + expressions gives a nonexistent expression. + """ if e1 is None or e1 == "y": return e2 if e2 is None or e2 == "y": @@ -3442,9 +3037,11 @@ def _make_and(e1, e2): return (_AND, e1, e2) def _make_or(e1, e2): - """Constructs an _OR (||) expression. Performs trivial simplification and + """ + Constructs an _OR (||) expression. Performs trivial simplification and avoids Nones. Nones equate to 'y', which is usually what we want, but needs - to be kept in mind.""" + to be kept in mind. + """ # Perform trivial simplification and avoid None's (which # correspond to y's) @@ -3454,8 +3051,26 @@ def _make_or(e1, e2): return e2 return (_OR, e1, e2) +def _eval_min(e1, e2): + """ + Returns the minimum value of the two expressions. Equates None with 'y'. + """ + e1_eval = eval_expr(e1) + e2_eval = eval_expr(e2) + return e1_eval if tri_less(e1_eval, e2_eval) else e2_eval + +def _eval_max(e1, e2): + """ + Returns the maximum value of the two expressions. Equates None with 'y'. + """ + e1_eval = eval_expr(e1) + e2_eval = eval_expr(e2) + return e1_eval if tri_greater(e1_eval, e2_eval) else e2_eval + def _expr_syms_rec(expr, res): - """_expr_syms() helper. Recurses through expressions.""" + """ + _expr_syms() helper. Recurses through expressions. + """ if isinstance(expr, Symbol): res.append(expr) elif isinstance(expr, str): @@ -3475,29 +3090,37 @@ def _expr_syms_rec(expr, res): "expression with token stream {}.".format(expr)) def _expr_syms(expr, res): - """append()s the symbols in 'expr' to 'res'. Does not remove duplicates.""" + """ + append()s the symbols in 'expr' to 'res'. Does not remove duplicates. + """ if expr is not None: _expr_syms_rec(expr, res) def _str_val(obj): - """Returns the value of obj as a string. If obj is not a string (constant - symbol), it must be a Symbol.""" - return obj if isinstance(obj, str) else obj.get_value() + """ + Returns the value of obj as a string. If obj is not a string (constant + symbol), it must be a Symbol. + """ + return obj if isinstance(obj, str) else obj.value def _format_and_op(expr): - """_expr_to_str() helper. Returns the string representation of 'expr', - which is assumed to be an operand to _AND, with parentheses added if - needed.""" + """ + _expr_to_str() helper. Returns the string representation of 'expr', which + is assumed to be an operand to _AND, with parentheses added if needed. + """ if isinstance(expr, tuple) and expr[0] == _OR: return "({})".format(_expr_to_str(expr)) return _expr_to_str(expr) def _expr_to_str(expr): if isinstance(expr, str): + if expr in ("n", "m", "y"): + # Don't print spammy quotes for these + return expr return '"{}"'.format(expr) if isinstance(expr, Symbol): - return expr._name + return expr.name if expr[0] == _NOT: if isinstance(expr[1], (str, Symbol)): @@ -3518,21 +3141,27 @@ def _expr_to_str(expr): _expr_to_str(expr[2])) def _type_and_val(obj): - """Helper to hack around the fact that we don't represent plain strings as - Symbols. Takes either a plain string or a Symbol and returns a - (, ) tuple.""" - return (obj._type, obj.get_value()) \ + """ + Helper to hack around the fact that we don't represent plain strings as + Symbols. Takes either a plain string or a Symbol and returns a (, + ) tuple. + """ + return (obj._type, obj.value) \ if not isinstance(obj, str) \ else (STRING, obj) def _indentation(line): - """Returns the length of the line's leading whitespace, treating tab stops - as being spaced 8 characters apart.""" + """ + Returns the length of the line's leading whitespace, treating tab stops as + being spaced 8 characters apart. + """ line = line.expandtabs() return len(line) - len(line.lstrip()) def _deindent(line, indent): - """Deindent 'line' by 'indent' spaces.""" + """ + Deindents 'line' by 'indent' spaces. + """ line = line.expandtabs() if len(line) <= indent: return line @@ -3546,12 +3175,16 @@ def _is_base_n(s, n): return False def _strcmp(s1, s2): - """strcmp()-alike that returns -1, 0, or 1.""" + """ + strcmp()-alike that returns -1, 0, or 1. + """ return (s1 > s2) - (s1 < s2) def _lines(*args): - """Returns a string consisting of all arguments, with newlines inserted - between them.""" + """ + Returns a string consisting of all arguments, with newlines inserted + between them. + """ return "\n".join(args) def _stderr_msg(msg, filename, linenr): @@ -3561,26 +3194,313 @@ def _stderr_msg(msg, filename, linenr): def _tokenization_error(s, filename, linenr): loc = "" if filename is None else "{}:{}: ".format(filename, linenr) - raise Kconfig_Syntax_Error("{}Couldn't tokenize '{}'" + raise KconfigSyntaxError("{}Couldn't tokenize '{}'" .format(loc, s.strip())) def _parse_error(s, msg, filename, linenr): loc = "" if filename is None else "{}:{}: ".format(filename, linenr) - raise Kconfig_Syntax_Error("{}Couldn't parse '{}'{}" - .format(loc, s.strip(), - "." if msg is None else ": " + msg)) + raise KconfigSyntaxError("{}Couldn't parse '{}'{}" + .format(loc, s.strip(), + "." if msg is None else ": " + msg)) def _internal_error(msg): - raise Internal_Error( + raise InternalError( msg + "\nSorry! You may want to send an email to ulfalizer a.t Google's " "email service to tell me about this. Include the message above and " "the stack trace and describe what you were doing.") +# Printing functions + +def _sym_choice_str(sc): + """ + Symbol/choice __str__() implementation. These have many properties in + common, so it makes sense to handle them together. + """ + lines = [] + + def indent_add(s): + lines.append("\t" + s) + + # We print the prompt(s) and help text(s) too as a convenience, even though + # they're actually part of the menu node. If a symbol or choice is defined + # in multiple locations (has more than one menu node), we output one + # statement for each location, and print all the properties that belong to + # the symbol/choice itself only at the first location. This gives output + # that would function if fed to a Kconfig parser, even for such + # symbols/choices (choices defined in multiple locations gets iffy since + # they also have child nodes, but I've never seen such a choice). + + if not sc.nodes: + return "" + + for node in sc.nodes: + if isinstance(sc, Symbol): + if node.is_menuconfig: + lines.append("menuconfig " + sc.name) + else: + lines.append("config " + sc.name) + else: + if sc.name is None: + lines.append("choice") + else: + lines.append("choice " + sc.name) + + if node is sc.nodes[0] and sc.type != UNKNOWN: + indent_add(_TYPENAME[sc.type]) + + if node.prompt is not None: + prompt_str = 'prompt "{}"'.format(node.prompt[0]) + if node.prompt[1] is not None: + prompt_str += " if " + _expr_to_str(node.prompt[1]) + indent_add(prompt_str) + + if node is sc.nodes[0]: + if isinstance(sc, Symbol): + if sc.is_allnoconfig_y: + indent_add("option allnoconfig_y") + if sc is sc.config.defconfig_list: + indent_add("option defconfig_list") + if sc.env_var is not None: + indent_add('option env="{}"'.format(sc.env_var)) + if sc is sc.config.modules: + indent_add("option modules") + + if isinstance(sc, Symbol): + for range_ in sc.ranges: + range_string = "range {} {}" \ + .format(_expr_to_str(range_[0]), + _expr_to_str(range_[1])) + if range_[2] is not None: + range_string += " if " + _expr_to_str(range_[2]) + indent_add(range_string) + + for default in sc.defaults: + default_string = "default " + _expr_to_str(default[0]) + if default[1] is not None: + default_string += " if " + _expr_to_str(default[1]) + indent_add(default_string) + + if isinstance(sc, Choice) and sc.is_optional: + indent_add("optional") + + if isinstance(sc, Symbol): + for select in sc.selects: + select_string = "select " + select[0].name + if select[1] is not None: + select_string += " if " + _expr_to_str(select[1]) + indent_add(select_string) + + for imply in sc.implies: + imply_string = "imply " + imply[0].name + if imply[1] is not None: + imply_string += " if " + _expr_to_str(imply[1]) + indent_add(imply_string) + + if node.help is not None: + indent_add("help") + for line in node.help.splitlines(): + indent_add(" " + line) + + # Add a blank line if there are more nodes to print + if node is not sc.nodes[-1]: + lines.append("") + + return "\n".join(lines) + "\n" + +# Menu manipulation + +def _eq_to_sym(eq): + """ + _expr_depends_on() helper. For (in)equalities of the form sym = y/m or + sym != n, returns sym. For other (in)equalities, returns None. + """ + relation, left, right = eq + + # Make sure the symbol (if any) appears to the left + if not isinstance(left, Symbol): + left, right = right, left + if not isinstance(left, Symbol): + return None + if (relation == _EQUAL and right in ("m", "y")) or \ + (relation == _UNEQUAL and right == "n"): + return left + return None + +def _expr_depends_on(expr, sym): + """ + Reimplementation of expr_depends_symbol() from mconf.c. Used to + determine if a submenu should be implicitly created, which influences + what items inside choice statements are considered choice items. + """ + if expr is None: + return False + + def rec(expr): + if isinstance(expr, str): + return False + if isinstance(expr, Symbol): + return expr is sym + + if expr[0] in (_EQUAL, _UNEQUAL): + return _eq_to_sym(expr) is sym + if expr[0] == _AND: + return rec(expr[1]) or rec(expr[2]) + return False + + return rec(expr) + +def _has_auto_menu_dep(node1, node2): + """ + Returns True if node2 has an "automatic menu dependency" on node1. If node2 + has a prompt, we check its condition. Otherwise, we look directly at + node2.dep. + """ + + if node2.prompt: + return _expr_depends_on(node2.prompt[1], node1.item) + + # If we have no prompt, use the menu node dependencies instead + return node2.dep is not None and \ + _expr_depends_on(node2.dep, node1.item) + +def _check_auto_menu(node): + """ + Looks for menu nodes after 'node' that depend on it. Creates an implicit + menu rooted at 'node' with the nodes as the children if such nodes are + found. The recursive call to _finalize_tree() makes this work recursively. + """ + + cur = node + while cur.next is not None and \ + _has_auto_menu_dep(node, cur.next): + _finalize_tree(cur.next) + cur = cur.next + cur.parent = node + + if cur is not node: + node.list = node.next + node.next = cur.next + cur.next = None + +def _flatten(node): + """ + "Flattens" menu nodes without prompts (e.g. 'if' nodes and non-visible + symbols with children from automatic menu creation) so that their children + appear after them instead. This gives a clean menu structure with no + unexpected "jumps" in the indentation. + """ + + while node is not None: + if node.list is not None and \ + (node.prompt is None or node.prompt == ""): + + last_node = node.list + while 1: + last_node.parent = node.parent + if last_node.next is None: + break + last_node = last_node.next + + last_node.next = node.next + node.next = node.list + node.list = None + + node = node.next + +def _remove_if(node): + """ + Removes 'if' nodes (which can be recognized by MenuNode.item being None), + which are assumed to already have been flattened. The C implementation + doesn't bother to do this, but we expose the menu tree directly, and it + makes it nicer to work with. + """ + first = node.list + while first is not None and first.item is None: + first = first.next + + cur = first + while cur is not None: + if cur.next is not None and cur.next.item is None: + cur.next = cur.next.next + cur = cur.next + + node.list = first + +def _finalize_choice(node): + """ + Finalizes a choice, marking each symbol whose menu node has the choice as + the parent as a choice symbol, and automatically determining types if not + specified. + """ + choice = node.item + + cur = node.list + while cur is not None: + if isinstance(cur.item, Symbol): + cur.item.choice = choice + choice.syms.append(cur.item) + cur = cur.next + + # If no type is specified for the choice, its type is that of + # the first choice item with a specified type + if choice._type == UNKNOWN: + for item in choice.syms: + if item._type != UNKNOWN: + choice._type = item._type + break + + # Each choice item of UNKNOWN type gets the type of the choice + for item in choice.syms: + if item._type == UNKNOWN: + item._type = choice._type + +def _finalize_tree(node): + """ + Creates implicit menus from dependencies (see kconfig-language.txt), + removes 'if' nodes, and finalizes choices. This pretty closely mirrors + menu_finalize() from the C implementation, though we propagate dependencies + during parsing instead. + """ + + # The ordering here gets a bit tricky, but it's important to do things in + # this order to have everything work out correctly. + + if node.list is not None: + # The menu node has children. Finalize them. + cur = node.list + while cur is not None: + _finalize_tree(cur) + # Note: _finalize_tree() might have changed cur.next. This is + # expected, so that we jump over e.g. implicitly created submenus. + cur = cur.next + + elif node.item is not None: + # The menu node has no children (yet). See if we can create an implicit + # menu rooted at it (due to menu nodes after it depending on it). + _check_auto_menu(node) + + if node.list is not None: + # We have a node with finalized children. Do final steps to finalize + # this node. + _flatten(node.list) + _remove_if(node) + + # Empty choices (node.list None) are possible, so this needs to go outside + if isinstance(node.item, Choice): + _finalize_choice(node) + # # Public global constants # +# Integers representing menu and comment nodes + +( + MENU, + COMMENT, +) = range(2) + # Integers representing symbol types ( BOOL, @@ -3625,6 +3545,7 @@ def _internal_error(msg): _T_LESS_EQUAL, _T_MAINMENU, _T_MENU, + _T_MENUCONFIG, _T_MODULES, _T_NOT, _T_ON, @@ -3640,7 +3561,7 @@ def _internal_error(msg): _T_TRISTATE, _T_UNEQUAL, _T_VISIBLE, -) = range(43) +) = range(44) # Keyword to token map. Note that the get() method is assigned directly as a # small optimization. @@ -3667,13 +3588,7 @@ _get_keyword = { "int": _T_INT, "mainmenu": _T_MAINMENU, "menu": _T_MENU, - - # 'menuconfig' only deals with presentation in the configuration interface - # and doesn't affect evaluation semantics, so treat it the same as - # 'config'. Perhaps some presentation-related support could be added as - # well. - "menuconfig": _T_CONFIG, - + "menuconfig": _T_MENUCONFIG, "modules": _T_MODULES, "on": _T_ON, "option": _T_OPTION, @@ -3687,12 +3602,10 @@ _get_keyword = { "visible": _T_VISIBLE, }.get -# Tokens after which identifier-like lexemes are treated as strings, plus -# _T_CONFIG. This allows us to quickly check if we have a symbol reference (as -# opposed to a definition or something else) when tokenizing. -_NOT_REF = frozenset(( +# Tokens after which identifier-like lexemes are treated as strings. _T_CHOICE +# is included to avoid symbols being registered for named choices. +_STRING_LEX = frozenset(( _T_BOOL, - _T_CONFIG, _T_CHOICE, _T_COMMENT, _T_HEX, @@ -3761,8 +3674,10 @@ _DEFAULT_VALUE = { STRING: "", } -# Indicates that no item is selected in a choice statement -_NO_SELECTION = 0 +# Constant representing that there's no cached choice selection. This is +# distinct from a cached None (no selection). We create a unique object (any +# will do) for it so we can test with 'is'. +_NO_CACHED_SELECTION = object() # Integers representing expression types ( @@ -3806,7 +3721,7 @@ _RELATIONS = frozenset(( )) # Token to relation (=, !=, <, ...) mapping -_TOKEN_TO_RELATION = { +_TOKEN_TO_REL = { _T_EQUAL: _EQUAL, _T_GREATER: _GREATER, _T_GREATER_EQUAL: _GREATER_EQUAL, diff --git a/makefile.patch b/makefile.patch index e8c4425..aefb021 100644 --- a/makefile.patch +++ b/makefile.patch @@ -38,7 +38,7 @@ index d9b1fef..e6311ed 100644 + "import kconfiglib; \ + import sys; \ + c = kconfiglib.Config(sys.argv[4 if \"$(PYTHONCMD)\".startswith(\"ipython\") else 1]); \ -+ print(\"A Config instance 'c' for the architecture ({0}) has been created.\".format(c.get_arch()))" \ ++ print(\"A Config instance 'c' for the architecture has been created.\")" \ + $(srctree)/$(Kconfig) + +# Used by testsuite.py to be able to compare output for nonsensical diff --git a/tests/Kchoice b/tests/Kchoice index e80e222..f635ccc 100644 --- a/tests/Kchoice +++ b/tests/Kchoice @@ -141,17 +141,39 @@ choice WEIRD_SYMS # Only WS1 is part of the choice config WS1 + bool "WS1" + config WS2 + bool "WS2" depends on WS1 + config WS3 + bool depends on WS2 + config WS4 + bool depends on WS1 -# 'if' has the same effect, so only WS5 is part of the choice config WS5 -if WS5 + bool "WS5" if WS1 + +# 'if' has the same effect, so only WS6 is part of the choice config WS6 + bool "WS6" + +if WS6 + +config WS7 + bool + +config WS8 + bool "WS8" + endif +# Should also be part of the choice +config WS9 + bool "WS9" + endchoice diff --git a/tests/Kdefconfig_existent b/tests/Kdefconfig_existent index 895c218..8dde443 100644 --- a/tests/Kdefconfig_existent +++ b/tests/Kdefconfig_existent @@ -2,6 +2,7 @@ # Should produce "Kconfiglib/tests/defconfig_2" config FOO + string option env="BAR" config A diff --git a/tests/Keval b/tests/Keval index 64bd4d8..4f63247 100644 --- a/tests/Keval +++ b/tests/Keval @@ -1,6 +1,7 @@ # Enabled/disabled in the test config MODULES bool "modules" + option modules config N def_tristate n diff --git a/tests/Klocation b/tests/Klocation index 404e5ae..498a372 100644 --- a/tests/Klocation +++ b/tests/Klocation @@ -1,73 +1,30 @@ -# Include some line continuations to make sure they don't mess up line numbers +if UNDEFINED +endif -# Defined and referenced in multiple locations -config A - bool +config SINGLE_DEF + +config MULTI_DEF # Throw in some line continuations too to make sure it doesn't mess up the line # numbers -menu "menu 1" - depends on A - visible if A && \ - NOT_DEFINED - -# Also defined in Klocation_included -choice B - bool "b" if A - -config C - bool "c" - -config D - bool "d" - -\ - -endchoice +if y && \ + y +if y && \ + y && \ + y -config A - def_bool NOT_DEFINED +config MULTI_DEF -comment "comment 1" +endif +endif -config E - bool "E" if A - depends on A - -endmenu - -config \ - FOO +config EXPANDED_FROM_ENV string - option \ - env\ - =\ - "FOO" - -\ -\ + option env="EXPANDED_FROM_ENV" -config BAR +config _INCLUDED string - default \ - "_included" + default "_included" # Expands to "tests/Klocation_included" -source \ -"$FOO/Klocation$BAR" - -\ -\ -\ - -config I - int - range A 0 - range 0 A - range 0 1 if A - default J if A < 0 - default K if 0 < A - default L if 0 <= A - default M if 0 > A - default N if 0 >= A - default N if y && 0 < A +source "$EXPANDED_FROM_ENV/Klocation$_INCLUDED" diff --git a/tests/Klocation_included b/tests/Klocation_included index 674116a..fb1afaa 100644 --- a/tests/Klocation_included +++ b/tests/Klocation_included @@ -1,42 +1,15 @@ -menuconfig A - bool "A" -menuconfig A - bool "A" -menu "menu 2" -config M - def_bool !(n || (n || A = n)) - default !A if y - default y if !A -config S - bool - select A if NOT_DEFINED = y - select E if A - imply A - imply E if A -endmenu -choice - bool "C" -config N - bool "N" -config O - bool "O" -endchoice -choice B - bool "B" -config B1 - tristate "B1" -config B2 - tristate "B2" +config MULTI_DEF +choice CHOICE endchoice -if !(NOT_DEFINED != A) -comment "comment 2" -endif +config MENU_HOOK -menu "visible if menu" - visible if A - visible if NOT_DEFINED +menu "menu" endmenu + +config COMMENT_HOOK + +comment "comment" diff --git a/tests/Kmisc b/tests/Kmisc index d2e55b2..c3a21f8 100644 --- a/tests/Kmisc +++ b/tests/Kmisc @@ -1,6 +1,6 @@ # For testing various minor APIs -# is_optional() +# optional choices choice NOT_OPTIONAL bool "not optional" diff --git a/tests/Kmodifiable b/tests/Kmodifiable deleted file mode 100644 index 1f9dcc9..0000000 --- a/tests/Kmodifiable +++ /dev/null @@ -1,50 +0,0 @@ -config MODULES - def_bool y - -config VISIBLE - tristate "bool visible" - -config NOT_VISIBLE - tristate - -config Y_SELECTOR - def_tristate y - select SELECTED_TO_Y - -config SELECTED_TO_Y - tristate "selected to y" - -config M_SELECTOR - def_tristate m - select TRISTATE_SELECTED_TO_M - select M_VISIBLE_TRISTATE_SELECTED_TO_M - select BOOL_SELECTED_TO_M - -config TRISTATE_SELECTED_TO_M - tristate "tristate selected to m" - -config M_VISIBLE_TRISTATE_SELECTED_TO_M - tristate "m-visible tristate selected to m" - depends on m - -# The "m" will get promoted to a "y", so this should still not be modifiable -config BOOL_SELECTED_TO_M - bool "bool selected to m" - -config VISIBLE_STRING - string "visible string" - -config VISIBLE_INT - int "visible int" - -config VISIBLE_HEX - hex "visible hex" - -config NOT_VISIBLE_STRING - string - -config NOT_VISIBLE_INT - int "not visible int" if n - -config NOT_VISIBLE_HEX - hex diff --git a/tests/Kprompt b/tests/Kprompt deleted file mode 100644 index a36a0fd..0000000 --- a/tests/Kprompt +++ /dev/null @@ -1,77 +0,0 @@ -config NO_PROMPT - tristate - -config SINGLE_PROMPT_1 - bool "single prompt 1" - -config SINGLE_PROMPT_2 - bool - prompt "single prompt 2" if n - -config MULTI_PROMPT - bool "ignored prompt" - prompt "prompt 1" -config MULTI_PROMPT - bool "prompt 2" -config MULTI_PROMPT - bool -config MULTI_PROMPT - bool - prompt "prompt 3" -config MULTI_PROMPT - bool - prompt "ignored prompt" - prompt "ignored prompt 2" if y - prompt "prompt 4" if y - -choice NO_PROMPT_CHOICE - tristate -config A - bool -config B - bool -endchoice - -choice SINGLE_PROMPT_1_CHOICE - bool "single prompt 1 choice" -config C - bool -config D - bool -endchoice - -choice SINGLE_PROMPT_2_CHOICE - bool "ignored prompt" - prompt "single prompt 2 choice" -config E - bool -config F - bool -endchoice - -choice MULTI_PROMPT_CHOICE - bool "prompt 1 choice" -config G - bool -config H - bool -endchoice - -choice MULTI_PROMPT_CHOICE - bool - prompt "prompt 2 choice" -config I - bool -config J - bool -endchoice - -choice MULTI_PROMPT_CHOICE - bool - prompt "ignored prompt" - prompt "prompt 3 choice" -config K - bool -config L - bool -endchoice diff --git a/tests/Krelation b/tests/Krelation index 057b585..420152f 100644 --- a/tests/Krelation +++ b/tests/Krelation @@ -1,5 +1,6 @@ config A bool + depends on UNDEFINED choice bool "C" @@ -18,6 +19,7 @@ menu "m2" config F bool choice + tristate "foo" config G bool "g" config H diff --git a/tests/Krepr b/tests/Krepr new file mode 100644 index 0000000..d886fe3 --- /dev/null +++ b/tests/Krepr @@ -0,0 +1,61 @@ +config MODULES + bool + option modules + default y + +if UNDEFINED +endif + +config BASIC + bool + default y + help + +config VISIBLE + bool "visible" + +config DIR_DEP_N + depends on n + +config OPTIONS + option allnoconfig_y + option defconfig_list + option env="ENV" + +config MULTI_DEF +config MULTI_DEF + +menuconfig MENUCONFIG + +choice CHOICE + tristate "choice" + +config CHOICE_1 + tristate "choice sym" + +config CHOICE_2 + tristate "choice sym" + +endchoice + +config CHOICE_HOOK + +choice + tristate "choice" if n + optional +endchoice + +config NO_VISIBLE_IF_HOOK + +menu "no visible if" +endmenu + +config VISIBLE_IF_HOOK + +menu "visible if" + visible if m +endmenu + +config COMMENT_HOOK + +comment "comment" diff --git a/tests/Kstr b/tests/Kstr new file mode 100644 index 0000000..8d74017 --- /dev/null +++ b/tests/Kstr @@ -0,0 +1,76 @@ +if UNDEFINED +endif + +config NO_TYPE + +config BASIC_NO_PROMPT + bool + help + blah blah + + blah blah blah + + blah + +config BASIC_PROMPT + bool "basic" + +config ADVANCED + tristate "prompt" if DEP + default DEFAULT_1 + default DEFAULT_2 if DEP + select SELECTED_1 + select SELECTED_2 if DEP + imply IMPLIED_1 + imply IMPLIED_2 if DEP + help + first help text + +config ADVANCED + prompt "prompt 2" + +menuconfig ADVANCED + prompt "prompt 3" + depends on DEP2 + +config ADVANCED + help + second help text + +config STRING + string + default "foo" + default "bar" if DEP + default STRING2 + default STRING3 if DEP + +config INT + int + range 1 2 + range FOO BAR + range BAZ QAZ if DEP + +config MODULES + option modules + +config OPTIONS + option allnoconfig_y + option defconfig_list + option env="ENV" + +choice CHOICE + tristate "foo" + default CHOICE_1 + default CHOICE_2 if dep + +config CHOICE_1 + tristate "choice 1" + +config CHOICE_2 + tristate "choice 2" + +endchoice + +choice + tristate "no name" +endchoice diff --git a/tests/Ktext b/tests/Ktext deleted file mode 100644 index 3440c3c..0000000 --- a/tests/Ktext +++ /dev/null @@ -1,145 +0,0 @@ -config BASIC - bool - -if !BASIC && !BASIC - -config ADVANCED - tristate "advanced prompt 1" if y || (BASIC && BASIC) - select SELECTED_1 if BASIC && DUMMY - select SELECTED_2 if !(DUMMY || BASIC) - imply IMPLIED_1 if BASIC || DUMMY - imply IMPLIED_2 if !(DUMMY && BASIC) - default y if BASIC && !BASIC - default n if BASIC = DUMMY && X < Y && X <= Y && X > Y && X >= Y - -config ADVANCED - tristate "advanced prompt 2" - -config STRING - string - default "foo" - default "bar" if BAR - default STRING2 if BAZ - -config STRING2 - string - default "baz" - -endif - -config SELECTED_1 -config SELECTED_2 -config SELECTING_1 - select ADVANCED if BASIC -config SELECTING_2 - select ADVANCED if !BASIC -config IMPLYING_1 - imply ADVANCED if DUMMY -config IMPLYING_2 - imply ADVANCED if !DUMMY - -config INT - int - default 7 - -config HAS_RANGES - int "ranged" - range 1 2 if !DUMMY - range INT INT if DUMMY - range 123 456 - -choice - bool "choice" - -config CHOICE_ITEM_1 - bool "A" -config CHOICE_ITEM_2 - bool "B" -config CHOICE_ITEM_3 - bool "C" - -endchoice - -menu "simple menu" -endmenu - -if !DUMMY -menu "advanced menu" - depends on !BASIC - visible if !DUMMY -endmenu -endif - -comment "simple comment" - -if !DUMMY - comment "advanced comment" - depends on !BASIC -endif - -config NO_HELP - bool - -choice NO_HELP_CHOICE -config FOO -endchoice - -config EMPTY_HELP - bool - help -config DUMMY - -choice EMPTY_HELP_CHOICE - bool - help -config DUMMY2 -endchoice - -config S - bool - help - help for - S -choice C - bool - help - help for - C -config A - bool "A" - -config B - bool "B" - -endchoice - -comment "a comment" - -menu "a menu" -endmenu - -config HELP_TERMINATED_BY_COMMENT - bool - help - a - b - c -# - -config TRICKY_HELP - bool - help - - - a - b - c - - d - e - f - - - g - h - i diff --git a/tests/Kvisibility b/tests/Kvisibility index 715d098..91def0a 100644 --- a/tests/Kvisibility +++ b/tests/Kvisibility @@ -1,5 +1,6 @@ config MODULES bool "MODULES" + option modules # # Symbol visibility @@ -12,186 +13,204 @@ config NO_PROMPT config MOD def_tristate m -config BOOL_n +config BOOL_N bool "bool n" if n -# Rewritten to m && MODULES -config BOOL_m +config BOOL_M + # Rewritten to m && MODULES bool "bool m" if m -# Not rewritten config BOOL_MOD bool "bool MOD" + # Not rewritten depends on MOD -# Rewritten to m && MODULES -config BOOL_y +config BOOL_Y bool "bool y" + # Rewritten to m && MODULES depends on y || m -config TRISTATE_n +config TRISTATE_N tristate "tristate n" if n -# Rewritten to m && MODULES -config TRISTATE_m +config TRISTATE_M + # Rewritten to m && MODULES tristate "tristate m" if m -# Not rewritten config TRISTATE_MOD tristate "tristate MOD" + # Not rewritten depends on MOD -# Rewritten to m && MODULES -config TRISTATE_y +config TRISTATE_Y bool "tristate y" + # Rewritten to m && MODULES depends on y || m # Symbols nested in 'if' if n -config BOOL_if_n + +config BOOL_IF_N bool "bool if n" -config TRISTATE_if_n + +config TRISTATE_IF_N tristate "tristate if n" + endif if m -config BOOL_if_m + +config BOOL_IF_M bool "bool if m" -config TRISTATE_if_m + +config TRISTATE_IF_M tristate "tristate if n" + endif if y -config BOOL_if_y + +config BOOL_IF_Y bool "bool if y" -config TRISTATE_if_y + +config TRISTATE_IF_Y tristate "tristate if y" + endif # Symbols nested in 'menu' menu "menu 1" depends on n -config BOOL_menu_n + +config BOOL_MENU_N bool "bool menu n" -config TRISTATE_menu_n + +config TRISTATE_MENU_N tristate "tristate menu n" + endmenu menu "menu 2" depends on m -config BOOL_menu_m + +config BOOL_MENU_M bool "bool menu m" -config TRISTATE_menu_m + +config TRISTATE_MENU_M tristate "tristate menu n" + endmenu menu "menu 3" depends on y -config BOOL_menu_y + +config BOOL_MENU_Y bool "bool menu y" -config TRISTATE_menu_y + +config TRISTATE_MENU_Y tristate "tristate menu y" + endmenu # Symbols nested in choices choice C1 tristate "choice n" if n -config BOOL_choice_n + +config BOOL_CHOICE_N bool "bool choice n" -config TRISTATE_choice_n + +config TRISTATE_CHOICE_N tristate "tristate choice n" + endchoice choice C2 tristate "choice m" if m -config BOOL_choice_m + +config BOOL_CHOICE_M bool "bool choice m" -config TRISTATE_choice_m + +config TRISTATE_CHOICE_M tristate "tristate choice n" + endchoice choice C3 tristate "choice y" if y -config BOOL_choice_y + +config BOOL_CHOICE_Y bool "bool choice y" -config TRISTATE_choice_y + +config TRISTATE_CHOICE_Y tristate "tristate choice y" + endchoice # # Choice visibility # -choice BOOL_CHOICE_n +choice BOOL_CHOICE_N bool "bool choice n" if n -config A - bool "A" -config B - bool "B" endchoice -choice BOOL_CHOICE_m +choice BOOL_CHOICE_M bool "bool choice m" if m -config C - bool "C" -config D - bool "D" endchoice -choice BOOL_CHOICE_y +choice BOOL_CHOICE_Y bool "bool choice y" if y -config E - bool "E" -config F - bool "F" endchoice -choice TRISTATE_CHOICE_n +choice TRISTATE_CHOICE_N tristate "tristate choice n" if n -config G - tristate "G" -config H - tristate "H" endchoice -choice TRISTATE_CHOICE_m +choice TRISTATE_CHOICE_M tristate "tristate choice m" if m -config I - tristate "I" -config J - tristate "J" endchoice -choice TRISTATE_CHOICE_y +choice TRISTATE_CHOICE_Y tristate "tristate choice y" if y + config K tristate "K" + config L tristate "L" + endchoice if m -choice TRISTATE_CHOICE_IF_m_and_y +choice TRISTATE_CHOICE_IF_M_AND_Y tristate "tristate choice if m and y" if y + config M bool "M" + config N bool "N" + endchoice endif menu "choice-containing menu" depends on n && y -choice TRISTATE_CHOICE_MENU_n_and_y + +choice TRISTATE_CHOICE_MENU_N_AND_Y tristate "tristate choice if n and y" + config O tristate "O" + config P tristate "P" + endchoice + endmenu # @@ -245,19 +264,25 @@ comment "comment y" if n comment "comment if n" endif + if m comment "comment if m" endif + if y comment "comment if y" endif if "y" -menu "comment-containing menu" + +menu "menu with comment" depends on m + comment "double-nested m comment" depends on y + endmenu + endif # Used to verify that string/int/hex symbols with m visibility accept a user @@ -280,28 +305,38 @@ endif menu "n-visible menu" visible if n -config VISIBLE_IF_n + +config VISIBLE_IF_N tristate "visible if n" + endmenu menu "m-visible menu" visible if m -config VISIBLE_IF_m + +config VISIBLE_IF_M tristate "visible if m" + endmenu menu "y-visible menu" visible if y -config VISIBLE_IF_y + +config VISIBLE_IF_Y tristate "visible if m" + endmenu menu "m-visible menu 2" visible if y || n visible if m && y visible if y + if y -config VISIBLE_IF_m_2 + +config VISIBLE_IF_M_2 tristate "visible if m 2" + endif + endmenu diff --git a/testsuite.py b/testsuite.py index 11822ce..69eae6e 100644 --- a/testsuite.py +++ b/testsuite.py @@ -1,45 +1,40 @@ -# This is a test suite for Kconfiglib. It runs selftests on Kconfigs provided -# by us and tests compatibility with the C Kconfig implementation by comparing -# the output of Kconfiglib with the output of the scripts/kconfig/*conf -# utilities for different targets and defconfigs. It should be run from the -# top-level kernel directory with +# This is the Kconfiglib test suite. It runs selftests on Kconfigs provided by +# us and tests compatibility with the C Kconfig implementation by comparing the +# output of Kconfiglib with the output of the scripts/kconfig/*conf utilities +# for different targets and defconfigs. It should be run from the top-level +# kernel directory with # -# $ python Kconfiglib/testsuite.py +# $ python Kconfiglib/testsuite.py # -# Some additional options can be turned on by passing arguments. With no argument, -# they default to off. +# Some additional options can be turned on by passing them as arguments. They +# default to off. # # - speedy: -# Run scripts/kconfig/conf directly when comparing outputs instead of using -# 'make' targets. Makes things a lot faster, but could break if Kconfig -# files start depending on additional environment variables besides ARCH and -# SRCARCH. (These would be set in the Makefiles in that case.) Safe as of -# Linux 4.1.0-rc8. +# Run scripts/kconfig/conf directly instead of using 'make' targets. Makes +# things a lot faster, but could break if Kconfig files start referencing +# additional environment variables beyond ARCH, SRCARCH, and KERNELVERSION. +# Safe as of Linux 4.14-rc3. # # - obsessive: -# By default, only valid arch/defconfig pairs will be tested. With this -# enabled, every arch will be tested with every defconfig, which increases -# the test time by an order of magnitude. Occasionally finds (usually very -# obscure) bugs, and I make sure everything passes with it. +# By default, only valid arch/defconfig pairs are tested. In obsessive mode, +# every arch will be tested with every defconfig. Increases the testing time +# by an order of magnitude. Occasionally finds (usually obscure) bugs, and I +# make sure everything passes with it. # # - log: -# Log timestamped failures of the defconfig test to test_defconfig_fails in -# the root. Especially handy in obsessive mode. +# Log timestamped defconfig test failures to the file test_defconfig_fails. +# Handy in obsessive mode. # -# For example, to run in speedy mode with logging, run +# For example, this commands runs the test suite in speedy mode with logging +# enabled: # -# $ python Kconfiglib/testsuite.py speedy log +# $ python(3) Kconfiglib/testsuite.py speedy log # -# (PyPy also works, and runs the defconfig tests roughly 20% faster on my -# machine. Some of the other tests get an even greater speed-up.) -# -# The tests have been roughly arranged in order of time needed. +# pypy works too, and runs most tests much faster than CPython. # # All tests should pass. Report regressions to ulfalizer a.t Google's email # service. -from __future__ import print_function - import difflib import errno import kconfiglib @@ -51,101 +46,179 @@ import sys import textwrap import time -speedy_mode = False -obsessive_mode = False -log_mode = False +def shell(cmd): + with open(os.devnull, "w") as devnull: + subprocess.call(cmd, shell=True, stdout=devnull, stderr=devnull) + +all_passed = True + +def fail(msg=None): + global all_passed + all_passed = False + if msg is not None: + print("Fail: " + msg) + +def verify(cond, msg): + if not cond: + fail(msg) + +def verify_equal(x, y): + if x != y: + fail("'{}' does not equal '{}'".format(x, y)) # Assign this to avoid warnings from Kconfiglib. Nothing in the kernel's # Kconfig files seems to actually look at the value as of 3.7.0-rc8. This is # only relevant for the test suite, as this will get set by the kernel Makefile # when using (i)scriptconfig. -os.environ["KERNELVERSION"] = "3.7.0" +os.environ["KERNELVERSION"] = "1" # Prevent accidental loading of configuration files by removing # KCONFIG_ALLCONFIG from the environment os.environ.pop("KCONFIG_ALLCONFIG", None) +speedy = False +obsessive = False +log = False + # Number of arch/defconfig pairs tested so far nconfigs = 0 def run_tests(): - global speedy_mode, obsessive_mode, log_mode + global speedy, obsessive, log for s in sys.argv[1:]: if s == "speedy": - speedy_mode = True + speedy = True print("Speedy mode enabled") elif s == "obsessive": - obsessive_mode = True + obsessive = True print("Obsessive mode enabled") elif s == "log": - log_mode = True + log = True print("Log mode enabled") else: print("Unrecognized option '{}'".format(s)) - return run_selftests() run_compatibility_tests() -def run_selftests(): - """Runs tests on specific configurations provided by us.""" +def get_items(config, type_): + items = [] + def rec(node): + if node is not None: + if isinstance(node.item, type_): + items.append(node.item) + rec(node.list) + rec(node.next) + rec(config.top_menu) + return items + +def get_comments(config): + items = [] + def rec(node): + if node is not None: + if node.item == kconfiglib.COMMENT: + items.append(node) + rec(node.list) + rec(node.next) + rec(config.top_menu) + return items + +def get_menus(config): + items = [] + def rec(node): + if node is not None: + if node.item == kconfiglib.MENU: + items.append(node) + rec(node.list) + rec(node.next) + rec(config.top_menu) + return items + +def get_choices(config): + choices = get_items(config, kconfiglib.Choice) + unique_choices = [] + for choice in choices: + if choice not in unique_choices: + unique_choices.append(choice) + return unique_choices + +def get_parent(item): + if isinstance(item, (kconfiglib.Symbol, kconfiglib.Choice)): + if not item.nodes: + return None + return item.nodes[0].parent.item + return item.node.parent.item + +def get_prompts(item): + prompts = [] + for node in item.nodes: + if node.prompt is not None: + prompts.append(node.prompt[0]) + return prompts +def run_selftests(): # - # Helper functions + # Common helper functions. These all expect 'c' to hold the current + # configuration. # def verify_value(sym_name, val): - """Verifies that a symbol has a particular value.""" - sym = c[sym_name] - sym_val = sym.get_value() - verify(sym_val == val, - "{} should have the value '{}' but has the value '{}'" - .format(sym_name, val, sym_val)) - - def assign_and_verify_new_value(sym_name, user_val, new_val): - """Assigns a user value to the symbol and verifies the new value.""" - sym = c[sym_name] - sym_old_val = sym.get_value() - sym.set_user_value(user_val) - sym_new_val = sym.get_value() - verify(sym_new_val == new_val, - "{} should have the new value '{}' after being assigned the " - "user value '{}'. Instead, the value is '{}'. The old " - "value was '{}'." - .format(sym_name, new_val, user_val, sym_new_val, sym_old_val)) + """ + Verifies that a symbol has a particular value. + """ + sym = c.syms[sym_name] + verify(sym.value == val, + 'expected {} to have the value "{}", had the value "{}"' + .format(sym_name, val, sym.value)) + + def assign_and_verify_value(sym_name, val, new_val): + """ + Assigns 'val' to a symbol and verifies that its value becomes + 'new_val'. + """ + sym = c.syms[sym_name] + old_val = sym.value + sym.set_value(val) + verify(sym.value == new_val, + 'expected {} to have the value "{}" after being assigned the ' + 'value "{}". Instead, the value is "{}". The old value was ' + '"{}".' + .format(sym_name, new_val, val, sym.value, old_val)) def assign_and_verify(sym_name, user_val): - """Like assign_and_verify_new_value(), with the expected value being - the value just set.""" - assign_and_verify_new_value(sym_name, user_val, user_val) + """ + Like assign_and_verify_value(), with the expected value being the + value just set. + """ + assign_and_verify_value(sym_name, user_val, user_val) - def assign_and_verify_new_user_value(sym_name, user_val, new_user_val): + def assign_and_verify_user_value(sym_name, val, user_val): """Assigns a user value to the symbol and verifies the new user value.""" - sym = c[sym_name] - sym_old_user_val = sym.get_user_value() - sym.set_user_value(user_val) - sym_new_user_val = sym.get_user_value() - verify(sym_new_user_val == new_user_val, + sym = c.syms[sym_name] + sym_old_user_val = sym.user_value + sym.set_value(val) + verify(sym.user_value == user_val, "{} should have the user value '{}' after being assigned " "the user value '{}'. Instead, the new user value was '{}'. " "The old user value was '{}'." - .format(sym_name, new_user_val, user_val, sym_new_user_val, + .format(sym_name, user_val, user_val, sym.user_value, sym_old_user_val)) - print("Running selftests...\n") + # + # Selftests + # - print("Testing tristate comparisons...") + print("Testing tristate comparisons") - def verify_truth_table(comp_fn, *bools): - bools_list = list(bools) - for (x, y) in (("n", "n"), ("n", "m"), ("n", "y"), - ("m", "n"), ("m", "m"), ("m", "y"), - ("y", "n"), ("y", "m"), ("y", "y")): - expected = bools_list.pop(0) + def verify_truth_table(comp_fn, *table): + for (x, y), expected in zip((("n", "n"), ("n", "m"), ("n", "y"), + ("m", "n"), ("m", "m"), ("m", "y"), + ("y", "n"), ("y", "m"), ("y", "y")), + table): verify(comp_fn(x, y) == expected, - "Expected {} on ('{}', '{}') to be {}". + "expected {} on ('{}', '{}') to be '{}'". format(comp_fn, x, y, expected)) verify_truth_table(kconfiglib.tri_less, @@ -168,23 +241,23 @@ def run_selftests(): True, True, False, True, True, True) - # - # String literal lexing. (This tests an internal API.) - # - print("Testing string literal (constant symbol) lexing...") + print("Testing string literal (constant symbol) lexing") + # Dummy empty configuration just to get a Config object c = kconfiglib.Config("Kconfiglib/tests/empty") def verify_string_lex(s, res): - """Verifies that the string token 'res' is produced from lexing 's'. - Strips the first and last characters from 's' so we can use readable - raw strings as input.""" + """ + Verifies that the string (constant symbol) token 'res' is produced from + lexing 's'. Strips the first and last characters from 's' so that + readable raw strings can be used as input + """ s = s[1:-1] - s_res = c._tokenize(s, for_eval = True).get_next() - verify(s_res == res, - "'{}' produced the string token '{}'. Expected '{}'." - .format(s, s_res, res)) + token = c._tokenize(s, for_eval = True).next() + verify(token == res, + "expected {} to produced the string token {}, produced {}" + .format(s, token, res)) verify_string_lex(r""" "" """, "") verify_string_lex(r""" '' """, "") @@ -215,16 +288,18 @@ def run_selftests(): verify_string_lex(r""" '\a\\"\b\c\'"d' """, "a\\\"bc'\"d") def verify_string_bad(s): - """Verifies that tokenizing 's' throws a Kconfig_Syntax_Error. Strips - the first and last characters from 's' so we can use readable raw - strings as input.""" + """ + Verifies that tokenizing 's' throws a KconfigSyntaxError. Strips the + first and last characters from 's' so we can use readable raw strings + as input. + """ s = s[1:-1] try: c._tokenize(s, for_eval = True) - except kconfiglib.Kconfig_Syntax_Error: + except kconfiglib.KconfigSyntaxError: pass else: - fail("Tokenization of '{}' should have failed.".format(s)) + fail("expected tokenization of {} to fail, didn't".format(s)) verify_string_bad(r""" " """) verify_string_bad(r""" ' """) @@ -235,115 +310,18 @@ def run_selftests(): verify_string_bad(r""" "foo """) verify_string_bad(r""" 'foo """) - # - # is_modifiable() - # - - print("Testing is_modifiable() and range queries...") - - c = kconfiglib.Config("Kconfiglib/tests/Kmodifiable") - - for sym_name in ("VISIBLE", "TRISTATE_SELECTED_TO_M", "VISIBLE_STRING", - "VISIBLE_INT", "VISIBLE_HEX"): - sym = c[sym_name] - verify(sym.is_modifiable(), - "{} should be modifiable".format(sym_name)) - - for sym_name in ("n", "m", "y", "NOT_VISIBLE", "SELECTED_TO_Y", - "BOOL_SELECTED_TO_M", "M_VISIBLE_TRISTATE_SELECTED_TO_M", - "NOT_VISIBLE_STRING", "NOT_VISIBLE_INT", "NOT_VISIBLE_HEX"): - sym = c[sym_name] - verify(not sym.is_modifiable(), - "{} should not be modifiable".format(sym_name)) - - # - # get_lower/upper_bound() and get_assignable_values() - # - - c = kconfiglib.Config("Kconfiglib/tests/Kbounds") - - def verify_bounds(sym_name, low, high): - sym = c[sym_name] - sym_low = sym.get_lower_bound() - sym_high = sym.get_upper_bound() - verify(sym_low == low and sym_high == high, - "Incorrectly calculated bounds for {}: {}-{}. " - "Expected {}-{}.".format(sym_name, sym_low, sym_high, - low, high)) - # See that we get back the corresponding range from - # get_assignable_values() - if sym_low is None: - vals = sym.get_assignable_values() - verify(vals == [], - "get_assignable_values() thinks there should be assignable " - "values for {} ({}) but not get_lower/upper_bound()". - format(sym_name, vals)) - if sym.get_type() in (kconfiglib.BOOL, kconfiglib.TRISTATE): - verify(not sym.is_modifiable(), - "get_lower_bound() thinks there should be no " - "assignable values for the bool/tristate {} but " - "is_modifiable() thinks it should be modifiable". - format(sym_name)) - else: - tri_to_int = { "n" : 0, "m" : 1, "y" : 2 } - bound_range = ["n", "m", "y"][tri_to_int[sym_low] : - tri_to_int[sym_high] + 1] - assignable_range = sym.get_assignable_values() - verify(bound_range == assignable_range, - "get_lower/upper_bound() thinks the range for {} should " - "be {} while get_assignable_values() thinks it should be " - "{}".format(sym_name, bound_range, assignable_range)) - if sym.get_type() in (kconfiglib.BOOL, kconfiglib.TRISTATE): - verify(sym.is_modifiable(), - "get_lower/upper_bound() thinks the range for the " - "bool/tristate {} should be {} while is_modifiable() " - "thinks the symbol should not be modifiable". - format(sym_name, bound_range)) - - verify_bounds("n", None, None) - verify_bounds("m", None, None) - verify_bounds("y", None, None) - verify_bounds("Y_VISIBLE_BOOL", "n", "y") - verify_bounds("Y_VISIBLE_TRISTATE", "n", "y") - verify_bounds("M_VISIBLE_BOOL", "n", "y") - verify_bounds("M_VISIBLE_TRISTATE", "n", "m") - verify_bounds("Y_SELECTED_BOOL", None, None) - verify_bounds("M_SELECTED_BOOL", None, None) - verify_bounds("Y_SELECTED_TRISTATE", None, None) - verify_bounds("M_SELECTED_TRISTATE", "m", "y") - verify_bounds("M_SELECTED_M_VISIBLE_TRISTATE", None, None) - verify_bounds("N_IMPLIED_BOOL", "n", "y") - verify_bounds("N_IMPLIED_TRISTATE", "n", "y") - verify_bounds("M_IMPLIED_BOOL", "n", "y") - verify_bounds("M_IMPLIED_TRISTATE", "n", "y") - verify_bounds("Y_IMPLIED_BOOL", "n", "y") - verify_bounds("Y_IMPLIED_TRISTATE", "n", "y") - verify_bounds("STRING", None, None) - verify_bounds("INT", None, None) - verify_bounds("HEX", None, None) + # TODO: Kmodifiable gone, test assignable - # - # eval() - # - print("Testing eval()...") + print("Testing expression evaluation") c = kconfiglib.Config("Kconfiglib/tests/Keval") def verify_eval(expr, val): - res = c.eval(expr) + res = c.eval_string(expr) verify(res == val, "'{}' evaluated to {}, expected {}".format(expr, res, val)) - def verify_eval_bad(expr): - try: - c.eval(expr) - except kconfiglib.Kconfig_Syntax_Error: - pass - else: - fail('eval("{}") should throw Kconfig_Syntax_Error' - .format(expr)) - # No modules verify_eval("n", "n") verify_eval("m", "n") @@ -352,8 +330,9 @@ def run_selftests(): verify_eval("'m'", "n") verify_eval("'y'", "y") verify_eval("M", "y") + # Modules - c["MODULES"].set_user_value("y") + c.syms["MODULES"].set_value("y") verify_eval("n", "n") verify_eval("m", "m") verify_eval("y", "y") @@ -370,6 +349,7 @@ def run_selftests(): # As are all constants besides "y" and "m" verify_eval('"foo"', "n") verify_eval('"foo" || "bar"', "n") + verify_eval('"foo" || m', "m") # Test equality for symbols @@ -398,7 +378,6 @@ def run_selftests(): verify_eval("N != Y", "y") verify_eval("M != Y", "y") - # string/int/hex verify_eval("Y_STRING = y", "y") verify_eval("Y_STRING = 'y'", "y") verify_eval('FOO_BAR_STRING = "foo bar"', "y") @@ -413,12 +392,13 @@ def run_selftests(): verify_eval("HEX_0X37 = '0x037'", "y") verify_eval("HEX_0X37 = '0x0037'", "y") - # Compare some constants... + # Constant symbol comparisons verify_eval('"foo" != "bar"', "y") verify_eval('"foo" = "bar"', "n") verify_eval('"foo" = "foo"', "y") + # Undefined symbols get their name as their value - c.set_print_warnings(False) + c.disable_warnings() verify_eval("'not_defined' = not_defined", "y") verify_eval("not_defined_2 = not_defined_2", "y") verify_eval("not_defined_1 != not_defined_2", "y") @@ -505,9 +485,18 @@ def run_selftests(): verify_eval("oops > INT_37", "n") verify_eval("oops >= INT_37", "n") + def verify_eval_bad(expr): + try: + c.eval_string(expr) + except kconfiglib.KconfigSyntaxError: + pass + else: + fail('expected eval_string("{}") to throw KconfigSyntaxError, ' \ + 'didn\'t'.format(expr)) + # The C implementation's parser can be pretty lax about syntax. Kconfiglib # sometimes needs to emulate that. Verify that some bad stuff throws - # Kconfig_Syntax_Error at least. + # KconfigSyntaxError at least. verify_eval_bad("") verify_eval_bad("&") verify_eval_bad("|") @@ -521,641 +510,498 @@ def run_selftests(): verify_eval_bad("X ||") verify_eval_bad("|| X") - # - # Text queries - # - print("Testing text queries...") + print("Testing Symbol.__str__()") + + def verify_str(item, s): + verify_equal(str(item), s[1:]) + + c = kconfiglib.Config("Kconfiglib/tests/Kstr", warn=False) + + verify_str(c.syms["UNDEFINED"], """ +""") + + verify_str(c.syms["BASIC_NO_PROMPT"], """ +config BASIC_NO_PROMPT + bool + help + blah blah + + blah blah blah + + blah +""") + + verify_str(c.syms["BASIC_PROMPT"], """ +config BASIC_PROMPT + bool + prompt "basic" +""") + + verify_str(c.syms["ADVANCED"], """ +config ADVANCED + tristate + prompt "prompt" if DEP + default DEFAULT_1 + default DEFAULT_2 if DEP + select SELECTED_1 + select SELECTED_2 if DEP + imply IMPLIED_1 + imply IMPLIED_2 if DEP + help + first help text + +config ADVANCED + prompt "prompt 2" + +menuconfig ADVANCED + prompt "prompt 3" if DEP2 + +config ADVANCED + help + second help text +""") + + verify_str(c.syms["STRING"], """ +config STRING + string + default "foo" + default "bar" if DEP + default STRING2 + default STRING3 if DEP +""") + + verify_str(c.syms["INT"], """ +config INT + int + range 1 2 + range FOO BAR + range BAZ QAZ if DEP +""") - def verify_print(o, s): - verify_equals(str(o), textwrap.dedent(s[1:])) + # We still hardcode the modules symbol. Otherwise OPTIONS would have made + # more sense as a name here. + verify_str(c.syms["MODULES"], """ +config MODULES + option modules +""") - for var in ("ARCH", "SRCARCH", "srctree"): - os.environ.pop(var, None) + verify_str(c.syms["OPTIONS"], """ +config OPTIONS + option allnoconfig_y + option defconfig_list + option env="ENV" +""") - # The tests below aren't meant to imply that the format is set in stone. - # It's just to verify that the strings do not change unexpectedly. + print("Testing Choice.__str__()") - # Printing of Config + verify_str(c.named_choices["CHOICE"], """ +choice CHOICE + tristate + prompt "foo" + default CHOICE_1 + default CHOICE_2 if dep +""") - c = kconfiglib.Config("Kconfiglib/tests/Ktext") + verify_str(c.named_choices["CHOICE"].nodes[0].next.item, """ +choice + tristate + prompt "no name" +""") - verify_print(c, """ - Configuration - File : Kconfiglib/tests/Ktext - Base directory : . - Value of $ARCH at creation time : (not set) - Value of $SRCARCH at creation time : (not set) - Value of $srctree at creation time : (not set) - Most recently loaded .config : (no .config loaded) - Print warnings : True - Print assignments to undefined symbols : False""") - os.environ["ARCH"] = "foo" - os.environ["SRCARCH"] = "bar" - os.environ["srctree"] = "baz" + print("Testing Symbol.__repr__()") + + def verify_repr(item, s): + verify_equal(repr(item) + "\n", s[1:]) + + c = kconfiglib.Config("Kconfiglib/tests/Krepr", warn=False) + + verify_repr(c.syms["UNDEFINED"], """ + +""") + + verify_repr(c.syms["BASIC"], """ + +""") - c = kconfiglib.Config("Kconfiglib/tests/Ktext", base_dir="foobar") - c.load_config("Kconfiglib/tests/empty") - c.set_print_warnings(False) - c.set_print_undef_assign(True) - - choice_print, choice_no_help, choice_empty_help, choice_help = \ - c.get_choices() - - verify_print(c, """ - Configuration - File : Kconfiglib/tests/Ktext - Base directory : foobar - Value of $ARCH at creation time : foo - Value of $SRCARCH at creation time : bar - Value of $srctree at creation time : baz - Most recently loaded .config : Kconfiglib/tests/empty - Print warnings : False - Print assignments to undefined symbols : True""") - - # Printing of Symbol - - verify_print(c["BASIC"], """ - Symbol BASIC - Type : bool - Value : "n" - User value : (no user value) - Visibility : "n" - Is choice item : False - Is defined : True - Is from env. : False - Is special : False - Prompts: - (no prompts) - Default values: - (no default values) - Selects: - (no selects) - Implies: - (no implies) - Reverse (select-related) dependencies: - (no reverse dependencies) - Weak reverse (imply-related) dependencies: - (no weak reverse dependencies) - Additional dependencies from enclosing menus and ifs: - (no additional dependencies) - Locations: Kconfiglib/tests/Ktext:1""") - - c["ADVANCED"].set_user_value("m") - - verify_print(c["ADVANCED"], """ - Symbol ADVANCED - Type : tristate - Value : "y" - User value : "m" - Visibility : "y" - Is choice item : False - Is defined : True - Is from env. : False - Is special : False - Prompts: - "advanced prompt 1" if y || BASIC && BASIC (value: "y") - "advanced prompt 2" - Default values: - y (value: "y") - Condition: BASIC && !BASIC (value: "n") - n (value: "n") - Condition: BASIC = DUMMY && X < Y && X <= Y && X > Y && X >= Y (value: "n") - Selects: - SELECTED_1 if BASIC && DUMMY (value: "n") - SELECTED_2 if !(DUMMY || BASIC) (value: "y") - Implies: - IMPLIED_1 if BASIC || DUMMY (value: "n") - IMPLIED_2 if !(DUMMY && BASIC) (value: "y") - Reverse (select-related) dependencies: - SELECTING_1 && BASIC || SELECTING_2 && !BASIC (value: "n") - Weak reverse (imply-related) dependencies: - IMPLYING_1 && DUMMY || IMPLYING_2 && !DUMMY (value: "n") - Additional dependencies from enclosing menus and ifs: - !BASIC && !BASIC (value: "y") - Locations: Kconfiglib/tests/Ktext:6 Kconfiglib/tests/Ktext:15""") - - verify_print(c["STRING"], """ - Symbol STRING - Type : string - Value : "foo" - User value : (no user value) - Visibility : "n" - Is choice item : False - Is defined : True - Is from env. : False - Is special : False - Prompts: - (no prompts) - Default values: - "foo" - Condition: (none) - "bar" - Condition: BAR (value: "n") - STRING2 (value: "baz") - Condition: BAZ (value: "n") - Selects: - (no selects) - Implies: - (no implies) - Reverse (select-related) dependencies: - (no reverse dependencies) - Weak reverse (imply-related) dependencies: - (no weak reverse dependencies) - Additional dependencies from enclosing menus and ifs: - !BASIC && !BASIC (value: "y") - Locations: Kconfiglib/tests/Ktext:18""") - - verify_print(c["HAS_RANGES"], """ - Symbol HAS_RANGES - Type : int - Value : "1" - User value : (no user value) - Visibility : "y" - Is choice item : False - Is defined : True - Is from env. : False - Is special : False - Ranges: - [1, 2] if !DUMMY (value: "y") - [INT, INT] if DUMMY (value: "n") - [123, 456] - Prompts: - "ranged" - Default values: - (no default values) - Selects: - (no selects) - Implies: - (no implies) - Reverse (select-related) dependencies: - (no reverse dependencies) - Weak reverse (imply-related) dependencies: - (no weak reverse dependencies) - Additional dependencies from enclosing menus and ifs: - (no additional dependencies) - Locations: Kconfiglib/tests/Ktext:45""") - - # Printing of Choice - - verify_print(choice_print, """ - Choice - Name (for named choices): (no name) - Type : bool - Selected symbol : CHOICE_ITEM_1 - User value : (no user value) - Mode : "y" - Visibility : "y" - Optional : False - Prompts: - "choice" - Defaults: - (no default values) - Choice symbols: - CHOICE_ITEM_1 CHOICE_ITEM_2 CHOICE_ITEM_3 - Additional dependencies from enclosing menus and ifs: - (no additional dependencies) - Locations: Kconfiglib/tests/Ktext:51""") - - c["CHOICE_ITEM_2"].set_user_value("y") - - verify_print(choice_print, """ - Choice - Name (for named choices): (no name) - Type : bool - Selected symbol : CHOICE_ITEM_2 - User value : CHOICE_ITEM_2 - Mode : "y" - Visibility : "y" - Optional : False - Prompts: - "choice" - Defaults: - (no default values) - Choice symbols: - CHOICE_ITEM_1 CHOICE_ITEM_2 CHOICE_ITEM_3 - Additional dependencies from enclosing menus and ifs: - (no additional dependencies) - Locations: Kconfiglib/tests/Ktext:51""") - - # Printing of Menu - - verify_print(c.get_menus()[0], """ - Menu - Title : simple menu - 'depends on' dependencies : (no dependencies) - 'visible if' dependencies : (no dependencies) - Additional dependencies from enclosing menus and ifs: - (no additional dependencies) - Location: Kconfiglib/tests/Ktext:63""") - - verify_print(c.get_menus()[1], """ - Menu - Title : advanced menu - 'depends on' dependencies : !BASIC (value: "y") - 'visible if' dependencies : !DUMMY (value: "y") - Additional dependencies from enclosing menus and ifs: - !DUMMY (value: "y") - Location: Kconfiglib/tests/Ktext:67""") - - # Printing of Comment - - verify_print(c.get_comments()[0], """ - Comment - Text: simple comment - Dependencies: (no dependencies) - Additional dependencies from enclosing menus and ifs: - (no additional dependencies) - Location: Kconfiglib/tests/Ktext:73""") - - verify_print(c.get_comments()[1], """ - Comment - Text: advanced comment - Dependencies: !BASIC (value: "y") - Additional dependencies from enclosing menus and ifs: - !DUMMY (value: "y") - Location: Kconfiglib/tests/Ktext:76""") - - verify_equals(c["NO_HELP"].get_help(), None) - verify_equals(choice_no_help.get_help(), None) - verify_equals(c["EMPTY_HELP"].get_help(), "") - verify_equals(choice_empty_help.get_help(), "") - verify_equals(c["HELP_TERMINATED_BY_COMMENT"].get_help(), "a\nb\nc\n") - verify_equals(c["TRICKY_HELP"].get_help(), - "a\n b\n c\n\n d\n e\n f\n\n\ng\n h\n i\n") - verify_equals(c["S"].get_help(), "help for\nS\n") - verify_equals(choice_help.get_help(), "help for\nC\n") - - verify_equals(c["S"].get_name(), "S") - verify_equals(c.get_comments()[2].get_text(), "a comment") - verify_equals(c.get_menus()[2].get_title(), "a menu") + verify_repr(c.syms["VISIBLE"], """ + +""") - # - # Prompt queries - # + verify_repr(c.syms["DIR_DEP_N"], """ + +""") - print("Testing prompt queries...") + verify_repr(c.syms["OPTIONS"], """ + +""") - def verify_prompts(sym_or_choice, prompts): - sym_or_choice_prompts = sym_or_choice.get_prompts() - verify(len(sym_or_choice_prompts) == len(prompts), - "Wrong number of prompts for " + sym_or_choice.get_name()) - for i in range(0, len(sym_or_choice_prompts)): - verify(sym_or_choice_prompts[i] == prompts[i], - "Prompt {} wrong for {}: Was '{}', should be '{}'". - format(i, sym_or_choice.get_name(), sym_or_choice_prompts[i], - prompts[i])) + verify_repr(c.syms["MULTI_DEF"], """ + +""") - def verify_sym_prompts(sym_name, *prompts): - verify_prompts(c[sym_name], prompts) + verify_repr(c.syms["CHOICE_1"], """ + +""") - def verify_choice_prompts(choice, *prompts): - verify_prompts(choice, prompts) + verify_repr(c.syms["MODULES"], """ + +""") - c = kconfiglib.Config("Kconfiglib/tests/Kprompt") - # Symbols - verify_sym_prompts("NO_PROMPT") - verify_sym_prompts("SINGLE_PROMPT_1", "single prompt 1") - verify_sym_prompts("SINGLE_PROMPT_2", "single prompt 2") - verify_sym_prompts("MULTI_PROMPT", "prompt 1", "prompt 2", "prompt 3", "prompt 4") - no_prompt_choice, single_prompt_1_choice, single_prompt_2_choice, multi_prompt_choice = \ - c.get_choices() + print("Testing Choice.__repr__()") - # Choices - verify_choice_prompts(no_prompt_choice) - verify_choice_prompts(single_prompt_1_choice, "single prompt 1 choice") - verify_choice_prompts(single_prompt_2_choice, "single prompt 2 choice") - verify_choice_prompts(multi_prompt_choice, - "prompt 1 choice", "prompt 2 choice", "prompt 3 choice") + verify_repr(c.named_choices["CHOICE"], """ + +""") - # - # Location queries - # + c.named_choices["CHOICE"].set_value("y") + + verify_repr(c.named_choices["CHOICE"], """ + +""") + + c.syms["CHOICE_2"].set_value("y") + + verify_repr(c.named_choices["CHOICE"], """ + +""") + + verify_repr(c.syms["CHOICE_HOOK"].nodes[0].next.item, """ + +""") + + + print("Testing MenuNode.__repr__()") + + verify_repr(c.syms["BASIC"].nodes[0], """ + +""") + + verify_repr(c.syms["DIR_DEP_N"].nodes[0], """ + +""") + + verify_repr(c.syms["MULTI_DEF"].nodes[0], """ + +""") + + verify_repr(c.syms["MULTI_DEF"].nodes[1], """ + +""") + + verify_repr(c.syms["MENUCONFIG"].nodes[0], """ + +""") + + verify_repr(c.named_choices["CHOICE"].nodes[0], """ + +""") + + verify_repr(c.syms["CHOICE_HOOK"].nodes[0].next, """ + +""") + + verify_repr(c.syms["NO_VISIBLE_IF_HOOK"].nodes[0].next, """ + +""") + + verify_repr(c.syms["VISIBLE_IF_HOOK"].nodes[0].next, """ + +""") + + verify_repr(c.syms["COMMENT_HOOK"].nodes[0].next, """ + +""") + + + print("Testing Config.__repr__()") + + verify_repr(c, """ + +""") + + os.environ["srctree"] = "srctree value" + os.environ["CONFIG_"] = "CONFIG_ value" + + c = kconfiglib.Config("Kconfiglib/tests/Krepr", warn=False) + c.enable_warnings() + c.enable_undef_warnings() + + verify_repr(c, """ + +""") + + os.environ.pop("srctree", None) + os.environ.pop("CONFIG_", None) + + + print("Testing tricky help strings") + + c = kconfiglib.Config("Kconfiglib/tests/Khelp") + + def verify_help(node, s): + verify_equal(node.help, s[1:]) + + verify_help(c.syms["TWO_HELP_STRINGS"].nodes[0], """ +first help string +""") + + verify_help(c.syms["TWO_HELP_STRINGS"].nodes[1], """ +second help string +""") + + verify_help(c.syms["NO_BLANK_AFTER_HELP"].nodes[0], """ +help for +NO_BLANK_AFTER_HELP +""") + + verify_help(c.named_choices["CHOICE_HELP"].nodes[0], """ +help for +CHOICE_HELP +""") + + verify_help(c.syms["HELP_TERMINATED_BY_COMMENT"].nodes[0], """ +a +b +c +""") + + verify_help(c.syms["TRICKY_HELP"].nodes[0], """ +a + b + c + + d + e + f + + +g + h + i +""") - print("Testing location queries...") - def verify_def_locations(sym_name, *locs): - sym_locs = c[sym_name].get_def_locations() - verify(len(sym_locs) == len(locs), - "Wrong number of def. locations for " + sym_name) - for i in range(0, len(sym_locs)): - verify(sym_locs[i] == locs[i], - "Wrong def. location for {}: Was {}, should be {}". - format(sym_name, sym_locs[i], locs[i])) + print("Testing locations and 'source'") + + def verify_locations(nodes, *expected_locs): + verify(len(nodes) == len(expected_locs), + "Wrong number of locations for " + repr(nodes)) + + for node, expected_loc in zip(nodes, expected_locs): + node_loc = "{}:{}".format(node.filename, node.linenr) + verify(node_loc == expected_loc, + "expected {} to have the location {}, had the location {}" + .format(repr(node), expected_loc, node_loc)) # Expanded in the 'source' statement in Klocation - os.environ["FOO"] = "tests" - - c = kconfiglib.Config("Kconfiglib/tests/Klocation", base_dir="Kconfiglib/") - - verify_def_locations("n") - verify_def_locations("m") - verify_def_locations("y") - - verify_def_locations("A", - ("Kconfiglib/tests/Klocation", 4), - ("Kconfiglib/tests/Klocation", 28), - ("Kconfiglib/tests/Klocation_included", 1), - ("Kconfiglib/tests/Klocation_included", 3)) - verify_def_locations("C", - ("Kconfiglib/tests/Klocation", 18)) - verify_def_locations("M", - ("Kconfiglib/tests/Klocation_included", 6)) - verify_def_locations("N", - ("Kconfiglib/tests/Klocation_included", 19)) - verify_def_locations("O", - ("Kconfiglib/tests/Klocation_included", 21)) - verify_def_locations("NOT_DEFINED") # No locations - - def verify_ref_locations(sym_name, *locs): - sym_locs = c[sym_name].get_ref_locations() - verify(len(sym_locs) == len(locs), - "Wrong number of ref. locations for " + sym_name) - for i in range(0, len(sym_locs)): - verify(sym_locs[i] == locs[i], - "Wrong ref. location for {}: Was {}, should be {}". - format(sym_name, sym_locs[i], locs[i])) - - # Reload without the slash at the end of 'base_dir' to get coverage for - # that as well - c = kconfiglib.Config("Kconfiglib/tests/Klocation", base_dir="Kconfiglib") - - verify_ref_locations("A", - ("Kconfiglib/tests/Klocation", 10), - ("Kconfiglib/tests/Klocation", 12), - ("Kconfiglib/tests/Klocation", 16), - ("Kconfiglib/tests/Klocation", 34), - ("Kconfiglib/tests/Klocation", 35), - ("Kconfiglib/tests/Klocation_included", 7), - ("Kconfiglib/tests/Klocation_included", 8), - ("Kconfiglib/tests/Klocation_included", 9), - ("Kconfiglib/tests/Klocation_included", 12), - ("Kconfiglib/tests/Klocation_included", 13), - ("Kconfiglib/tests/Klocation_included", 14), - ("Kconfiglib/tests/Klocation_included", 15), - ("Kconfiglib/tests/Klocation_included", 35), - ("Kconfiglib/tests/Klocation_included", 40), - ("Kconfiglib/tests/Klocation", 65), - ("Kconfiglib/tests/Klocation", 66), - ("Kconfiglib/tests/Klocation", 67), - ("Kconfiglib/tests/Klocation", 68), - ("Kconfiglib/tests/Klocation", 69), - ("Kconfiglib/tests/Klocation", 70), - ("Kconfiglib/tests/Klocation", 71), - ("Kconfiglib/tests/Klocation", 72), - ("Kconfiglib/tests/Klocation", 73)) - verify_ref_locations("C") - verify_ref_locations("NOT_DEFINED", - ("Kconfiglib/tests/Klocation", 12), - ("Kconfiglib/tests/Klocation", 29), - ("Kconfiglib/tests/Klocation_included", 12), - ("Kconfiglib/tests/Klocation_included", 35), - ("Kconfiglib/tests/Klocation_included", 41)) - - # Location queries for choices - - def verify_choice_locations(choice, *locs): - choice_locs = choice.get_def_locations() - verify(len(choice_locs) == len(locs), - "Wrong number of def. locations for choice") - for i in range(0, len(choice_locs)): - verify(choice_locs[i] == locs[i], - "Wrong def. location for choice: Was {}, should be {}". - format(choice_locs[i], locs[i])) - - choice_1, choice_2 = c.get_choices() - - # Throw in named choice test - verify(choice_1.get_name() == "B", - "The first choice should be called B") - verify(choice_2.get_name() is None, - "The second choice should have no name") - - verify_choice_locations(choice_1, - ("Kconfiglib/tests/Klocation", 15), - ("Kconfiglib/tests/Klocation_included", 24)) - verify_choice_locations(choice_2, - ("Kconfiglib/tests/Klocation_included", 17)) - - # Location queries for menus and comments - - def verify_location(menu_or_comment, loc): - menu_or_comment_loc = menu_or_comment.get_location() - verify(menu_or_comment_loc == loc, - "Wrong location for {} with text '{}': Was {}, should be " - "{}".format("menu" if menu_or_comment.is_menu() else "comment", - menu_or_comment.get_title() if - menu_or_comment.is_menu() else - menu_or_comment.get_text(), - menu_or_comment_loc, - loc)) - - menu_1, menu_2 = c.get_menus()[:-1] - comment_1, comment_2 = c.get_comments() - - verify_location(menu_1, ("Kconfiglib/tests/Klocation", 9)) - verify_location(menu_2, ("Kconfiglib/tests/Klocation_included", 5)) - verify_location(comment_1, ("Kconfiglib/tests/Klocation", 31)) - verify_location(comment_2, ("Kconfiglib/tests/Klocation_included", 36)) + os.environ["EXPANDED_FROM_ENV"] = "tests" + os.environ["srctree"] = "Kconfiglib/" - # - # Visibility queries - # + c = kconfiglib.Config("tests/Klocation") - print("Testing visibility queries...") + os.environ.pop("EXPANDED_FROM_ENV", None) + os.environ.pop("srctree", None) - c = kconfiglib.Config("Kconfiglib/tests/Kvisibility") + verify_locations(c.syms["SINGLE_DEF"].nodes, "tests/Klocation:4") + + verify_locations(c.syms["MULTI_DEF"].nodes, + "tests/Klocation:6", + "tests/Klocation:16", + "tests/Klocation_included:3") - def verify_sym_visibility(sym_name, no_module_vis, module_vis): - sym = c[sym_name] + verify_locations(c.named_choices["CHOICE"].nodes, + "tests/Klocation_included:5") + + verify_locations([c.syms["MENU_HOOK"].nodes[0].next], + "tests/Klocation_included:10") + + verify_locations([c.syms["COMMENT_HOOK"].nodes[0].next], + "tests/Klocation_included:15") + + + print("Testing visibility") + + c = kconfiglib.Config("Kconfiglib/tests/Kvisibility") - c["MODULES"].set_user_value("n") - sym_vis = sym.get_visibility() - verify(sym_vis == no_module_vis, - "{} should have visibility '{}' without modules, had " - "visibility '{}'". - format(sym_name, no_module_vis, sym_vis)) + def verify_visibility(item, no_module_vis, module_vis): + c.syms["MODULES"].set_value("n") + verify(item.visibility == no_module_vis, + "expected {} to have visibility {} without modules, had " + "visibility {}". + format(repr(item), no_module_vis, item.visibility)) - c["MODULES"].set_user_value("y") - sym_vis = sym.get_visibility() - verify(sym_vis == module_vis, - "{} should have visibility '{}' with modules, had " - "visibility '{}'". - format(sym_name, module_vis, sym_vis)) + c.syms["MODULES"].set_value("y") + verify(item.visibility == module_vis, + "expected {} to have visibility {} with modules, had " + "visibility {}". + format(repr(item), module_vis, item.visibility)) # Symbol visibility - verify_sym_visibility("NO_PROMPT", "n", "n") - verify_sym_visibility("BOOL_n", "n", "n") - verify_sym_visibility("BOOL_m", "n", "y") # Promoted - verify_sym_visibility("BOOL_MOD", "y", "y") # Promoted - verify_sym_visibility("BOOL_y", "y", "y") - verify_sym_visibility("TRISTATE_m", "n", "m") - verify_sym_visibility("TRISTATE_MOD", "y", "m") # Promoted - verify_sym_visibility("TRISTATE_y", "y", "y") - verify_sym_visibility("BOOL_if_n", "n", "n") - verify_sym_visibility("BOOL_if_m", "n", "y") # Promoted - verify_sym_visibility("BOOL_if_y", "y", "y") - verify_sym_visibility("BOOL_menu_n", "n", "n") - verify_sym_visibility("BOOL_menu_m", "n", "y") # Promoted - verify_sym_visibility("BOOL_menu_y", "y", "y") - verify_sym_visibility("BOOL_choice_n", "n", "n") + verify_visibility(c.syms["NO_PROMPT"], "n", "n") + verify_visibility(c.syms["BOOL_N"], "n", "n") + verify_visibility(c.syms["BOOL_M"], "n", "y") + verify_visibility(c.syms["BOOL_MOD"], "y", "y") + verify_visibility(c.syms["BOOL_Y"], "y", "y") + verify_visibility(c.syms["TRISTATE_M"], "n", "m") + verify_visibility(c.syms["TRISTATE_MOD"], "y", "m") + verify_visibility(c.syms["TRISTATE_Y"], "y", "y") + verify_visibility(c.syms["BOOL_IF_N"], "n", "n") + verify_visibility(c.syms["BOOL_IF_M"], "n", "y") + verify_visibility(c.syms["BOOL_IF_Y"], "y", "y") + verify_visibility(c.syms["BOOL_MENU_N"], "n", "n") + verify_visibility(c.syms["BOOL_MENU_M"], "n", "y") + verify_visibility(c.syms["BOOL_MENU_Y"], "y", "y") + verify_visibility(c.syms["BOOL_CHOICE_N"], "n", "n") # Non-tristate symbols in tristate choices are only visible if the choice # is in "y" mode - verify_sym_visibility("BOOL_choice_m", "n", "n") - verify_sym_visibility("BOOL_choice_y", "y", "n") - c["TRISTATE_choice_m"].set_user_value("y") - c["TRISTATE_choice_y"].set_user_value("y") - # Still limited by the visibility of the choice - verify_sym_visibility("BOOL_choice_m", "n", "n") - # This one should become visible now though - verify_sym_visibility("BOOL_choice_y", "y", "y") - - verify_sym_visibility("TRISTATE_if_n", "n", "n") - verify_sym_visibility("TRISTATE_if_m", "n", "m") - verify_sym_visibility("TRISTATE_if_y", "y", "y") - verify_sym_visibility("TRISTATE_menu_n", "n", "n") - verify_sym_visibility("TRISTATE_menu_m", "n", "m") - verify_sym_visibility("TRISTATE_menu_y", "y", "y") - verify_sym_visibility("TRISTATE_choice_n", "n", "n") - verify_sym_visibility("TRISTATE_choice_m", "n", "m") - verify_sym_visibility("TRISTATE_choice_y", "y", "y") - - # Choice visibility - - def verify_choice_visibility(choice, no_module_vis, module_vis): - c["MODULES"].set_user_value("n") - choice_vis = choice.get_visibility() - verify(choice_vis == no_module_vis, - "choice {} should have visibility '{}' without modules, " - "has visibility '{}'". - format(choice.get_name(), no_module_vis, choice_vis)) - - c["MODULES"].set_user_value("y") - choice_vis = choice.get_visibility() - verify(choice_vis == module_vis, - "choice {} should have visibility '{}' with modules, " - "has visibility '{}'". - format(choice.get_name(), module_vis, choice_vis)) + verify_visibility(c.syms["BOOL_CHOICE_M"], "n", "n") - choice_bool_n, choice_bool_m, choice_bool_y, choice_tristate_n, \ - choice_tristate_m, choice_tristate_y, choice_tristate_if_m_and_y, \ - choice_tristate_menu_n_and_y \ - = c.get_choices()[3:] + # Tristate choices start out in "m" mode. When running without modules, + # their type gets adjusted to bool. + verify_visibility(c.syms["BOOL_CHOICE_Y"], "y", "n") - verify_choice_visibility(choice_bool_n, "n", "n") - verify_choice_visibility(choice_bool_m, "n", "y") # Promoted - verify_choice_visibility(choice_bool_y, "y", "y") - verify_choice_visibility(choice_tristate_n, "n", "n") - verify_choice_visibility(choice_tristate_m, "n", "m") - verify_choice_visibility(choice_tristate_y, "y", "y") + c.syms["TRISTATE_CHOICE_M"].set_value("y") + c.syms["TRISTATE_CHOICE_Y"].set_value("y") - verify_choice_visibility(choice_tristate_if_m_and_y, "n", "m") - verify_choice_visibility(choice_tristate_menu_n_and_y, "n", "n") + # Still limited by the visibility of the choice + verify_visibility(c.syms["BOOL_CHOICE_M"], "n", "n") + + # This one should become visible now + verify_visibility(c.syms["BOOL_CHOICE_Y"], "y", "y") + + verify_visibility(c.syms["TRISTATE_IF_N"], "n", "n") + verify_visibility(c.syms["TRISTATE_IF_M"], "n", "m") + verify_visibility(c.syms["TRISTATE_IF_Y"], "y", "y") + verify_visibility(c.syms["TRISTATE_MENU_N"], "n", "n") + verify_visibility(c.syms["TRISTATE_MENU_M"], "n", "m") + verify_visibility(c.syms["TRISTATE_MENU_Y"], "y", "y") + verify_visibility(c.syms["TRISTATE_CHOICE_N"], "n", "n") + verify_visibility(c.syms["TRISTATE_CHOICE_M"], "n", "m") + verify_visibility(c.syms["TRISTATE_CHOICE_Y"], "y", "y") + + verify_visibility(c.named_choices["BOOL_CHOICE_N"], "n", "n") + verify_visibility(c.named_choices["BOOL_CHOICE_M"], "n", "y") + verify_visibility(c.named_choices["BOOL_CHOICE_Y"], "y", "y") + verify_visibility(c.named_choices["TRISTATE_CHOICE_N"], "n", "n") + verify_visibility(c.named_choices["TRISTATE_CHOICE_M"], "n", "m") + verify_visibility(c.named_choices["TRISTATE_CHOICE_Y"], "y", "y") + + verify_visibility(c.named_choices["TRISTATE_CHOICE_IF_M_AND_Y"], + "n", "m") + verify_visibility(c.named_choices["TRISTATE_CHOICE_MENU_N_AND_Y"], + "n", "n") # Menu visibility def verify_menu_visibility(menu, no_module_vis, module_vis): - c["MODULES"].set_user_value("n") - menu_vis = menu.get_visibility() + c["MODULES"].set_value("n") + menu_vis = kconfiglib.eval_expr(menu.node.dep) verify(menu_vis == no_module_vis, "menu \"{}\" should have visibility '{}' without modules, " - "has visibility '{}'". - format(menu.get_title(), no_module_vis, menu_vis)) + "has visibility '{}'" + .format(menu.title, no_module_vis, menu_vis)) - c["MODULES"].set_user_value("y") - menu_vis = menu.get_visibility() + c["MODULES"].set_value("y") + menu_vis = kconfiglib.eval_expr(menu.node.dep) verify(menu_vis == module_vis, "menu \"{}\" should have visibility '{}' with modules, " "has visibility '{}'". - format(menu.get_title(), module_vis, menu_vis)) + format(menu.title, module_vis, menu_vis)) - menu_n, menu_m, menu_y, menu_if_n, menu_if_m, menu_if_y, \ - menu_if_m_and_y = c.get_menus()[4:-5] - verify(menu_n.get_title() == "menu n", "Ops - testing the wrong menus") + # TODO: does this make sense anymore? - verify_menu_visibility(menu_n, "n", "n") - verify_menu_visibility(menu_m, "n", "m") - verify_menu_visibility(menu_y, "y", "y") - verify_menu_visibility(menu_if_n, "n", "n") - verify_menu_visibility(menu_if_m, "n", "m") - verify_menu_visibility(menu_if_y, "y", "y") - verify_menu_visibility(menu_if_m_and_y, "n", "m") + #menu_n, menu_m, menu_y, menu_if_n, menu_if_m, menu_if_y, \ + # menu_if_m_and_y = get_menus(c)[5:-5] + + #verify_menu_visibility(menu_n, "n", "n") + #verify_menu_visibility(menu_m, "n", "m") + #verify_menu_visibility(menu_y, "y", "y") + #verify_menu_visibility(menu_if_n, "n", "n") + #verify_menu_visibility(menu_if_m, "n", "m") + #verify_menu_visibility(menu_if_y, "y", "y") + #verify_menu_visibility(menu_if_m_and_y, "n", "m") # Menu 'visible if' visibility menu_visible_if_n, menu_visible_if_m, menu_visible_if_y, \ - menu_visible_if_m_2 = c.get_menus()[12:] + menu_visible_if_m_2 = get_menus(c)[13:] def verify_visible_if_visibility(menu, no_module_vis, module_vis): - c["MODULES"].set_user_value("n") + c["MODULES"].set_value("n") menu_vis = menu.get_visible_if_visibility() verify(menu_vis == no_module_vis, "menu \"{}\" should have 'visible if' visibility '{}' " "without modules, has 'visible if' visibility '{}'". - format(menu.get_title(), no_module_vis, menu_vis)) + format(menu.title, no_module_vis, menu_vis)) - c["MODULES"].set_user_value("y") + c["MODULES"].set_value("y") menu_vis = menu.get_visible_if_visibility() verify(menu_vis == module_vis, "menu \"{}\" should have 'visible if' visibility '{}' " "with modules, has 'visible if' visibility '{}'". - format(menu.get_title(), module_vis, menu_vis)) + format(menu.title, module_vis, menu_vis)) + + # TODO: verify the visible if stuff after unclassing # Ordinary visibility should not affect 'visible if' visibility - verify_visible_if_visibility(menu_n, "y", "y") - verify_visible_if_visibility(menu_if_n, "y", "y") - verify_visible_if_visibility(menu_m, "y", "y") - verify_visible_if_visibility(menu_if_m, "y", "y") - - verify_visible_if_visibility(menu_visible_if_n, "n", "n") - verify_visible_if_visibility(menu_visible_if_m, "n", "m") - verify_visible_if_visibility(menu_visible_if_y, "y", "y") - verify_visible_if_visibility(menu_visible_if_m_2, "n", "m") - - # Verify that 'visible if' visibility gets propagated to contained symbols - verify_sym_visibility("VISIBLE_IF_n", "n", "n") - verify_sym_visibility("VISIBLE_IF_m", "n", "m") - verify_sym_visibility("VISIBLE_IF_y", "y", "y") - verify_sym_visibility("VISIBLE_IF_m_2", "n", "m") + #verify_visible_if_visibility(menu_n, "y", "y") + #verify_visible_if_visibility(menu_if_n, "y", "y") + #verify_visible_if_visibility(menu_m, "y", "y") + #verify_visible_if_visibility(menu_if_m, "y", "y") + + #verify_visible_if_visibility(menu_visible_if_n, "n", "n") + #verify_visible_if_visibility(menu_visible_if_m, "n", "m") + #verify_visible_if_visibility(menu_visible_if_y, "y", "y") + #verify_visible_if_visibility(menu_visible_if_m_2, "n", "m") + + # Verify that 'visible if' visibility gets propagated to prompts + verify_visibility(c.syms["VISIBLE_IF_N"], "n", "n") + verify_visibility(c.syms["VISIBLE_IF_M"], "n", "m") + verify_visibility(c.syms["VISIBLE_IF_Y"], "y", "y") + verify_visibility(c.syms["VISIBLE_IF_M_2"], "n", "m") # Comment visibility def verify_comment_visibility(comment, no_module_vis, module_vis): - c["MODULES"].set_user_value("n") - comment_vis = comment.get_visibility() + c["MODULES"].set_value("n") + # TODO: uninternalize + comment_vis = kconfiglib.eval_expr(comment.node.dep) verify(comment_vis == no_module_vis, "comment \"{}\" should have visibility '{}' without " "modules, has visibility '{}'". - format(comment.get_text(), no_module_vis, comment_vis)) + format(comment.text, no_module_vis, comment_vis)) - c["MODULES"].set_user_value("y") - comment_vis = comment.get_visibility() + c["MODULES"].set_value("y") + comment_vis = kconfiglib.eval_expr(comment.node.dep) verify(comment_vis == module_vis, "comment \"{}\" should have visibility '{}' with " "modules, has visibility '{}'". - format(comment.get_text(), module_vis, comment_vis)) + format(comment.text, module_vis, comment_vis)) + + # TODO: verify the visibility stuff for comments - comment_n, comment_m, comment_y, comment_if_n, comment_if_m, \ - comment_if_y, comment_m_nested = c.get_comments() + #comment_n, comment_m, comment_y, comment_if_n, comment_if_m, \ + # comment_if_y, comment_m_nested = get_comments(c) - verify_comment_visibility(comment_n, "n", "n") - verify_comment_visibility(comment_m, "n", "m") - verify_comment_visibility(comment_y, "y", "y") - verify_comment_visibility(comment_if_n, "n", "n") - verify_comment_visibility(comment_if_m, "n", "m") - verify_comment_visibility(comment_if_y, "y", "y") - verify_comment_visibility(comment_m_nested, "n", "m") + #verify_comment_visibility(comment_n, "n", "n") + #verify_comment_visibility(comment_m, "n", "m") + #verify_comment_visibility(comment_y, "y", "y") + #verify_comment_visibility(comment_if_n, "n", "n") + #verify_comment_visibility(comment_if_m, "n", "m") + #verify_comment_visibility(comment_if_y, "y", "y") + #verify_comment_visibility(comment_m_nested, "n", "m") # Verify that string/int/hex symbols with m visibility accept a user value - assign_and_verify_new_value("STRING_m", "foo bar", "foo bar") - assign_and_verify_new_value("INT_m", "123", "123") - assign_and_verify_new_value("HEX_m", "0x123", "0x123") + assign_and_verify("STRING_m", "foo bar") + assign_and_verify("INT_m", "123") + assign_and_verify("HEX_m", "0x123") # # Object relations @@ -1163,61 +1009,41 @@ def run_selftests(): c = kconfiglib.Config("Kconfiglib/tests/Krelation") - A, B, C, D, E, F, G, H, I = c["A"], c["B"], c["C"], c["D"], c["E"], c["F"],\ - c["G"], c["H"], c["I"] - choice_1, choice_2 = c.get_choices() - verify([menu.get_title() for menu in c.get_menus()] == - ["m1", "m2", "m3", "m4"], - "menu ordering is broken") - menu_1, menu_2, menu_3, menu_4 = c.get_menus() + UNDEFINED, A, B, C, D, E, F, G, H, I = \ + c.syms["UNDEFINED"], c.syms["A"], c.syms["B"], c.syms["C"], \ + c.syms["D"], c.syms["E"], c.syms["F"], c.syms["G"], c.syms["H"], \ + c.syms["I"] + choice_1, choice_2 = get_choices(c) + + # TODO: test new prompts + #verify([menu.title for menu in get_menus(c)[1:]] == + # ["m1", "m2", "m3", "m4"], + # "menu ordering is broken") + #menu_1, menu_2, menu_3, menu_4 = get_menus(c)[1:] print("Testing object relations...") - verify(A.get_parent() is None, "A should not have a parent") - verify(B.get_parent() is choice_1, "B's parent should be the first choice") - verify(C.get_parent() is choice_1, "C's parent should be the first choice") - verify(E.get_parent() is menu_1, "E's parent should be the first menu") - verify(E.get_parent().get_parent() is None, - "E's grandparent should be None") - verify(G.get_parent() is choice_2, + # TODO: check parents for menus + + verify(get_parent(UNDEFINED) is None, + "Undefined symbols should have no parent") + # TODO: update this test (should be the main menu) + # TODO: test parents when automatic menus are involved + #verify(A.get_parent() is None, "A should not have a parent") + verify(get_parent(B) is choice_1, "B's parent should be the first choice") + # TODO: no longer true due to auto menus + #verify(get_parent(C) is choice_1, "C's parent should be the first choice") + #verify(get_parent(E) is menu_1, "E's parent should be the first menu") + # TODO: update this test + #verify(E.get_parent().get_parent() is None, + # "E's grandparent should be None") + verify(get_parent(G) is choice_2, "G's parent should be the second choice") - verify(G.get_parent().get_parent() is menu_2, - "G's grandparent should be the second menu") - - # - # Object fetching (same test file) - # + #verify(get_parent(get_parent(G)) is menu_2, + # "G's grandparent should be the second menu") - print("Testing object fetching...") - - verify_equals(c.get_symbol("NON_EXISTENT"), None) - verify(c.get_symbol("A") is A, "get_symbol() is broken") - - verify(c.get_top_level_items() == [A, choice_1, menu_1, menu_3, menu_4], - "Wrong items at top level") - verify(c.get_symbols(False) == [A, B, C, D, E, F, G, H, I], - "get_symbols() is broken") - - verify(choice_1.get_items() == [B, C, D], - "Wrong get_items() items in 'choice'") - # Test Kconfig quirk - verify(choice_1.get_symbols() == [B, D], - "Wrong get_symbols() symbols in 'choice'") - - verify(menu_1.get_items() == [E, menu_2, I], "Wrong items in first menu") - verify(menu_1.get_symbols() == [E, I], "Wrong symbols in first menu") - verify(menu_1.get_items(True) == [E, menu_2, F, choice_2, G, H, I], - "Wrong recursive items in first menu") - verify(menu_1.get_symbols(True) == [E, F, G, H, I], - "Wrong recursive symbols in first menu") - verify(menu_2.get_items() == [F, choice_2], - "Wrong items in second menu") - verify(menu_2.get_symbols() == [F], - "Wrong symbols in second menu") - verify(menu_2.get_items(True) == [F, choice_2, G, H], - "Wrong recursive items in second menu") - verify(menu_2.get_symbols(True) == [F, G, H], - "Wrong recursive symbols in second menu") + # TODO: test parents of comments + # TODO: test top node # # hex/int ranges @@ -1227,16 +1053,16 @@ def run_selftests(): c = kconfiglib.Config("Kconfiglib/tests/Krange") - for sym_name in ("HEX_NO_RANGE", "INT_NO_RANGE", "HEX_40", "INT_40"): - sym = c[sym_name] - verify(not sym.has_ranges(), + for sym_name in "HEX_NO_RANGE", "INT_NO_RANGE", "HEX_40", "INT_40": + sym = c.syms[sym_name] + verify(not sym.ranges, "{} should not have ranges".format(sym_name)) - for sym_name in ("HEX_ALL_RANGES_DISABLED", "INT_ALL_RANGES_DISABLED", - "HEX_RANGE_10_20_LOW_DEFAULT", - "INT_RANGE_10_20_LOW_DEFAULT"): - sym = c[sym_name] - verify(sym.has_ranges(), "{} should have ranges".format(sym_name)) + for sym_name in "HEX_ALL_RANGES_DISABLED", "INT_ALL_RANGES_DISABLED", \ + "HEX_RANGE_10_20_LOW_DEFAULT", \ + "INT_RANGE_10_20_LOW_DEFAULT": + sym = c.syms[sym_name] + verify(sym.ranges, "{} should have ranges".format(sym_name)) # hex/int symbols without defaults should get no default value verify_value("HEX_NO_RANGE", "") @@ -1246,9 +1072,9 @@ def run_selftests(): verify_value("INT_ALL_RANGES_DISABLED", "") # Make sure they are assignable though, and test that the form of the user # value is reflected in the value for hex symbols - assign_and_verify_new_value("HEX_NO_RANGE", "0x123", "0x123") - assign_and_verify_new_value("HEX_NO_RANGE", "123", "123") - assign_and_verify_new_value("INT_NO_RANGE", "123", "123") + assign_and_verify("HEX_NO_RANGE", "0x123") + assign_and_verify("HEX_NO_RANGE", "123") + assign_and_verify("INT_NO_RANGE", "123") # Defaults outside of the valid range should be clamped verify_value("HEX_RANGE_10_20_LOW_DEFAULT", "0x10") @@ -1275,13 +1101,13 @@ def run_selftests(): """Tests that the values in the range 'low'-'high' can be assigned, and that assigning values outside this range reverts the value back to 'default' (None if it should revert back to "").""" - is_hex = (c[sym_name].get_type() == kconfiglib.HEX) + is_hex = (c.syms[sym_name].type == kconfiglib.HEX) for i in range(low, high + 1): - assign_and_verify_new_user_value(sym_name, str(i), str(i)) + assign_and_verify_user_value(sym_name, str(i), str(i)) if is_hex: # The form of the user value should be preserved for hex # symbols - assign_and_verify_new_user_value(sym_name, hex(i), hex(i)) + assign_and_verify_user_value(sym_name, hex(i), hex(i)) # Verify that assigning a user value just outside the range causes # defaults to be used @@ -1298,8 +1124,8 @@ def run_selftests(): too_low_str = str(low - 1) too_high_str = str(high + 1) - assign_and_verify_new_value(sym_name, too_low_str, default_str) - assign_and_verify_new_value(sym_name, too_high_str, default_str) + assign_and_verify_value(sym_name, too_low_str, default_str) + assign_and_verify_value(sym_name, too_high_str, default_str) verify_range("HEX_RANGE_10_20_LOW_DEFAULT", 0x10, 0x20, 0x10) verify_range("HEX_RANGE_10_20_HIGH_DEFAULT", 0x10, 0x20, 0x20) @@ -1321,15 +1147,15 @@ def run_selftests(): verify_value("HEX_40", "40") verify_value("INT_40", "40") - c["HEX_RANGE_10_20"].unset_user_value() - c["INT_RANGE_10_20"].unset_user_value() + c.syms["HEX_RANGE_10_20"].unset_value() + c.syms["INT_RANGE_10_20"].unset_value() verify_value("HEX_RANGE_10_40_DEPENDENT", "0x10") verify_value("INT_RANGE_10_40_DEPENDENT", "10") - c["HEX_RANGE_10_20"].set_user_value("15") - c["INT_RANGE_10_20"].set_user_value("15") + c.syms["HEX_RANGE_10_20"].set_value("15") + c.syms["INT_RANGE_10_20"].set_value("15") verify_value("HEX_RANGE_10_40_DEPENDENT", "0x15") verify_value("INT_RANGE_10_40_DEPENDENT", "15") - c.unset_user_values() + c.unset_values() verify_range("HEX_RANGE_10_40_DEPENDENT", 0x10, 0x40, 0x10) verify_range("INT_RANGE_10_40_DEPENDENT", 10, 40, 10) @@ -1338,184 +1164,64 @@ def run_selftests(): verify_value("INACTIVE_RANGE", "2") verify_value("ACTIVE_RANGE", "1") - # - # get_referenced_symbols() - # - - c = kconfiglib.Config("Kconfiglib/tests/Kref") - - # General function for checking get_referenced_symbols() output. - # Specialized for symbols below. - def verify_refs(item, refs_no_enclosing, refs_enclosing): - item_refs = item.get_referenced_symbols() - item_refs_enclosing = item.get_referenced_symbols(True) - - # For failure messages - if item.is_symbol(): - item_string = item.get_name() - elif item.is_choice(): - if item.get_name() is None: - item_string = "choice" - else: - item_string = "choice " + item.get_name() - elif item.is_menu(): - item_string = 'menu "{}"'.format(item.get_title()) - else: - # Comment - item_string = 'comment "{}"'.format(item.get_text()) - - verify(len(item_refs) == len(refs_no_enclosing), - "Wrong number of refs excluding enclosing for {}". - format(item_string)) - verify(len(item_refs_enclosing) == len(refs_enclosing), - "Wrong number of refs including enclosing for {}". - format(item_string)) - for r in [c[name] for name in refs_no_enclosing]: - verify(r in item_refs, - "{} should reference {} when excluding enclosing". - format(item_string, r.get_name())) - for r in [c[name] for name in refs_enclosing]: - verify(r in item_refs_enclosing, - "{} should reference {} when including enclosing". - format(item_string, r.get_name())) - - # Symbols referenced by symbols - - def verify_sym_refs(sym_name, refs_no_enclosing, refs_enclosing): - verify_refs(c[sym_name], refs_no_enclosing, refs_enclosing) - - verify_sym_refs("NO_REF", [], []) - verify_sym_refs("ONE_REF", ["A"], ["A"]) - own_refs = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", - "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", - "Y", "Z", "AA"] - verify_sym_refs("MANY_REF", - own_refs, - own_refs + ["IF_REF_1", "IF_REF_2", "MENU_REF_1", - "MENU_REF_2"]) - - # Symbols referenced by choices - - own_refs = ["CHOICE_REF_4", "CHOICE_REF_5", "CHOICE_REF_6"] - verify_refs(c.get_choices()[0], - own_refs, - own_refs + ["CHOICE_REF_1", "CHOICE_REF_2", "CHOICE_REF_3"]) - - # Symbols referenced by menus - - own_refs = ["NO_REF", "MENU_REF_3"] - verify_refs(c.get_menus()[1], - own_refs, - own_refs + ["MENU_REF_1", "MENU_REF_2"]) - - # Symbols referenced by comments - - own_refs = ["COMMENT_REF_3", "COMMENT_REF_4", "COMMENT_REF_5"] - verify_refs(c.get_comments()[0], - own_refs, - own_refs + ["COMMENT_REF_1", "COMMENT_REF_2"]) - - # - # get_selected_symbols() (same test file) - # - - def verify_selects(sym_name, selection_names): - sym = c[sym_name] - sym_selections = sym.get_selected_symbols() - verify(len(sym_selections) == len(selection_names), - "Wrong number of selects for {}".format(sym_name)) - for sel_name in selection_names: - sel_sym = c[sel_name] - verify(sel_sym in sym_selections, - "{} should be selected by {}".format(sel_name, sym_name)) - - verify_selects("n", []) - verify_selects("m", []) - verify_selects("y", []) - verify_selects("UNAME_RELEASE", []) - - verify_selects("NO_REF", []) - verify_selects("MANY_REF", ["I", "N"]) + # TODO: test symbol references in some other way? + # TODO: test selects in some other way? + # TODO: test implies in some other way? # - # get_implied_symbols() (same test file) + # defconfig_filename # - def verify_implies(sym_name, imply_names): - sym = c[sym_name] - sym_implies = sym.get_implied_symbols() - verify(len(sym_implies) == len(imply_names), - "Wrong number of implies for {}".format(sym_name)) - for imply_name in imply_names: - implied_sym = c[imply_name] - verify(implied_sym in sym_implies, - "{} should be implied by {}".format(imply_name, sym_name)) - - verify_implies("n", []) - verify_implies("m", []) - verify_implies("y", []) - verify_implies("UNAME_RELEASE", []) - - verify_implies("NO_REF", []) - verify_implies("MANY_REF", ["P", "U"]) - - # - # get_defconfig_filename() - # - - print("Testing get_defconfig_filename()...") + print("Testing defconfig_filename...") c = kconfiglib.Config("Kconfiglib/tests/empty") - verify(c.get_defconfig_filename() is None, - "get_defconfig_filename() should be None with no defconfig_list " - "symbol") + verify(c.defconfig_filename is None, + "defconfig_filename should be None with no defconfig_list symbol") c = kconfiglib.Config("Kconfiglib/tests/Kdefconfig_nonexistent") - verify(c.get_defconfig_filename() is None, - "get_defconfig_filename() should be None when none of the files " - "in the defconfig_list symbol exist") + verify(c.defconfig_filename is None, + "defconfig_filename should be None when none of the files in the " + "defconfig_list symbol exist") # Referenced in Kdefconfig_existent(_but_n) os.environ["BAR"] = "defconfig_2" c = kconfiglib.Config("Kconfiglib/tests/Kdefconfig_existent_but_n") - verify(c.get_defconfig_filename() is None, - "get_defconfig_filename() should be None when the condition is " - "n for all the defaults") + verify(c.defconfig_filename is None, + "defconfig_filename should be None when the condition is n for all " + "the defaults") c = kconfiglib.Config("Kconfiglib/tests/Kdefconfig_existent") - verify(c.get_defconfig_filename() == "Kconfiglib/tests/defconfig_2", - "get_defconfig_filename() should return the existent file " + verify(c.defconfig_filename == "Kconfiglib/tests/defconfig_2", + "defconfig_filename should return the existent file " "Kconfiglib/tests/defconfig_2") # Should also look relative to $srctree if the defconfig is an absolute # path and not found - del os.environ["srctree"] c = kconfiglib.Config("Kconfiglib/tests/Kdefconfig_srctree") - verify(c.get_defconfig_filename() == "Kconfiglib/tests/defconfig_2", - "get_defconfig_filename() returned wrong file with $srctree unset") + verify(c.defconfig_filename == "Kconfiglib/tests/defconfig_2", + "defconfig_filename gave wrong file with $srctree unset") os.environ["srctree"] = "Kconfiglib/tests" c = kconfiglib.Config("Kconfiglib/tests/Kdefconfig_srctree") - verify(c.get_defconfig_filename() == - "Kconfiglib/tests/sub/defconfig_in_sub", - "get_defconfig_filename() returned wrong file with $srctree set") + verify(c.defconfig_filename == "Kconfiglib/tests/sub/defconfig_in_sub", + "defconfig_filename gave wrong file with $srctree set") # - # get_mainmenu_text() + # mainmenu_text # - print("Testing get_mainmenu_text()...") + print("Testing mainmenu_text...") c = kconfiglib.Config("Kconfiglib/tests/empty") - verify(c.get_mainmenu_text() is None, - "An empty Kconfig should not have a mainmenu text") + verify(c.mainmenu_text == "Linux Kernel Configuration", + "An empty Kconfig should get a default main menu prompt") # Expanded in the mainmenu text os.environ["FOO"] = "bar baz" c = kconfiglib.Config("Kconfiglib/tests/Kmainmenu") - verify(c.get_mainmenu_text() == "---bar baz---", + verify(c.mainmenu_text == "---bar baz---", "Wrong mainmenu text") # @@ -1524,134 +1230,95 @@ def run_selftests(): os.environ["ENV_VAR"] = "foo" # Contains reference to undefined environment variable, so disable warnings - c = kconfiglib.Config("Kconfiglib/tests/Kmisc", print_warnings = False) + c = kconfiglib.Config("Kconfiglib/tests/Kmisc", warn = False) - print("Testing is_optional()...") + print("Testing is_optional...") - verify(not c.get_choices()[0].is_optional(), + verify(not get_choices(c)[0].is_optional, "First choice should not be optional") - verify(c.get_choices()[1].is_optional(), + verify(get_choices(c)[1].is_optional, "Second choice should be optional") - print("Testing get_user_value()...") + print("Testing user_value...") # Avoid warnings from assigning invalid user values and assigning user # values to symbols without prompts - c.set_print_warnings(False) + c.disable_warnings() - syms = [c[name] for name in \ + syms = [c.syms[name] for name in \ ("BOOL", "TRISTATE", "STRING", "INT", "HEX")] for sym in syms: - verify(sym.get_user_value() is None, + verify(sym.user_value is None, "{} should not have a user value to begin with") # Assign valid values for the types - assign_and_verify_new_user_value("BOOL", "n", "n") - assign_and_verify_new_user_value("BOOL", "y", "y") - assign_and_verify_new_user_value("TRISTATE", "n", "n") - assign_and_verify_new_user_value("TRISTATE", "m", "m") - assign_and_verify_new_user_value("TRISTATE", "y", "y") - assign_and_verify_new_user_value("STRING", "foo bar", "foo bar") - assign_and_verify_new_user_value("INT", "123", "123") - assign_and_verify_new_user_value("HEX", "0x123", "0x123") + assign_and_verify_user_value("BOOL", "n", "n") + assign_and_verify_user_value("BOOL", "y", "y") + assign_and_verify_user_value("TRISTATE", "n", "n") + assign_and_verify_user_value("TRISTATE", "m", "m") + assign_and_verify_user_value("TRISTATE", "y", "y") + assign_and_verify_user_value("STRING", "foo bar", "foo bar") + assign_and_verify_user_value("INT", "123", "123") + assign_and_verify_user_value("HEX", "0x123", "0x123") # Assign invalid values for the types. They should retain their old user # value. - assign_and_verify_new_user_value("BOOL", "m", "y") - assign_and_verify_new_user_value("BOOL", "foo", "y") - assign_and_verify_new_user_value("BOOL", "1", "y") - assign_and_verify_new_user_value("TRISTATE", "foo", "y") - assign_and_verify_new_user_value("TRISTATE", "1", "y") - assign_and_verify_new_user_value("INT", "foo", "123") - assign_and_verify_new_user_value("HEX", "foo", "0x123") + assign_and_verify_user_value("BOOL", "m", "y") + assign_and_verify_user_value("BOOL", "foo", "y") + assign_and_verify_user_value("BOOL", "1", "y") + assign_and_verify_user_value("TRISTATE", "foo", "y") + assign_and_verify_user_value("TRISTATE", "1", "y") + assign_and_verify_user_value("INT", "foo", "123") + assign_and_verify_user_value("HEX", "foo", "0x123") for s in syms: - s.unset_user_value() - verify(s.get_user_value() is None, + s.unset_value() + verify(s.user_value is None, "{} should not have a user value after being reset". - format(s.get_name())) - - print("Testing is_defined()...") - - for sym_name in ("n", "m", "y", "UNAME_RELEASE", "A", "B", "C", "D", - "BOOL", "TRISTATE", "STRING", "INT", "HEX"): - sym = c[sym_name] - verify(sym.is_defined(), - "{} should be defined".format(sym_name)) - - for sym_name in ("NOT_DEFINED_1", "NOT_DEFINED_2", "NOT_DEFINED_3", - "NOT_DEFINED_4"): - sym = c[sym_name] - verify(not sym.is_defined(), - "{} should not be defined".format(sym_name)) - - print("Testing is_special()...") - - for sym_name in ("n", "m", "y", "UNAME_RELEASE", "FROM_ENV", - "FROM_ENV_MISSING"): - sym = c[sym_name] - verify(sym.is_special(), - "{} should be special".format(sym_name)) - - for sym_name in ("A", "B", "C", "D", "BOOL", "TRISTATE", "STRING", - "INT", "HEX", "NOT_DEFINED_1", "NOT_DEFINED_2", - "NOT_DEFINED_3", "NOT_DEFINED_4"): - sym = c[sym_name] - verify(not sym.is_special(), - "{} should not be special".format(sym_name)) - - print("Testing is_from_environment()...") - - for sym_name in ("FROM_ENV", "FROM_ENV_MISSING"): - sym = c[sym_name] - verify(sym.is_from_environment(), - "{} should be from the environment".format(sym_name)) - - for sym_name in ("n", "m", "y", "UNAME_RELEASE", "A", "B", "C", "D", - "BOOL", "TRISTATE", "STRING", "INT", "HEX", - "NOT_DEFINED_1", "NOT_DEFINED_2", "NOT_DEFINED_3", - "NOT_DEFINED_4"): - sym = c[sym_name] - verify(not sym.is_from_environment(), - "{} should not be from the environment".format(sym_name)) - - print("Testing is_choice_symbol()...") - - for sym_name in ("A", "B", "C", "D"): - sym = c[sym_name] - verify(sym.is_choice_symbol(), - "{} should be a choice symbol".format(sym_name)) - - for sym_name in ("n", "m", "y", "UNAME_RELEASE", "Q1", "Q2", "Q3", "BOOL", - "TRISTATE", "STRING", "INT", "HEX", "FROM_ENV", - "FROM_ENV_MISSING", "NOT_DEFINED_1", "NOT_DEFINED_2", - "NOT_DEFINED_3", "NOT_DEFINED_4"): - sym = c[sym_name] - verify(not sym.is_choice_symbol(), - "{} should not be a choice symbol".format(sym_name)) - - print("Testing is_allnoconfig_y()...") - - verify(not c["NOT_ALLNOCONFIG_Y"].is_allnoconfig_y(), + format(s.name)) + + print("Testing defined vs undefined symbols...") + + for name in "A", "B", "C", "D", "BOOL", "TRISTATE", "STRING", "INT", "HEX": + verify(c.syms[name].nodes, + "{} should be defined".format(name)) + + for name in "NOT_DEFINED_1", "NOT_DEFINED_2", "NOT_DEFINED_3", \ + "NOT_DEFINED_4": + sym = c.syms[name] + verify(not c.syms[name].nodes, + "{} should not be defined".format(name)) + + print("Testing Symbol.choice...") + + for name in "A", "B", "C", "D": + verify(c.syms[name].choice is not None, + "{} should be a choice symbol".format(name)) + + for name in "Q1", "Q2", "Q3", "BOOL", "TRISTATE", "STRING", "INT", "HEX", \ + "FROM_ENV", "FROM_ENV_MISSING", "NOT_DEFINED_1", \ + "NOT_DEFINED_2", "NOT_DEFINED_3", "NOT_DEFINED_4": + verify(c.syms[name].choice is None, + "{} should not be a choice symbol".format(name)) + + print("Testing is_allnoconfig_y...") + + verify(not c.syms["NOT_ALLNOCONFIG_Y"].is_allnoconfig_y, "NOT_ALLNOCONFIG_Y should not be allnoconfig_y") - verify(c["ALLNOCONFIG_Y"].is_allnoconfig_y(), + verify(c.syms["ALLNOCONFIG_Y"].is_allnoconfig_y, "ALLNOCONFIG_Y should be allnoconfig_y") - print("Testing UNAME_RELEASE value...") + print("Testing UNAME_RELEASE...") verify_value("UNAME_RELEASE", platform.uname()[2]) - - # Expansion of environment variables in Config.__init__'s base_dir - # parameter. Just make sure we don't crash when Kbase_dir 'source's a file - # from the same directory. - - os.environ["EnV_VaR1"] = "Kconfigl" - os.environ["EnV_VaR2"] = "ib/tests" - kconfiglib.Config("Kconfiglib/tests/Kbase_dir", - base_dir="$EnV_VaR1$EnV_VaR2/") + ur = c.syms["UNAME_RELEASE"] + verify(ur.config is c and + ur.type == kconfiglib.STRING and + ur.env_var == "", + "UNAME_RELEASE has wrong fields") # # .config reading and writing @@ -1664,9 +1331,9 @@ def run_selftests(): def write_and_verify_header(header): c.write_config(config_test_file, header) c.load_config(config_test_file) - verify(c.get_config_header() == header, + verify(c.config_header == header, "The header {} morphed into {} on loading" - .format(repr(header), repr(c.get_config_header()))) + .format(repr(header), repr(c.config_header))) def verify_file_contents(fname, contents): with open(fname, "r") as f: @@ -1680,12 +1347,12 @@ def run_selftests(): c = kconfiglib.Config("Kconfiglib/tests/Kescape") # Test the default value - c.write_config(config_test_file + "_from_def") + c.write_config(config_test_file + "_from_def", header="") verify_file_contents(config_test_file + "_from_def", r'''CONFIG_STRING="\"\\"''' "\n") # Write our own value - c["STRING"].set_user_value(r'''\"a'\\''') - c.write_config(config_test_file + "_from_user") + c.syms["STRING"].set_value(r'''\"a'\\''') + c.write_config(config_test_file + "_from_user", header="") verify_file_contents(config_test_file + "_from_user", r'''CONFIG_STRING="\\\"a'\\\\"''' "\n") @@ -1695,46 +1362,6 @@ def run_selftests(): c.load_config(config_test_file + "_from_user") verify_value("STRING", r'''\"a'\\''') - # Reading and writing of .config headers - - verify(c.get_config_header() is None, - "Expected no header before .config loaded, got '{}'". - format(c.get_config_header())) - - write_and_verify_header("") - write_and_verify_header(" ") - write_and_verify_header("\n") - write_and_verify_header("\n\n") - write_and_verify_header("#") - write_and_verify_header("a") - write_and_verify_header("a\n") - write_and_verify_header("a\n\n") - write_and_verify_header("abcdef") - write_and_verify_header("foo\nbar baz\n\n\n qaz#") - - c.load_config("Kconfiglib/tests/empty") - verify(c.get_config_header() is None, - "Expected no header in empty .config, got '{}'". - format(c.get_config_header())) - - c.load_config("Kconfiglib/tests/config_hash") - verify(c.get_config_header() == "", - "Expected empty header in file with just '#', got '{}'". - format(c.get_config_header())) - - # TODO: Line joining (which stems from _FileFeed reuse) probably doesn't - # make sense within .config files. (The C implementation has no notion of - # continuation lines within .config files.) It's harmless except for fairly - # obscure cases though. - # - # Add a test for now just to get test coverage for _FileFeed.peek_next(), - # which is only used while reading .config files as of writing. - - c.load_config("Kconfiglib/tests/config_continuation") - verify(c.get_config_header() == - " Foo # Bar\n Baz # Foo # Bar\n Baz\n Foo", - "Continuation line handling within .config headers is broken") - # Appending values from a .config c = kconfiglib.Config("Kconfiglib/tests/Kappend") @@ -1767,70 +1394,30 @@ def run_selftests(): verify_value("IGNOREME", "y") # - # get_config() + # .config # - print("Testing get_config()...") + print("Testing .config...") - c1 = kconfiglib.Config("Kconfiglib/tests/Kmisc", print_warnings = False) - c2 = kconfiglib.Config("Kconfiglib/tests/Kmisc", print_warnings = False) + c1 = kconfiglib.Config("Kconfiglib/tests/Kmisc", warn = False) + c2 = kconfiglib.Config("Kconfiglib/tests/Kmisc", warn = False) - c1_bool, c1_choice, c1_menu, c1_comment = c1["BOOL"], \ - c1.get_choices()[0], c1.get_menus()[0], c1.get_comments()[0] - c2_bool, c2_choice, c2_menu, c2_comment = c2["BOOL"], \ - c2.get_choices()[0], c2.get_menus()[0], c2.get_comments()[0] + c1_undef, c1_bool, c1_choice, c1_menu, c1_comment = c1.syms["BOOL"], \ + c1.syms["NOT_DEFINED_1"], get_choices(c1)[0], get_menus(c1)[0], \ + get_comments(c1)[0] + c2_undef, c2_bool, c2_choice, c2_menu, c2_comment = c2.syms["BOOL"], \ + c2.syms["NOT_DEFINED_1"], get_choices(c2)[0], get_menus(c2)[0], \ + get_comments(c2)[0] - verify((c1_bool is not c2_bool) and (c1_choice is not c2_choice) and - (c1_menu is not c2_menu) and (c1_comment is not c2_comment) and - (c1_bool.get_config() is c1) and (c2_bool.get_config() is c2) and - (c1_choice.get_config() is c1) and (c2_choice.get_config() is c2) and - (c1_menu.get_config() is c1) and (c2_menu.get_config() is c2) and - (c1_comment.get_config() is c1) and (c2_comment.get_config() is c2), - "Config instance state separation or get_config() is broken") - - # - # get_arch/srcarch/srctree/kconfig_filename() - # - - del os.environ["ARCH"] - del os.environ["SRCARCH"] - del os.environ["srctree"] - - c = kconfiglib.Config("Kconfiglib/tests/Kmisc", print_warnings = False) - arch = c.get_arch() - verify(arch is None, "Expected None arch, got '{}'".format(arch)) - srcarch = c.get_srcarch() - verify(srcarch is None, "Expected None srcarch, got '{}'".format(srcarch)) - srctree = c.get_srctree() - verify(srctree is None, "Expected None srctree, got '{}'".format(srctree)) - - os.environ["ARCH"] = "ARCH value" - os.environ["SRCARCH"] = "SRCARCH value" - os.environ["srctree"] = "srctree value" - c = kconfiglib.Config("Kconfiglib/tests/Kmisc", print_warnings = False) - c.load_config("Kconfiglib/tests/empty") - - arch = c.get_arch() - srcarch = c.get_srcarch() - srctree = c.get_srctree() - config_filename = c.get_config_filename() - kconfig_filename = c.get_kconfig_filename() - - print("Testing get_arch()...") - verify(arch == "ARCH value", - "Wrong arch value - got '{}'".format(arch)) - print("Testing get_srcarch()...") - verify(srcarch == "SRCARCH value", - "Wrong srcarch value - got '{}'".format(srcarch)) - print("Testing get_srctree()...") - verify(srctree == "srctree value", - "Wrong srctree value - got '{}'".format(srctree)) - print("Testing get_config_filename()...") - verify(config_filename == "Kconfiglib/tests/empty", - "Wrong config filename - got '{}'".format(config_filename)) - print("Testing get_kconfig_filename()...") - verify(kconfig_filename == "Kconfiglib/tests/Kmisc", - "Wrong Kconfig filename - got '{}'".format(kconfig_filename)) + verify((c1_undef is not c2_undef) and (c1_bool is not c2_bool) and + (c1_choice is not c2_choice) and (c1_menu is not c2_menu) and + (c1_comment is not c2_comment) and + (c1_undef.config is c1) and (c2_undef.config is c2) and + (c1_bool.config is c1) and (c2_bool.config is c2) and + (c1_choice.config is c1) and (c2_choice.config is c2) and + (c1_menu.config is c1) and (c2_menu.config is c2) and + (c1_comment.config is c1) and (c2_comment.config is c2), + "Config instance state separation or .config is broken") # # Imply semantics @@ -1887,7 +1474,7 @@ def run_selftests(): assign_and_verify("IMPLIED_TRISTATE", "n") assign_and_verify("IMPLIED_TRISTATE", "m") assign_and_verify("IMPLIED_TRISTATE", "y") - c["IMPLIED_TRISTATE"].unset_user_value() + c.syms["IMPLIED_TRISTATE"].unset_value() verify_value("IMPLIED_TRISTATE", "n") # Same as above for "m". Anything still goes, but "m" by default now. @@ -1896,7 +1483,7 @@ def run_selftests(): assign_and_verify("IMPLIED_TRISTATE", "n") assign_and_verify("IMPLIED_TRISTATE", "m") assign_and_verify("IMPLIED_TRISTATE", "y") - c["IMPLIED_TRISTATE"].unset_user_value() + c.syms["IMPLIED_TRISTATE"].unset_value() verify_value("IMPLIED_TRISTATE", "m") # Same as above for "y". Only "n" and "y" should be accepted. "m" gets @@ -1904,14 +1491,14 @@ def run_selftests(): assign_and_verify("IMPLY", "y") assign_and_verify("IMPLIED_TRISTATE", "n") - assign_and_verify_new_value("IMPLIED_TRISTATE", "m", "y") + assign_and_verify_value("IMPLIED_TRISTATE", "m", "y") assign_and_verify("IMPLIED_TRISTATE", "y") - c["IMPLIED_TRISTATE"].unset_user_value() + c.syms["IMPLIED_TRISTATE"].unset_value() verify_value("IMPLIED_TRISTATE", "y") # Being implied to either "m" or "y" should give a bool the value "y" - c["IMPLY"].unset_user_value() + c.syms["IMPLY"].unset_value() verify_value("IMPLIED_BOOL", "n") assign_and_verify("IMPLY", "n") verify_value("IMPLIED_BOOL", "n") @@ -1922,11 +1509,11 @@ def run_selftests(): # A bool implied to "m" or "y" can take the values "n" and "y" - c["IMPLY"].set_user_value("m") + c.syms["IMPLY"].set_value("m") assign_and_verify("IMPLIED_BOOL", "n") assign_and_verify("IMPLIED_BOOL", "y") - c["IMPLY"].set_user_value("y") + c.syms["IMPLY"].set_value("y") assign_and_verify("IMPLIED_BOOL", "n") assign_and_verify("IMPLIED_BOOL", "y") @@ -1942,90 +1529,82 @@ def run_selftests(): choice_bool_m, choice_tristate_m, choice_defaults, \ choice_defaults_not_visible, choice_no_type_bool, \ choice_no_type_tristate, choice_missing_member_type_1, \ - choice_missing_member_type_2, choice_weird_syms = c.get_choices() + choice_missing_member_type_2, choice_weird_syms = get_choices(c) for choice in (choice_bool, choice_bool_opt, choice_bool_m, choice_defaults): - verify(choice.get_type() == kconfiglib.BOOL, - "choice {} should have type bool".format(choice.get_name())) + verify(choice.type == kconfiglib.BOOL, + "choice {} should have type bool".format(choice.name)) - for choice in (choice_tristate, choice_tristate_opt, choice_tristate_m): - verify(choice.get_type() == kconfiglib.TRISTATE, - "choice {} should have type tristate" - .format(choice.get_name())) + # TODO: fix this laters. type automatically changed. + #for choice in (choice_tristate, choice_tristate_opt, choice_tristate_m): + # verify(choice.type == kconfiglib.TRISTATE, + # "choice {} should have type tristate" + # .format(choice.name)) def select_and_verify(sym): - choice = sym.get_parent() - sym.set_user_value("y") - verify(choice.get_mode() == "y", + choice = get_parent(sym) + sym.set_value("y") + verify(choice.value == "y", 'The mode of the choice should be "y" after selecting a ' "symbol") - verify(sym.is_choice_selection(), - "is_choice_selection() should be true for {}" - .format(sym.get_name())) - verify(choice.get_selection() is sym, - "{} should be the selected symbol".format(sym.get_name())) - verify(choice.get_user_selection() is sym, + verify(sym.choice.selection is sym, + "{} should be the selected choice symbol" + .format(sym.name)) + verify(choice.selection is sym, + "{} should be the selected symbol".format(sym.name)) + verify(choice.user_selection is sym, "{} should be the user selection of the choice" - .format(sym.get_name())) + .format(sym.name)) def select_and_verify_all(choice): - choice_syms = choice.get_symbols() # Select in forward order - for sym in choice_syms: + for sym in choice.syms: select_and_verify(sym) # Select in reverse order - for i in range(len(choice_syms) - 1, 0, -1): - select_and_verify(choice_syms[i]) + for i in range(len(choice.syms) - 1, 0, -1): + select_and_verify(choice.syms[i]) def verify_mode(choice, no_modules_mode, modules_mode): - c["MODULES"].set_user_value("n") - choice_mode = choice.get_mode() + c.syms["MODULES"].set_value("n") + choice_mode = choice.value verify(choice_mode == no_modules_mode, 'Wrong mode for choice {} with no modules. Expected "{}", ' - 'got "{}".'.format(choice.get_name(), no_modules_mode, - choice_mode)) + 'got "{}".'.format(choice.name, no_modules_mode, choice_mode)) - c["MODULES"].set_user_value("y") - choice_mode = choice.get_mode() + c.syms["MODULES"].set_value("y") + choice_mode = choice.value verify(choice_mode == modules_mode, 'Wrong mode for choice {} with modules. Expected "{}", ' - 'got "{}".'.format(choice.get_name(), modules_mode, + 'got "{}".'.format(choice.name, modules_mode, choice_mode)) verify_mode(choice_bool, "y", "y") verify_mode(choice_bool_opt, "n", "n") verify_mode(choice_tristate, "y", "m") verify_mode(choice_tristate_opt, "n", "n") - verify_mode(choice_bool_m, "n", "y") # Promoted - verify_mode(choice_tristate_m, "n", "m") + verify_mode(choice_bool_m, "y", "y") + verify_mode(choice_tristate_m, "y", "m") # Test defaults - c["TRISTATE_SYM"].set_user_value("n") - verify(choice_defaults.get_selection_from_defaults() is c["OPT_4"] and - choice_defaults.get_selection() is c["OPT_4"], + c.syms["TRISTATE_SYM"].set_value("n") + verify(choice_defaults.selection is c.syms["OPT_4"], "Wrong choice default with TRISTATE_SYM = n") - c["TRISTATE_SYM"].set_user_value("y") - verify(choice_defaults.get_selection_from_defaults() is c["OPT_2"] and - choice_defaults.get_selection() is c["OPT_2"], + c.syms["TRISTATE_SYM"].set_value("y") + verify(choice_defaults.selection is c.syms["OPT_2"], "Wrong choice default with TRISTATE_SYM = y") - c["OPT_1"].set_user_value("y") - verify(choice_defaults.get_selection_from_defaults() is c["OPT_2"], - "User selection changed default selection - shouldn't have") - verify(choice_defaults.get_selection() is c["OPT_1"], + c.syms["OPT_1"].set_value("y") + verify(choice_defaults.selection is c.syms["OPT_1"], "User selection should override defaults") - verify(choice_defaults_not_visible.get_selection_from_defaults() - is c["OPT_8"] and - choice_defaults_not_visible.get_selection() - is c["OPT_8"], + verify(choice_defaults_not_visible.selection is c.syms["OPT_8"], "Non-visible choice symbols should cause the next default to be " "considered") # Test "y" mode selection - c["MODULES"].set_user_value("y") + c.syms["MODULES"].set_value("y") select_and_verify_all(choice_bool) select_and_verify_all(choice_bool_opt) @@ -2039,70 +1618,76 @@ def run_selftests(): # ...for a choice that can also be in "y" mode for sym_name in ("T_1", "T_2"): - assign_and_verify_new_value(sym_name, "m", "m") - verify(choice_tristate.get_mode() == "m", + assign_and_verify_value(sym_name, "m", "m") + verify(choice_tristate.value == "m", 'Selecting {} to "m" should have changed the mode of the ' 'choice to "m"'.format(sym_name)) - assign_and_verify_new_value(sym_name, "y", "y") - verify(choice_tristate.get_mode() == "y" and - choice_tristate.get_selection() is c[sym_name], + assign_and_verify_value(sym_name, "y", "y") + verify(choice_tristate.value == "y" and + choice_tristate.selection is c.syms[sym_name], 'Selecting {} to "y" should have changed the mode of the ' 'choice to "y" and made it the selection'.format(sym_name)) # ...for a choice that can only be in "m" mode for sym_name in ("TM_1", "TM_2"): - assign_and_verify_new_value(sym_name, "m", "m") - assign_and_verify_new_value(sym_name, "n", "n") + assign_and_verify_value(sym_name, "m", "m") + assign_and_verify_value(sym_name, "n", "n") # "y" should be truncated - assign_and_verify_new_value(sym_name, "y", "m") - verify(choice_tristate_m.get_mode() == "m", + assign_and_verify_value(sym_name, "y", "m") + verify(choice_tristate_m.value == "m", 'A choice that can only be in "m" mode was not') # Verify that choices with no explicitly specified type get the type of the # first contained symbol with a type - verify(choice_no_type_bool.get_type() == kconfiglib.BOOL, + verify(choice_no_type_bool.type == kconfiglib.BOOL, "Expected first choice without explicit type to have type bool") - verify(choice_no_type_tristate.get_type() == kconfiglib.TRISTATE, + verify(choice_no_type_tristate.type == kconfiglib.TRISTATE, "Expected second choice without explicit type to have type " "tristate") # Verify that symbols without a type in the choice get the type of the # choice - verify((c["MMT_1"].get_type(), c["MMT_2"].get_type(), - c["MMT_3"].get_type()) == + verify((c.syms["MMT_1"]._type, c.syms["MMT_2"]._type, + c.syms["MMT_3"]._type) == (kconfiglib.BOOL, kconfiglib.BOOL, kconfiglib.TRISTATE), "Wrong types for first choice with missing member types") - verify((c["MMT_4"].get_type(), c["MMT_5"].get_type()) == + verify((c.syms["MMT_4"]._type, c.syms["MMT_5"]._type) == (kconfiglib.BOOL, kconfiglib.BOOL), "Wrong types for second choice with missing member types") # Verify that symbols in choices that depend on the preceding symbol aren't # considered choice symbols - def verify_is_normal_choice_symbol(sym): - verify(sym.is_choice_symbol() and - sym in choice_weird_syms.get_symbols() and - sym.get_parent() is choice_weird_syms, - "{} should be a normal choice symbol".format(sym.get_name())) - - def verify_is_weird_choice_symbol(sym): - verify(not sym.is_choice_symbol() and - sym not in choice_weird_syms.get_symbols() and - sym in choice_weird_syms.get_items() and - sym.get_parent() is choice_weird_syms, - "{} should be a weird (non-)choice symbol") - - verify_is_normal_choice_symbol(c["WS1"]) - verify_is_weird_choice_symbol(c["WS2"]) - verify_is_weird_choice_symbol(c["WS3"]) - verify_is_weird_choice_symbol(c["WS4"]) - verify_is_normal_choice_symbol(c["WS5"]) - verify_is_weird_choice_symbol(c["WS6"]) + def verify_is_normal_choice_symbol(name): + sym = c.syms[name] + verify(sym.choice is not None and + sym in choice_weird_syms.syms and + get_parent(sym) is choice_weird_syms, + "{} should be a normal choice symbol".format(sym.name)) + + # TODO: parent stuff + + def verify_is_weird_choice_symbol(name): + sym = c.syms[name] + verify(sym.choice is None and + sym not in choice_weird_syms.syms, + "{} should be a weird (non-)choice symbol" + .format(sym.name)) + + verify_is_normal_choice_symbol("WS1") + verify_is_weird_choice_symbol("WS2") + verify_is_weird_choice_symbol("WS3") + verify_is_weird_choice_symbol("WS4") + verify_is_weird_choice_symbol("WS5") + verify_is_normal_choice_symbol("WS6") + verify_is_weird_choice_symbol("WS7") + verify_is_weird_choice_symbol("WS8") + verify_is_normal_choice_symbol("WS9") # # Object dependencies @@ -2115,27 +1700,29 @@ def run_selftests(): c = kconfiglib.Config("Kconfiglib/tests/Kdep") def verify_dependent(sym_name, deps_names): - sym = c[sym_name] - deps = [c[name] for name in deps_names] + sym = c.syms[sym_name] + deps = [c.syms[name] for name in deps_names] sym_deps = sym._get_dependent() - verify(len(sym_deps) == len(deps), - "Wrong number of dependent symbols for {}".format(sym_name)) verify(len(sym_deps) == len(set(sym_deps)), "{}'s dependencies contains duplicates".format(sym_name)) + sym_deps = [item for item in sym_deps + if not isinstance(item, kconfiglib.Choice)] + verify(len(sym_deps) == len(deps), + "Wrong number of dependent symbols for {}".format(sym_name)) for dep in deps: verify(dep in sym_deps, "{} should depend on {}". - format(dep.get_name(), sym_name)) + format(dep.name, sym_name)) # Test twice to cover dependency caching for i in range(0, 2): n_deps = 39 # Verify that D1, D2, .., D are dependent on D - verify_dependent("D", ["D{}".format(i) for i in range(1, n_deps + 1)]) + verify_dependent("D", ("D{}".format(i) for i in range(1, n_deps + 1))) # Choices - verify_dependent("A", ["B", "C"]) - verify_dependent("B", ["A", "C"]) - verify_dependent("C", ["A", "B"]) - verify_dependent("S", ["A", "B", "C"]) + verify_dependent("A", ("B", "C")) + verify_dependent("B", ("A", "C")) + verify_dependent("C", ("A", "B")) + verify_dependent("S", ("A", "B", "C")) # Verify that the last symbol depends on the first in a long chain of # dependencies. Test twice to cover dependency caching. @@ -2143,21 +1730,28 @@ def run_selftests(): c = kconfiglib.Config("Kconfiglib/tests/Kchain") for i in range(0, 2): - verify(c["CHAIN_26"] in c["CHAIN_1"]._get_dependent(), + verify(c.syms["CHAIN_26"] in c.syms["CHAIN_1"]._get_dependent(), "Dependency chain broken") - print("\nAll selftests passed\n" if _all_ok else + print("Testing compatibility with weird selects/implies...") + + # Check that Kconfiglib doesn't crash for stuff like 'select n' (seen in + # U-Boot). These probably originate from misunderstandings of how Kconfig + # works. + kconfiglib.Config("Kconfiglib/tests/Kwtf") + + print("\nAll selftests passed\n" if all_passed else "\nSome selftests failed\n") def run_compatibility_tests(): """Runs tests on configurations from the kernel. Tests compability with the C implementation by comparing outputs.""" - del os.environ["ARCH"] - del os.environ["SRCARCH"] - del os.environ["srctree"] + os.environ.pop("ARCH", None) + os.environ.pop("SRCARCH", None) + os.environ.pop("srctree", None) - if speedy_mode and not os.path.exists("scripts/kconfig/conf"): + if speedy and not os.path.exists("scripts/kconfig/conf"): print("\nscripts/kconfig/conf does not exist -- running " "'make allnoconfig' to build it...") shell("make allnoconfig") @@ -2193,11 +1787,11 @@ def run_compatibility_tests(): # Previously we used to load all the arches once and keep them # around for the tests. That now uses a huge amount of memory (pypy # helps a bit), so reload them for each test instead. - test_fn(kconfiglib.Config(base_dir=".")) + test_fn(kconfiglib.Config(), arch) # Let kbuild infer SRCARCH from ARCH if we aren't in speedy mode. # This could detect issues with the test suite. - if not speedy_mode: + if not speedy: del os.environ["SRCARCH"] if compare_configs: @@ -2207,7 +1801,7 @@ def run_compatibility_tests(): print(" {:14}FAIL".format(arch)) fail() - if all_ok(): + if all_passed: print("All selftests and compatibility tests passed") print(nconfigs, "arch/defconfig pairs tested") else: @@ -2240,13 +1834,13 @@ def get_arch_srcarch_list(): return res -def test_load(conf): +def test_load(conf, arch): """Load all arch Kconfigs to make sure we don't throw any errors""" - print(" {:14}OK".format(conf.get_arch())) + print(" {:14}OK".format(arch)) # The weird docstring formatting is to get the format right when we print the # docstring ourselves -def test_all_no(conf): +def test_all_no(conf, arch): """ Verify that our examples/allnoconfig.py script generates the same .config as 'make allnoconfig', for each architecture. Runs the script via @@ -2256,12 +1850,12 @@ def test_all_no(conf): shell("make scriptconfig SCRIPT=Kconfiglib/examples/allnoconfig.py " "PYTHONCMD='{}'".format(sys.executable)) shell("mv .config ._config") - if speedy_mode: + if speedy: shell("scripts/kconfig/conf --allnoconfig Kconfig") else: shell("make allnoconfig") -def test_all_no_simpler(conf): +def test_all_no_simpler(conf, arch): """ Verify that our examples/allnoconfig_simpler.py script generates the same .config as 'make allnoconfig', for each architecture. Runs the script via @@ -2271,12 +1865,12 @@ def test_all_no_simpler(conf): shell("make scriptconfig SCRIPT=Kconfiglib/examples/allnoconfig_simpler.py " "PYTHONCMD='{}'".format(sys.executable)) shell("mv .config ._config") - if speedy_mode: + if speedy: shell("scripts/kconfig/conf --allnoconfig Kconfig") else: shell("make allnoconfig") -def test_all_yes(conf): +def test_all_yes(conf, arch): """ Verify that our examples/allyesconfig.py script generates the same .config as 'make allyesconfig', for each architecture. Runs the script via @@ -2286,157 +1880,59 @@ def test_all_yes(conf): shell("make scriptconfig SCRIPT=Kconfiglib/examples/allyesconfig.py " "PYTHONCMD='{}'".format(sys.executable)) shell("mv .config ._config") - if speedy_mode: + if speedy: shell("scripts/kconfig/conf --allyesconfig Kconfig") else: shell("make allyesconfig") -def test_call_all(conf): +def test_call_all(conf, arch): """ Call all public methods on all symbols, menus, choices, and comments for all architectures to make sure we never crash or hang. (Nearly all public methods: some are hard to test like this, but are exercised by other - tests.) Also do misc. sanity checks.""" - print(" For {}...".format(conf.get_arch())) - - conf.__str__() - conf.get_arch() - conf.get_base_dir() - conf.get_config_filename() - conf.get_config_header() - conf.get_defconfig_filename() - conf.get_kconfig_filename() - conf.get_mainmenu_text() - conf.get_srcarch() - conf.get_srctree() - conf.get_symbol("y") - conf.get_symbols(False) - conf.get_top_level_items() - conf.set_print_undef_assign(True) - conf.set_print_undef_assign(False) - conf.set_print_warnings(False) - conf.set_print_warnings(True) - conf.unset_user_values() - - conf.eval("y && ARCH") - - for s in conf.get_symbols(): + tests.)""" + print(" For {}...".format(arch)) + + conf.defconfig_filename + conf.mainmenu_text + conf.enable_undef_warnings() + conf.disable_undef_warnings() + conf.disable_warnings() + conf.enable_warnings() + conf.unset_values() + + # Python 2/3 compatible + for _, s in conf.syms.items(): s.__str__() - s.get_assignable_values() - s.get_config() - s.get_help() - s.get_implied_symbols() - s.get_lower_bound() - s.get_name() - s.get_parent() - s.get_prompts() - s.get_ref_locations() - s.get_referenced_symbols() - s.get_referenced_symbols(True) - s.get_selected_symbols() - s.get_type() - s.get_upper_bound() - s.get_user_value() - s.get_value() - s.get_visibility() - s.has_ranges() - s.is_choice_selection() - s.is_choice_symbol() - s.is_defined() - s.is_from_environment() - s.is_modifiable() - s.is_allnoconfig_y() - s.unset_user_value() - - # Check get_ref/def_location() sanity - - if s.is_special(): - if s.is_from_environment(): - # Special symbols from the environment should have define - # locations - verify(s.get_def_locations() != [], - "The symbol '{}' is from the environment but lacks " - "define locations".format(s.get_name())) - else: - # Special symbols that are not from the environment should be - # defined and have no define locations - verify(s.is_defined(), - "The special symbol '{}' is not defined". - format(s.get_name())) - verify(s.get_def_locations() == [], - "The special symbol '{}' has recorded def. locations". - format(s.get_name())) - else: - # Non-special symbols should have define locations iff they are - # defined - if s.is_defined(): - verify(s.get_def_locations() != [], - "'{}' defined but lacks recorded locations". - format(s.get_name())) - else: - verify(s.get_def_locations() == [], - "'{}' undefined but has recorded locations". - format(s.get_name())) - verify(s.get_ref_locations() != [], - "'{}' both undefined and unreferenced". - format(s.get_name())) - - for c in conf.get_choices(): + s.__repr__() + s.assignable + s.type + s.value + s.visibility + s.unset_value() + + # Cheat with internals + for c in conf._choices: c.__str__() - c.get_config() - c.get_def_locations() - c.get_help() - c.get_items() - c.get_mode() - c.get_name() - c.get_parent() - c.get_prompts() - c.get_referenced_symbols() - c.get_referenced_symbols(True) - c.get_selection() - c.get_selection_from_defaults() - c.get_symbols() - c.get_type() - c.get_user_selection() - c.get_visibility() - c.is_optional() - - for m in conf.get_menus(): - m.__str__() - m.get_config() - m.get_items() - m.get_items(True) - m.get_location() - m.get_parent() - m.get_referenced_symbols() - m.get_referenced_symbols(True) - m.get_symbols() - m.get_symbols(True) - m.get_title() - m.get_visibility() - m.get_visible_if_visibility() - - for c in conf.get_comments(): - c.__str__() - c.get_config() - c.get_location() - c.get_parent() - c.get_referenced_symbols() - c.get_referenced_symbols(True) - c.get_text() - c.get_visibility() - -def test_config_absent(conf): + c.__repr__() + c.value + c.assignable + c.selection + c.default_selection + c.type + c.visibility + +def test_config_absent(conf, arch): """ Verify that Kconfiglib generates the same .config as 'make alldefconfig', for each architecture""" conf.write_config("._config") - if speedy_mode: + if speedy: shell("scripts/kconfig/conf --alldefconfig Kconfig") else: shell("make alldefconfig") -def test_defconfig(conf): +def test_defconfig(conf, arch): """ Verify that Kconfiglib generates the same .config as scripts/kconfig/conf, for each architecture/defconfig pair. In obsessive mode, this test includes @@ -2450,8 +1946,8 @@ def test_defconfig(conf): global nconfigs defconfigs = [] - def add_configs_for_arch(arch): - arch_dir = os.path.join("arch", arch) + def add_configs_for_arch(arch_): + arch_dir = os.path.join("arch", arch_) # Some arches have a "defconfig" in the root of their arch// # directory root_defconfig = os.path.join(arch_dir, "defconfig") @@ -2470,13 +1966,13 @@ def test_defconfig(conf): for filename in filenames: defconfigs.append(os.path.join(dirpath, filename)) - if obsessive_mode: + if obsessive: # Collect all defconfigs. This could be done once instead, but it's # a speedy operation comparatively. - for arch in os.listdir("arch"): - add_configs_for_arch(arch) + for arch_ in os.listdir("arch"): + add_configs_for_arch(arch_) else: - add_configs_for_arch(conf.get_arch()) + add_configs_for_arch(arch) # Test architecture for each defconfig @@ -2487,7 +1983,7 @@ def test_defconfig(conf): conf.load_config(defconfig) conf.write_config("._config") - if speedy_mode: + if speedy: shell("scripts/kconfig/conf --defconfig='{}' Kconfig". format(defconfig)) else: @@ -2500,31 +1996,24 @@ def test_defconfig(conf): # bugs. shell("make kconfiglibtestconfig") - arch_defconfig_str = " {:14}with {:60} " \ - .format(conf.get_arch(), defconfig) + arch_defconfig_str = " {:14}with {:60} ".format(arch, defconfig) if equal_confs(): print(arch_defconfig_str + "OK") else: print(arch_defconfig_str + "FAIL") fail() - if log_mode: + if log: with open("test_defconfig_fails", "a") as fail_log: fail_log.write("{} {} with {} did not match\n" .format(time.strftime("%d %b %Y %H:%M:%S", time.localtime()), - conf.get_arch(), - defconfig)) + arch, defconfig)) # # Helper functions # -devnull = open(os.devnull, "w") - -def shell(cmd): - subprocess.call(cmd, shell = True, stdout = devnull, stderr = devnull) - def rm_configs(): """Delete any old ".config" (generated by the C implementation) and "._config" (generated by us), if present.""" @@ -2557,7 +2046,8 @@ def equal_confs(): return False else: with f: - our = f.readlines() + # [1:] strips the default header + our = f.readlines()[1:] if their == our: return True @@ -2569,26 +2059,5 @@ def equal_confs(): return False -_all_ok = True - -def verify(cond, msg): - """Fails and prints 'msg' if 'cond' is False.""" - if not cond: - fail(msg) - -def verify_equals(x, y): - """Fails if 'x' does not equal 'y'.""" - if x != y: - fail("'{}' does not equal '{}'".format(x, y)) - -def fail(msg = None): - global _all_ok - if msg is not None: - print("Fail: " + msg) - _all_ok = False - -def all_ok(): - return _all_ok - if __name__ == "__main__": run_tests() -- cgit v1.2.3 From 463cebd7f8bf26673732bf5afcd113ee5e2268b7 Mon Sep 17 00:00:00 2001 From: Ulf Magnusson Date: Wed, 25 Oct 2017 13:35:31 +0200 Subject: Backup --- examples/allnoconfig.py | 35 +-- examples/allyesconfig.py | 4 +- examples/help_grep.py | 2 +- examples/print_tree.py | 2 +- kconfiglib.py | 637 ++++++++++++++++++++++++++--------------------- testsuite.py | 8 +- 6 files changed, 388 insertions(+), 300 deletions(-) diff --git a/examples/allnoconfig.py b/examples/allnoconfig.py index edc473d..8d41912 100644 --- a/examples/allnoconfig.py +++ b/examples/allnoconfig.py @@ -12,7 +12,7 @@ from kconfiglib import Config, Symbol, tri_less import sys def do_allnoconfig(node): - global no_changes + global changed # Walk the tree of menu nodes. You can imagine this as going down/into menu # entries in the menuconfig interface, setting each to 'n' (or the lowest @@ -22,14 +22,16 @@ def do_allnoconfig(node): if isinstance(node.item, Symbol): sym = node.item - # Is the symbol a non-choice symbol that can be set to a lower - # value than its current value? - if sym.choice is None and sym.assignable and \ - tri_less(sym.assignable[0], sym.value): + # Is the symbol a non-choice, non-allnoconfig_y symbol that can be + # set to a lower value than its current value? + if (sym.choice is None and + not sym.is_allnoconfig_y and + sym.assignable and + tri_less(sym.assignable[0], sym.value)): # Yup, lower it sym.set_value(sym.assignable[0]) - no_changes = False + changed = True # Recursively lower children if node.list is not None: @@ -39,19 +41,22 @@ def do_allnoconfig(node): conf = Config(sys.argv[1]) +# Do an initial pass to set 'option allnoconfig_y' symbols to 'y' +for sym in conf.defined_syms: + if sym.is_allnoconfig_y: + sym.set_value("y") + while 1: - # For tricky dependencies involving '!', setting later symbols to 'n' might - # actually raise the value of earlier symbols. To be super safe, we do - # additional passes until a pass no longer changes the value of any symbol. - # - # This isn't actually needed for any ARCH in the kernel as of 4.14. A - # single pass gives the correct result. - no_changes = True + # Changing later symbols in the configuration can sometimes allow earlier + # symbols to be lowered, e.g. if a later symbol 'select's an earlier + # symbol. To handle such situations, we do additional passes over the tree + # until we're no longer able to change the value of any symbol in a pass. + changed = False - do_allnoconfig(conf.top_menu) + do_allnoconfig(conf.top_node) # Did the pass change any symbols? - if no_changes: + if not changed: break conf.write_config(".config") diff --git a/examples/allyesconfig.py b/examples/allyesconfig.py index 4847c05..32b302c 100644 --- a/examples/allyesconfig.py +++ b/examples/allyesconfig.py @@ -27,7 +27,7 @@ conf = Config(sys.argv[1]) # tree can be walked iteratively by using the parent pointers. choices = [] -node = conf.top_menu +node = conf.top_node while 1: if isinstance(node.item, Choice): @@ -85,7 +85,7 @@ while 1: # Does the choice have a default selection that we haven't already # selected? if selection is not None and \ - selection is not choice.user_value: + selection is not choice.user_selection: # Yup, select it selection.set_value("y") diff --git a/examples/help_grep.py b/examples/help_grep.py index fed1731..6fcb08f 100644 --- a/examples/help_grep.py +++ b/examples/help_grep.py @@ -69,4 +69,4 @@ def search_tree(node): node = node.next conf = Config(sys.argv[1]) -search_tree(conf.top_menu) +search_tree(conf.top_node) diff --git a/examples/print_tree.py b/examples/print_tree.py index da795c9..8d18ce9 100644 --- a/examples/print_tree.py +++ b/examples/print_tree.py @@ -64,4 +64,4 @@ def print_items(node, indent): node = node.next conf = Config(sys.argv[1]) -print_items(conf.top_menu, 0) +print_items(conf.top_node, 0) diff --git a/kconfiglib.py b/kconfiglib.py index e4d0b58..04edb67 100644 --- a/kconfiglib.py +++ b/kconfiglib.py @@ -1,4 +1,7 @@ """ +Overview +======== + Kconfiglib is a Python 2/3 library for scripting and extracting information from Kconfig-based configuration systems. Features include the following: @@ -6,40 +9,79 @@ from Kconfig-based configuration systems. Features include the following: - Reading/writing of .config files - - Expression inspection and evaluation. All expressions are exposed and use a - simple format that can be processed manually if needed. + - Inspection of symbol properties: print()ing a symbol (which calls __str__()) + produces output which could be fed back into a Kconfig parser to redefine + the symbol, and __str__() is implemented with only public APIs. + + A helpful __repr__() is implemented on all objects as well. + + - Expression inspection and evaluation: All expressions are exposed and use a + simple tuple-based format that can be processed manually if needed. + + - Menu tree inspection: The underlying menu tree is exposed, including + submenus created implicitly from symbols depending on preceding symbols. + This can be used e.g. to implement menuconfig-like functionality. - - Menu tree inspection. The underlying menu tree is exposed, including - submenus created implicitly by symbols depending on preceding symbols. This - can be used e.g. to implement menuconfig-like functionality. + - Runs under both Python 2 and 3. The code mostly uses basic Python features + (the most advanced things used are probably @property and __slots__). - - Highly compatible with the standard Kconfig C tools: The test suite compares - outputs between Kconfiglib and the C tools on real-world kernel Kconfig and - defconfig files for a large number of cases (by diffing generated .configs). + - Robust and highly compatible with the standard Kconfig C tools: The test + suite automatically compares the output from Kconfiglib with the output from + the C tools on the real kernel Kconfig and defconfig files for all ARCHes. + The comparison is done by diffing the generated .config files to make sure + they're identical. All tests are expected to pass. + + A suite of self tests is also included. + + - Internals that (mostly) mirror the C implementation. A lot can indirectly be + learned about how it works by reading the Kconfiglib documentation and code. - Pretty speedy by pure Python standards: Parses the x86 Kconfigs in about a second on a Core i7 2600K (with a warm file cache). For long-running jobs, - PyPy gives a nice speedup. + PyPy gives a large performance boost. + + +Using Kconfiglib on the Linux kernel +==================================== For the Linux kernel, a handy interface is provided by the -scripts/kconfig/Makefile patch. For experimentation, you can use the -iscriptconfig target, which gives an interactive Python prompt where the -configuration for ARCH has been loaded: +scripts/kconfig/Makefile patch. - $ make [ARCH=] iscriptconfig +Use the 'iscriptconfig' target for experimentation. It gives an interactive +Python prompt where the configuration for ARCH has been preloaded. -To run a script, use the scriptconfig target: + $ make [ARCH=] [PYTHONCMD=] iscriptconfig - $ make [ARCH=] scriptconfig SCRIPT= [SCRIPT_ARG=] +To run a script, use the 'scriptconfig' target. + + $ make [ARCH=] [PYTHONCMD=] scriptconfig SCRIPT= [SCRIPT_ARG=] + +PYTHONCMD is the Python interpreter to use. It defaults to "python". + +Tip: IronPython (PYTHONCMD=ipython) autocompletion is handy when figuring out +the API. + + +Writing scriptconfig scripts +---------------------------- See the examples/ subdirectory for example scripts. +Scripts receive the name of the Kconfig file to load in sys.argv[1]. As far as +I can tell, this is always "Kconfig" from the kernel top-level directory as of +Linux 4.14. If an argument is provided with SCRIPT_ARG, it appears as +sys.argv[2]. + + +Running Kconfiglib without the Makefile patch +--------------------------------------------- The Makefile patch is used to pick up the ARCH, SRCARCH, and KERNELVERSION environment variables (and any future environment variables that might get used). If you want to run Kconfiglib without the Makefile patch, the following will probably work in practice (it's what the test suite does in 'speedy' mode, -except it tests all ARCHes): +except it tests all ARCHes and doesn't bother setting KERNELVERSION to a sane +value to save some time on startup). $ ARCH=x86 SRCARCH=x86 KERNELVERSION=`make kernelversion` python script.py @@ -49,18 +91,40 @@ possible variations. The value of KERNELVERSION doesn't seem to matter as of Linux 4.14. Kconfiglib will warn if you forget to set some environment variable that's -referenced in the configuration (via 'option env="ENV_VAR"'). +referenced in a Kconfig file (via 'option env="ENV_VAR"'). -When using scriptconfig, scripts receive the name of the Kconfig file to load -in sys.argv[1]. As far as I can tell, this is always "Kconfig" from the kernel -top-level directory as of Linux 4.14. If an argument is provided with -SCRIPT_ARG, it appears as sys.argv[2]. +Expression format +================= +The following table should help you figure out how expressions are represented. +A, B, C, ... are symbols (Symbol instances) or strings (which represent +constant symbols). NOT is the kconfiglib.NOT constant, etc. -Kconfiglib supports both Python 2 and Python 3 (and PyPy). For (i)scriptconfig, -the Python interpreter to use can be passed in PYTHONCMD, which defaults to -"python". +Expression Representation +---------- -------------- +A A +!A (NOT, A) +A && B (AND, A, B) +A || B (OR, A, B) +A = B (EQUAL, A, B) +A != "foo" (UNEQUAL, A, "foo") +A || (B && C && D) (OR, A, (AND, B, (AND, C, D))) +y "y" +"y" "y" + +As seen in the final two examples, n/m/y are always represented as the strings +(constant symbols) "n"/"m"/"y" in Kconfiglib, regardless of whether they're +written with or without quotes. This simplifies some internals. + +A missing expression (e.g. if the 'if ' part is removed from +'default A if ') is represented as "y". The standard __str__() functions +avoid printing 'if y' conditions to give cleaner output. + +Implementation note +------------------- + +TODO: blah blah constant symbols Send bug reports, suggestions, and questions to ulfalizer a.t Google's email @@ -97,58 +161,70 @@ class Config(object): The following attributes are available on Config instances. They should be viewed as read-only, and some are implemented through @property magic. - Modifying symbols is fine, but not the 'syms' dictionary itself. syms: A dictionary with all symbols in the configuration. The key is the name - of the symbol, so that e.g. conf.syms["MODULES"] returns the MODULES - symbol. Symbols that are referenced in expressions but never defined are - included as well. + of the symbol, so that e.g. conf.syms["FOO"] returns the Symbol instance + for the symbol FOO. Symbols that are referenced in expressions but never + defined also appear in 'syms'. + + Constant symbols, e.g. "foo" in 'A = "foo"', are not included in + Config.syms. defined_syms: A list of all defined symbols, in the same order as they appear in the - Kconfig files. Provided as a convenience (and also used internally). The - defined symbols are those whose 'nodes' attribute is non-empty. + Kconfig files. Provided as a convenience. The defined symbols are those + whose 'nodes' attribute is non-empty. named_choices: - A dictionary like 'syms' for named choices (choice FOO). This is mostly - for completeness. I've never seen named choices being used. + A dictionary like 'syms' for named choices (choice FOO). This is for + completeness. I've never seen named choices being used. - top_menu: - The menu node (see the MenuNode class) of the top-level menu. Acts as the - root of the menu tree. + modules: + The Symbol instance for the modules symbol. This is currently hardcoded + to MODULES, which is backwards compatible, and Kconfiglib will warn if + 'option modules' is specified on some other symbol. Tell me if you need + proper 'option modules' support. - mainmenu_text: - The prompt (title) of the top_menu menu, with Kconfig variable references - ("$FOO") expanded. Defaults to "Linux Kernel Configuration" (like in the - C tools). Can be changed with the 'mainmenu' statement (see - kconfig-language.txt). + Never None. + + defconfig_list: + The Symbol instance for the 'option defconfig_list' symbol, or None if no + defconfig_list symbol exists. The defconfig filename derived from this + symbol can be found in Config.defconfig_filename. + + Setting 'option defconfig_list' on multiple symbols ignores the setting + on all symbols after the first. defconfig_filename: - The filename given by the 'option defconfig_list' symbol. This is the - first existing file with a satisfied condition among the 'default' - properties of the symbol. If a file is not found at the given path, it is - also looked up relative to $srctree if set ($srctree/foo/defconfig is - looked up if foo/defconfig is not found). + The filename given by the 'option defconfig_list' symbol. This is taken + from the first 'default' with a satisfied condition where the file + specified by the 'default' exists. If a defconfig file foo/defconfig is + not found and $srctree was set when the Config was created, + $srctree/foo/defconfig is looked up as well. - Has the value None if either no defconfig_list symbol exists, or if it - has no 'default' with a satisfied dependency that points to an existing - file. + None if either no defconfig_list symbol exists, or if the defconfig_list + symbol has no 'default' with a satisfied condition that points to an + existing file. References to Kconfig symbols ("$FOO") are expanded in 'default' properties. - Setting 'option defconfig_list' on multiple symbols ignores symbols past - the first one. + Something to look out for is that scripts/kconfig/Makefile might pass + --defconfig= to scripts/kconfig/conf when running e.g. + 'make defconfig'. This option overrides the defconfig_list symbol, + meaning defconfig_filename might not match what 'make defconfig' would + use in those cases. - Do print(c.syms["DEFCONFIG_LIST"]) on a kernel configuration to see an - example of a defconfig_list symbol. + top_node: + The menu node (see the MenuNode class) of the top-level menu. Acts as the + root of the menu tree. - Something to look out for is that scripts/kconfig/Makefile might use the - --defconfig= option when calling the C tools of e.g. 'make - defconfig'. This option overrides the 'option defconfig_list' symbol, - meaning defconfig_filename might not match what 'make defconfig' would - use. + mainmenu_text: + The prompt (title) of the top_node menu, with Kconfig variable references + ("$FOO") expanded. Defaults to "Linux Kernel Configuration" (like in the + C tools). Can be changed with the 'mainmenu' statement (see + kconfig-language.txt). srctree: The value of the $srctree environment variable when the configuration was @@ -157,9 +233,9 @@ class Config(object): (unless absolute paths are specified). This is to support out-of-tree builds. The C tools use this variable in the same way. - Changing $srctree after loading the configuration has no effect. Only the - value when the configuration is loaded matters. This avoids surprises if - multiple configurations are loaded with different values for $srctree. + Changing $srctree after creating the Config instance has no effect. Only + the value when the configuration is loaded matters. This avoids surprises + if multiple configurations are loaded with different values for $srctree. config_prefix: The value of the $CONFIG_ environment variable when the configuration was @@ -183,7 +259,7 @@ class Config(object): "named_choices", "srctree", "syms", - "top_menu", + "top_node", ) # @@ -199,13 +275,15 @@ class Config(object): filename (default: "Kconfig"): The base Kconfig file. For the Linux kernel, you'll want "Kconfig" from the top-level directory, as environment variables will make sure - the right Kconfig is included from there - (arch//Kconfig). If you are using Kconfiglib via 'make - scriptconfig', the filename of the base base Kconfig file will be in - sys.argv[1] (always "Kconfig" in practice). + the right Kconfig is included from there (arch/$SRCARCH/Kconfig as of + writing). + + If you are using Kconfiglib via 'make scriptconfig', the filename of + the base base Kconfig file will be in sys.argv[1]. It's currently + always "Kconfig" in practice. - The $srctree environment variable is used if set (see the class - documentation). + The $srctree environment variable is used to look up Kconfig files if + set (see the class documentation). warn (default: True): True if warnings related to this configuration should be printed to @@ -214,67 +292,66 @@ class Config(object): argument since warnings might be generated during parsing. """ + self.srctree = os.environ.get("srctree") + + self.config_prefix = os.environ.get("CONFIG_") + if self.config_prefix is None: + self.config_prefix = "CONFIG_" + + # Regular expressions for parsing .config files + self._set_re = re.compile(r"{}(\w+)=(.*)" + .format(self.config_prefix)) + self._unset_re = re.compile(r"# {}(\w+) is not set" + .format(self.config_prefix)) + + self._print_warnings = warn + self._print_undef_assign = False + self.syms = {} self.defined_syms = [] self.named_choices = {} - # Used for quickly invalidating all choices self._choices = [] - # Predefined symbol. DEFCONFIG_LIST has been seen using this. + self.modules = self._lookup_sym("MODULES") + self.defconfig_list = None + + # Predefined symbol. DEFCONFIG_LIST uses this. uname_sym = Symbol() uname_sym._type = STRING uname_sym.name = "UNAME_RELEASE" uname_sym.config = self - uname_sym.defaults.append((platform.uname()[2], None)) + uname_sym.defaults.append((platform.uname()[2], "y")) # env_var doubles as the SYMBOL_AUTO flag from the C implementation, so # just set it to something. The naming breaks a bit here, but it's # pretty obscure. uname_sym.env_var = "" self.syms["UNAME_RELEASE"] = uname_sym - # The symbol with "option defconfig_list" set, containing a list of - # default .config files - self.defconfig_list = None - - self.config_prefix = os.environ.get("CONFIG_") - if self.config_prefix is None: - self.config_prefix = "CONFIG_" - - # Regular expressions for parsing .config files - self._set_re = re.compile(r"{}(\w+)=(.*)" - .format(self.config_prefix)) - self._unset_re = re.compile(r"# {}(\w+) is not set" - .format(self.config_prefix)) - - self.srctree = os.environ.get("srctree") - - self._print_warnings = warn - self._print_undef_assign = False - - self.top_menu = MenuNode() - self.top_menu.config = self - self.top_menu.item = MENU - self.top_menu.visibility = None - self.top_menu.prompt = ("Linux Kernel Configuration", None) - self.top_menu.parent = None - self.top_menu.dep = None - self.top_menu.filename = filename - self.top_menu.linenr = 1 - - # We hardcode MODULES for backwards compatibility. Proper support via - # 'option modules' wouldn't be that tricky to add with backwards - # compatibility either though. - self.modules = self._lookup_sym("MODULES") + self.top_node = MenuNode() + self.top_node.config = self + self.top_node.item = MENU + self.top_node.visibility = None + self.top_node.prompt = ("Linux Kernel Configuration", "y") + self.top_node.parent = None + self.top_node.dep = "y" + self.top_node.filename = filename + self.top_node.linenr = 1 # Parse the Kconfig files self._parse_block(_FileFeed(self._open(filename), filename), - None, self.top_menu, None, None, self.top_menu) + None, # end_token + self.top_node, # parent + "y", # visible_if_deps + None, # prev_line + self.top_node) # prev_node - self.top_menu.list = self.top_menu.next - self.top_menu.next = None + self.top_node.list = self.top_node.next + self.top_node.next = None - _finalize_tree(self.top_menu) + # Do various post-processing of the menu tree, e.g. to finalize + # choices, flatten ifs, and implicitly create menus + _finalize_tree(self.top_node) # Build Symbol._direct_dependents for all symbols self._build_dep() @@ -284,7 +361,7 @@ class Config(object): """ See the class documentation. """ - return self._expand_sym_refs(self.top_menu.prompt[0]) + return self._expand_sym_refs(self.top_node.prompt[0]) @property def defconfig_filename(self): @@ -727,7 +804,7 @@ class Config(object): return _Feed(tokens) - def _parse_block(self, line_feeder, end_marker, parent, visible_if_deps, + def _parse_block(self, line_feeder, end_token, parent, visible_if_deps, prev_line, prev_node): """ Parses a block, which is the contents of either a file or an if, menu, @@ -737,7 +814,7 @@ class Config(object): A _FileFeed instance feeding lines from a file. The Kconfig language is line-based in practice. - end_marker: + end_token: The token that ends the block, e.g. _T_ENDIF ("endif") for ifs. None for files. @@ -774,7 +851,7 @@ class Config(object): else: line = line_feeder.next() if line is None: - if end_marker is not None: + if end_token is not None: raise KconfigSyntaxError("Unexpected end of file " + line_feeder.filename) @@ -837,11 +914,14 @@ class Config(object): e.message)) prev_node = self._parse_block(_FileFeed(f, exp_kconfig_file), - None, parent, visible_if_deps, - None, prev_node) + None, # end_token + parent, + visible_if_deps, + None, # prev_line + prev_node) prev_line = None - elif t0 == end_marker: + elif t0 == end_token: # We have reached the end of the block. Terminate the final # node and return it. prev_node.next = None @@ -860,8 +940,12 @@ class Config(object): line_feeder.filename, line_feeder.linenr, True)) - self._parse_block(line_feeder, _T_ENDIF, node, visible_if_deps, - None, node) + self._parse_block(line_feeder, + _T_ENDIF, + node, # parent + visible_if_deps, + None, # prev_line + node) # prev_node node.list = node.next prev_line = None @@ -872,7 +956,7 @@ class Config(object): node = MenuNode() node.config = self node.item = MENU - node.visibility = None + node.visibility = "y" node.parent = parent node.filename = line_feeder.filename node.linenr = line_feeder.linenr @@ -881,9 +965,12 @@ class Config(object): visible_if_deps) node.prompt = (tokens.next(), node.dep) - self._parse_block(line_feeder, _T_ENDMENU, node, + self._parse_block(line_feeder, + _T_ENDMENU, + node, # parent _make_and(visible_if_deps, node.visibility), - prev_line, node) + prev_line, + node) # prev_node node.list = node.next prev_line = None @@ -931,8 +1018,12 @@ class Config(object): prev_line = self._parse_properties(line_feeder, node, visible_if_deps) - self._parse_block(line_feeder, _T_ENDCHOICE, node, - visible_if_deps, prev_line, node) + self._parse_block(line_feeder, + _T_ENDCHOICE, + node, # parent + visible_if_deps, + prev_line, + node) # prev_node node.list = node.next prev_line = None @@ -942,9 +1033,9 @@ class Config(object): prev_node.next = prev_node = node elif t0 == _T_MAINMENU: - self.top_menu.prompt = (tokens.next(), None) - self.top_menu.filename = line_feeder.filename - self.top_menu.linenr = line_feeder.linenr + self.top_node.prompt = (tokens.next(), "y") + self.top_node.filename = line_feeder.filename + self.top_node.linenr = line_feeder.linenr else: _parse_error(line, "unrecognized construct", @@ -953,15 +1044,15 @@ class Config(object): def _parse_cond(self, tokens, line, filename, linenr): """ Parses an optional 'if ' construct and returns the parsed , - or None if the next token is not _T_IF + or "y" if the next token is not _T_IF """ return self._parse_expr(tokens, line, filename, linenr, True) \ - if tokens.check(_T_IF) else None + if tokens.check(_T_IF) else "y" def _parse_val_and_cond(self, tokens, line, filename, linenr): """ Parses ' if ' constructs, where the 'if' part is - optional. Returns a tuple containing the parsed expressions, with None + optional. Returns a tuple containing the parsed expressions, with "y" as the second element if the 'if' part is missing. """ return (self._parse_expr(tokens, line, filename, linenr, False), @@ -1001,7 +1092,7 @@ class Config(object): # Menu node dependency from 'depends on'. Will get propagated to the # properties above. - node.dep = None + node.dep = "y" # The cached (line, tokens) tuple that we return last_line = None @@ -1145,7 +1236,7 @@ class Config(object): "service.".format(node.item.name, env_var), filename, linenr) else: - defaults.append((os.environ[env_var], None)) + defaults.append((os.environ[env_var], "y")) elif tokens.check(_T_DEFCONFIG_LIST): if self.defconfig_list is None: @@ -1272,12 +1363,12 @@ class Config(object): Parses an expression from the tokens in 'feed' using a simple top-down approach. The result has the form '( )' where is e.g. - kconfiglib._AND. If there is only one operand (i.e., no && or ||), then + kconfiglib.AND. If there is only one operand (i.e., no && or ||), then the operand is returned directly. This also goes for subexpressions. As an example, A && B && (!C || D == 3) is represented as the tuple - structure (_AND, A, (_AND, B, (_OR, (_NOT, C), (_EQUAL, D, 3)))), with - the Symbol objects stored directly in the expression. + structure (AND, A, (AND, B, (OR, (NOT, C), (EQUAL, D, 3)))), with the + Symbol objects stored directly in the expression. feed: _Feed instance containing the tokens for the expression. @@ -1326,24 +1417,23 @@ class Config(object): transform_m) # Return 'and_expr' directly if we have a "single-operand" OR. - # Otherwise, parse the expression on the right and make an _OR node. - # This turns A || B || C || D into - # (_OR, A, (_OR, B, (_OR, C, D))). + # Otherwise, parse the expression on the right and make an OR node. + # This turns A || B || C || D into (OR, A, (OR, B, (OR, C, D))). return and_expr \ if not feed.check(_T_OR) else \ - (_OR, and_expr, self._parse_expr(feed, line, filename, linenr, - transform_m)) + (OR, and_expr, self._parse_expr(feed, line, filename, linenr, + transform_m)) def _parse_and_expr(self, feed, line, filename, linenr, transform_m): factor = self._parse_factor(feed, line, filename, linenr, transform_m) # Return 'factor' directly if we have a "single-operand" AND. - # Otherwise, parse the right operand and make an _AND node. This turns - # A && B && C && D into (_AND, A, (_AND, B, (_AND, C, D))). + # Otherwise, parse the right operand and make an AND node. This turns + # A && B && C && D into (AND, A, (AND, B, (AND, C, D))). return factor \ if not feed.check(_T_AND) else \ - (_AND, factor, self._parse_and_expr(feed, line, filename, - linenr, transform_m)) + (AND, factor, self._parse_and_expr(feed, line, filename, + linenr, transform_m)) def _parse_factor(self, feed, line, filename, linenr, transform_m): token = feed.next() @@ -1359,7 +1449,7 @@ class Config(object): # '... if ', etc.), "m" and m are rewritten to # "m" && MODULES. if transform_m and token == "m": - return (_AND, "m", self.modules) + return (AND, "m", self.modules) return token @@ -1367,8 +1457,8 @@ class Config(object): return (_TOKEN_TO_REL[feed.next()], token, feed.next()) if token == _T_NOT: - return (_NOT, self._parse_factor(feed, line, filename, linenr, - transform_m)) + return (NOT, self._parse_factor(feed, line, filename, linenr, + transform_m)) if token == _T_OPEN_PAREN: expr_parse = self._parse_expr(feed, line, filename, @@ -1414,7 +1504,7 @@ class Config(object): config_strings = [] add_fn = config_strings.append - node = self.top_menu.list + node = self.top_node.list if node is None: # Empty configuration return config_strings @@ -1424,7 +1514,7 @@ class Config(object): # .config entry. We reset it prior to writing out a new .config. It # only needs to be reset for defined symbols, because undefined symbols # will never be written out (because they do not appear structure - # rooted at Config.top_menu). + # rooted at Config.top_node). # # The C tools reuse _write_to_conf for this, but we cache # _write_to_conf together with the value and don't invalidate cached @@ -1731,12 +1821,18 @@ class Symbol(object): locations, the dependencies at each location are ORed together. env_var: - If the Symbol is set from the environment via 'option env="FOO"', this - contains the name ("FOO") of the environment variable. None for symbols - that aren't set from the environment. + If the Symbol has an 'option env="FOO"' option, this contains the name + ("FOO") of the environment variable. None for symbols that aren't set + from the environment. + + 'option env="FOO"' acts as a 'default' property whose value is the value + of $FOO. - Internally, this is only used to print the symbol. The value of the - environment variable is looked up once when the configuration is parsed. + env_var is also set (to "") on the predefined symbol + UNAME_RELEASE, which holds the 'release' field from uname. + + Symbols with an 'option env' option are never written out to .config + files. is_allnoconfig_y: True if the symbol has 'option allnoconfig_y' set on it. This has no @@ -1815,7 +1911,7 @@ class Symbol(object): if vis != "n" and self.user_value is not None: # If the symbol is visible and has a user value, we use # that - val = _eval_min(self.user_value, vis) + val = _tri_min(self.user_value, vis) else: # Otherwise, we look at defaults and weak reverse @@ -1825,7 +1921,7 @@ class Symbol(object): cond_val = eval_expr(cond) if cond_val != "n": self._write_to_conf = True - val = _eval_min(default, cond_val) + val = _tri_min(eval_expr(default), cond_val) break # Weak reverse dependencies are only considered if our @@ -1835,13 +1931,13 @@ class Symbol(object): eval_expr(self.weak_rev_dep) if weak_rev_dep_val != "n": self._write_to_conf = True - val = _eval_max(val, weak_rev_dep_val) + val = _tri_max(val, weak_rev_dep_val) # Reverse (select-related) dependencies take precedence rev_dep_val = eval_expr(self.rev_dep) if rev_dep_val != "n": self._write_to_conf = True - val = _eval_max(val, rev_dep_val) + val = _tri_max(val, rev_dep_val) else: # (bool/tristate) symbol in choice. See _get_visibility() for @@ -2082,7 +2178,7 @@ class Symbol(object): if self.is_allnoconfig_y: fields.append("allnoconfig_y") - if self is self.config.defconfig_list: + if self is self.config.defconfig_list: fields.append("is the defconfig_list symbol") if self.env_var is not None: @@ -2239,8 +2335,8 @@ class Symbol(object): if self.choice is not None and self._type in (BOOL, TRISTATE): if value == "y": - self.choice.user_selection = self self.choice.user_value = "y" + self.choice.user_selection = self elif value == "m": self.choice.user_value = "m" @@ -2441,7 +2537,7 @@ class Choice(object): See the class documentation. """ if self.user_value is not None: - val = _eval_min(self.user_value, self.visibility) + val = _tri_min(self.user_value, self.visibility) else: val = "n" @@ -2592,8 +2688,8 @@ class Choice(object): self.nodes = [] - self.user_selection = None self.user_value = None + self.user_selection = None # The prompts and default values without any dependencies from # enclosing menus and ifs propagated @@ -2638,7 +2734,7 @@ class MenuNode(object): menu node for each location. The top-level menu node, corresponding to the implicit top-level menu, is - available in Config.top_menu. + available in Config.top_node. For symbols and choices, the menu nodes are available in the 'nodes' attribute. Menus and comments are represented as plain menu nodes, with @@ -2819,12 +2915,9 @@ def tri_greater_eq(v1, v2): def eval_expr(expr): """ - Evaluates an expression to "n", "m", or "y". Returns "y" for None, which - makes sense as None usually indicates a missing condition. + Evaluates an expression to "n", "m", or "y". """ - return "y" if expr is None else _eval_expr_rec(expr) -def _eval_expr_rec(expr): if isinstance(expr, Symbol): # Non-bool/tristate symbols are always "n" in a tristate sense, # regardless of their value @@ -2833,28 +2926,22 @@ def _eval_expr_rec(expr): if isinstance(expr, str): return expr if expr in ("m", "y") else "n" - if expr[0] == _AND: - ev1 = _eval_expr_rec(expr[1]) - if ev1 == "n": - # No need to look at expr[2] - return "n" - ev2 = _eval_expr_rec(expr[2]) - return ev2 if ev1 == "y" else \ - "m" if ev2 != "n" else \ - "n" - - if expr[0] == _OR: - ev1 = _eval_expr_rec(expr[1]) - if ev1 == "y": - # No need to look at expr[2] - return "y" - ev2 = _eval_expr_rec(expr[2]) - return ev2 if ev1 == "n" else \ - "y" if ev2 == "y" else \ - "m" + if expr[0] == AND: + ev1 = eval_expr(expr[1]) + + # Short-circuit the ev1 == "n" case + return "n" if ev1 == "n" else \ + _tri_min(ev1, eval_expr(expr[2])) - if expr[0] == _NOT: - ev = _eval_expr_rec(expr[1]) + if expr[0] == OR: + ev1 = eval_expr(expr[1]) + + # Short-circuit the ev1 == "y" case + return "y" if ev1 == "y" else \ + _tri_max(ev1, eval_expr(expr[2])) + + if expr[0] == NOT: + ev = eval_expr(expr[1]) return "n" if ev == "y" else \ "y" if ev == "n" else \ "m" @@ -2884,16 +2971,16 @@ def _eval_expr_rec(expr): # They're not both valid numbers. If the comparison is # anything but = or !=, return 'n'. Otherwise, reuse # _strcmp() to check for (in)equality. - if oper not in (_EQUAL, _UNEQUAL): + if oper not in (EQUAL, UNEQUAL): return "n" comp = _strcmp(op1_str, op2_str) - if oper == _EQUAL: res = comp == 0 - elif oper == _UNEQUAL: res = comp != 0 - elif oper == _LESS: res = comp < 0 - elif oper == _LESS_EQUAL: res = comp <= 0 - elif oper == _GREATER: res = comp > 0 - elif oper == _GREATER_EQUAL: res = comp >= 0 + if oper == EQUAL: res = comp == 0 + elif oper == UNEQUAL: res = comp != 0 + elif oper == LESS: res = comp < 0 + elif oper == LESS_EQUAL: res = comp <= 0 + elif oper == GREATER: res = comp > 0 + elif oper == GREATER_EQUAL: res = comp >= 0 return "y" if res else "n" @@ -2997,7 +3084,7 @@ def _get_visibility(sc): for node in sc.nodes: if node.prompt: - vis = _eval_max(vis, node.prompt[1]) + vis = _tri_max(vis, eval_expr(node.prompt[1])) if isinstance(sc, Symbol) and sc.choice is not None: if sc.choice._type == TRISTATE and sc._type != TRISTATE and \ @@ -3011,7 +3098,7 @@ def _get_visibility(sc): # choice has mode "y" return "n" - vis = _eval_min(vis, sc.choice.visibility) + vis = _tri_min(vis, sc.choice.visibility) # Promote "m" to "y" if we're dealing with a non-tristate. This might lead # to infinite recursion if something really weird is done with MODULES, but @@ -3024,48 +3111,45 @@ def _get_visibility(sc): def _make_and(e1, e2): """ - Constructs an _AND (&&) expression. Performs trivial simplification. Nones - equate to 'y'. - - Returns None if e1 == e2 == None, so that ANDing two nonexistent - expressions gives a nonexistent expression. + Constructs an AND (&&) expression. Performs trivial simplification. """ - if e1 is None or e1 == "y": + if e1 == "y": return e2 - if e2 is None or e2 == "y": + + if e2 == "y": return e1 - return (_AND, e1, e2) + + if e1 == "n" or e2 == "n": + return "n" + + return (AND, e1, e2) def _make_or(e1, e2): """ - Constructs an _OR (||) expression. Performs trivial simplification and - avoids Nones. Nones equate to 'y', which is usually what we want, but needs - to be kept in mind. + Constructs an OR (||) expression. Performs trivial simplification. """ - - # Perform trivial simplification and avoid None's (which - # correspond to y's) - if e1 is None or e2 is None or e1 == "y" or e2 == "y": - return "y" if e1 == "n": return e2 - return (_OR, e1, e2) -def _eval_min(e1, e2): + if e2 == "n": + return e1 + + if e1 == "y" or e2 == "y": + return "y" + + return (OR, e1, e2) + +def _tri_min(v1, v2): """ - Returns the minimum value of the two expressions. Equates None with 'y'. + Returns the smallest tristate value among v1 and v2. """ - e1_eval = eval_expr(e1) - e2_eval = eval_expr(e2) - return e1_eval if tri_less(e1_eval, e2_eval) else e2_eval + return v1 if _TRI_TO_INT[v1] <= _TRI_TO_INT[v2] else v2 -def _eval_max(e1, e2): +def _tri_max(v1, v2): """ - Returns the maximum value of the two expressions. Equates None with 'y'. + Returns the largest tristate value among v1 and v2. """ - e1_eval = eval_expr(e1) - e2_eval = eval_expr(e2) - return e1_eval if tri_greater(e1_eval, e2_eval) else e2_eval + return v1 if _TRI_TO_INT[v1] >= _TRI_TO_INT[v2] else v2 def _expr_syms_rec(expr, res): """ @@ -3075,10 +3159,10 @@ def _expr_syms_rec(expr, res): res.append(expr) elif isinstance(expr, str): return - elif expr[0] in (_AND, _OR): + elif expr[0] in (AND, OR): _expr_syms_rec(expr[1], res) _expr_syms_rec(expr[2], res) - elif expr[0] == _NOT: + elif expr[0] == NOT: _expr_syms_rec(expr[1], res) elif expr[0] in _RELATIONS: if isinstance(expr[1], Symbol): @@ -3106,9 +3190,9 @@ def _str_val(obj): def _format_and_op(expr): """ _expr_to_str() helper. Returns the string representation of 'expr', which - is assumed to be an operand to _AND, with parentheses added if needed. + is assumed to be an operand to AND, with parentheses added if needed. """ - if isinstance(expr, tuple) and expr[0] == _OR: + if isinstance(expr, tuple) and expr[0] == OR: return "({})".format(_expr_to_str(expr)) return _expr_to_str(expr) @@ -3122,16 +3206,16 @@ def _expr_to_str(expr): if isinstance(expr, Symbol): return expr.name - if expr[0] == _NOT: + if expr[0] == NOT: if isinstance(expr[1], (str, Symbol)): return "!" + _expr_to_str(expr[1]) return "!({})".format(_expr_to_str(expr[1])) - if expr[0] == _AND: + if expr[0] == AND: return "{} && {}".format(_format_and_op(expr[1]), _format_and_op(expr[2])) - if expr[0] == _OR: + if expr[0] == OR: return "{} || {}".format(_expr_to_str(expr[1]), _expr_to_str(expr[2])) @@ -3251,7 +3335,7 @@ def _sym_choice_str(sc): if node.prompt is not None: prompt_str = 'prompt "{}"'.format(node.prompt[0]) - if node.prompt[1] is not None: + if node.prompt[1] != "y": prompt_str += " if " + _expr_to_str(node.prompt[1]) indent_add(prompt_str) @@ -3259,11 +3343,11 @@ def _sym_choice_str(sc): if isinstance(sc, Symbol): if sc.is_allnoconfig_y: indent_add("option allnoconfig_y") - if sc is sc.config.defconfig_list: + if sc is sc.config.defconfig_list: indent_add("option defconfig_list") if sc.env_var is not None: indent_add('option env="{}"'.format(sc.env_var)) - if sc is sc.config.modules: + if sc is sc.config.modules: indent_add("option modules") if isinstance(sc, Symbol): @@ -3271,13 +3355,13 @@ def _sym_choice_str(sc): range_string = "range {} {}" \ .format(_expr_to_str(range_[0]), _expr_to_str(range_[1])) - if range_[2] is not None: + if range_[2] != "y": range_string += " if " + _expr_to_str(range_[2]) indent_add(range_string) for default in sc.defaults: default_string = "default " + _expr_to_str(default[0]) - if default[1] is not None: + if default[1] != "y": default_string += " if " + _expr_to_str(default[1]) indent_add(default_string) @@ -3287,13 +3371,13 @@ def _sym_choice_str(sc): if isinstance(sc, Symbol): for select in sc.selects: select_string = "select " + select[0].name - if select[1] is not None: + if select[1] != "y": select_string += " if " + _expr_to_str(select[1]) indent_add(select_string) for imply in sc.implies: imply_string = "imply " + imply[0].name - if imply[1] is not None: + if imply[1] != "y": imply_string += " if " + _expr_to_str(imply[1]) indent_add(imply_string) @@ -3322,8 +3406,8 @@ def _eq_to_sym(eq): left, right = right, left if not isinstance(left, Symbol): return None - if (relation == _EQUAL and right in ("m", "y")) or \ - (relation == _UNEQUAL and right == "n"): + if (relation == EQUAL and right in ("m", "y")) or \ + (relation == UNEQUAL and right == "n"): return left return None @@ -3342,9 +3426,9 @@ def _expr_depends_on(expr, sym): if isinstance(expr, Symbol): return expr is sym - if expr[0] in (_EQUAL, _UNEQUAL): + if expr[0] in (EQUAL, UNEQUAL): return _eq_to_sym(expr) is sym - if expr[0] == _AND: + if expr[0] == AND: return rec(expr[1]) or rec(expr[2]) return False @@ -3494,13 +3578,6 @@ def _finalize_tree(node): # Public global constants # -# Integers representing menu and comment nodes - -( - MENU, - COMMENT, -) = range(2) - # Integers representing symbol types ( BOOL, @@ -3511,6 +3588,25 @@ def _finalize_tree(node): UNKNOWN ) = range(6) +# Integers representing expression types +( + AND, + OR, + NOT, + EQUAL, + UNEQUAL, + LESS, + LESS_EQUAL, + GREATER, + GREATER_EQUAL, +) = range(9) + +# Integers representing menu and comment menu nodes +( + MENU, + COMMENT, +) = range(2) + # # Internal global constants # @@ -3679,19 +3775,6 @@ _DEFAULT_VALUE = { # will do) for it so we can test with 'is'. _NO_CACHED_SELECTION = object() -# Integers representing expression types -( - _AND, - _OR, - _NOT, - _EQUAL, - _UNEQUAL, - _LESS, - _LESS_EQUAL, - _GREATER, - _GREATER_EQUAL, -) = range(9) - # Used in comparisons. 0 means the base is inferred from the format of the # string. The entries for BOOL and TRISTATE are a convenience - they should # never convert to valid numbers. @@ -3712,29 +3795,29 @@ _TRI_TO_INT = { } _RELATIONS = frozenset(( - _EQUAL, - _UNEQUAL, - _LESS, - _LESS_EQUAL, - _GREATER, - _GREATER_EQUAL, + EQUAL, + UNEQUAL, + LESS, + LESS_EQUAL, + GREATER, + GREATER_EQUAL, )) # Token to relation (=, !=, <, ...) mapping _TOKEN_TO_REL = { - _T_EQUAL: _EQUAL, - _T_GREATER: _GREATER, - _T_GREATER_EQUAL: _GREATER_EQUAL, - _T_LESS: _LESS, - _T_LESS_EQUAL: _LESS_EQUAL, - _T_UNEQUAL: _UNEQUAL, + _T_EQUAL: EQUAL, + _T_GREATER: GREATER, + _T_GREATER_EQUAL: GREATER_EQUAL, + _T_LESS: LESS, + _T_LESS_EQUAL: LESS_EQUAL, + _T_UNEQUAL: UNEQUAL, } _RELATION_TO_STR = { - _EQUAL: "=", - _GREATER: ">", - _GREATER_EQUAL: ">=", - _LESS: "<", - _LESS_EQUAL: "<=", - _UNEQUAL: "!=", + EQUAL: "=", + GREATER: ">", + GREATER_EQUAL: ">=", + LESS: "<", + LESS_EQUAL: "<=", + UNEQUAL: "!=", } diff --git a/testsuite.py b/testsuite.py index 69eae6e..4eba62a 100644 --- a/testsuite.py +++ b/testsuite.py @@ -110,7 +110,7 @@ def get_items(config, type_): items.append(node.item) rec(node.list) rec(node.next) - rec(config.top_menu) + rec(config.top_node) return items def get_comments(config): @@ -121,7 +121,7 @@ def get_comments(config): items.append(node) rec(node.list) rec(node.next) - rec(config.top_menu) + rec(config.top_node) return items def get_menus(config): @@ -132,7 +132,7 @@ def get_menus(config): items.append(node) rec(node.list) rec(node.next) - rec(config.top_menu) + rec(config.top_node) return items def get_choices(config): @@ -1803,7 +1803,7 @@ def run_compatibility_tests(): if all_passed: print("All selftests and compatibility tests passed") - print(nconfigs, "arch/defconfig pairs tested") + print("{} arch/defconfig pairs tested".format(nconfigs)) else: print("Some tests failed") -- cgit v1.2.3 From a90d18d8dca4e50b6f305599caa96828a520be01 Mon Sep 17 00:00:00 2001 From: Ulf Magnusson Date: Wed, 25 Oct 2017 22:41:57 +0200 Subject: Backup --- kconfiglib.py | 648 ++++++++++++++++++++++++++++++---------------------------- testsuite.py | 65 +++--- 2 files changed, 372 insertions(+), 341 deletions(-) diff --git a/kconfiglib.py b/kconfiglib.py index 04edb67..0d92b48 100644 --- a/kconfiglib.py +++ b/kconfiglib.py @@ -110,16 +110,17 @@ A || B (OR, A, B) A = B (EQUAL, A, B) A != "foo" (UNEQUAL, A, "foo") A || (B && C && D) (OR, A, (AND, B, (AND, C, D))) -y "y" -"y" "y" +y config.y +"y" config.y -As seen in the final two examples, n/m/y are always represented as the strings -(constant symbols) "n"/"m"/"y" in Kconfiglib, regardless of whether they're -written with or without quotes. This simplifies some internals. +As seen in the final two examples, n/m/y are always represented as the constant +symbols config.n/m/y, regardless of whether they're written with or without +quotes. 'config' represents the Config instance of the configuration the +expression appears in. A missing expression (e.g. if the 'if ' part is removed from -'default A if ') is represented as "y". The standard __str__() functions -avoid printing 'if y' conditions to give cleaner output. +'default A if ') is represented as config.y. The standard __str__() +functions avoid printing 'if y' conditions to give cleaner output. Implementation note ------------------- @@ -131,6 +132,8 @@ Send bug reports, suggestions, and questions to ulfalizer a.t Google's email service (or open a ticket on the GitHub page). """ +# TODO: document n/m/y + import errno import os import platform @@ -253,13 +256,17 @@ class Config(object): "_set_re", "_unset_re", "config_prefix", + "const_syms", "defconfig_list", "defined_syms", + "m", "modules", + "n", "named_choices", "srctree", "syms", "top_node", + "y", ) # @@ -308,20 +315,40 @@ class Config(object): self._print_undef_assign = False self.syms = {} + self.const_syms = {} self.defined_syms = [] self.named_choices = {} # Used for quickly invalidating all choices self._choices = [] - self.modules = self._lookup_sym("MODULES") + for nmy in "n", "m", "y": + sym = Symbol() + sym.config = self + sym.name = nmy + sym.is_constant = True + sym._type = TRISTATE + sym._cached_val = nmy + + self.const_syms[nmy] = sym + + self.n = self.const_syms["n"] + self.m = self.const_syms["m"] + self.y = self.const_syms["y"] + + # Just to make n/m/y well-formed symbols + for nmy in "n", "m", "y": + sym = self.const_syms[nmy] + sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n + + self.modules = self._lookup_sym("MODULES", False) self.defconfig_list = None # Predefined symbol. DEFCONFIG_LIST uses this. - uname_sym = Symbol() + uname_sym = self._lookup_const_sym("UNAME_RELEASE", False) uname_sym._type = STRING - uname_sym.name = "UNAME_RELEASE" - uname_sym.config = self - uname_sym.defaults.append((platform.uname()[2], "y")) + uname_sym.defaults.append( + (self._lookup_const_sym(platform.uname()[2], False), + self.y)) # env_var doubles as the SYMBOL_AUTO flag from the C implementation, so # just set it to something. The naming breaks a bit here, but it's # pretty obscure. @@ -331,10 +358,10 @@ class Config(object): self.top_node = MenuNode() self.top_node.config = self self.top_node.item = MENU - self.top_node.visibility = None - self.top_node.prompt = ("Linux Kernel Configuration", "y") + self.top_node.visibility = self.y + self.top_node.prompt = ("Linux Kernel Configuration", self.y) self.top_node.parent = None - self.top_node.dep = "y" + self.top_node.dep = self.y self.top_node.filename = filename self.top_node.linenr = 1 @@ -342,7 +369,7 @@ class Config(object): self._parse_block(_FileFeed(self._open(filename), filename), None, # end_token self.top_node, # parent - "y", # visible_if_deps + self.y, # visible_if_deps None, # prev_line self.top_node) # prev_node @@ -372,7 +399,7 @@ class Config(object): return None for filename, cond_expr in self.defconfig_list.defaults: if eval_expr(cond_expr) != "n": - filename = self._expand_sym_refs(filename) + filename = self._expand_sym_refs(filename.value) try: with self._open(filename) as f: return f.name @@ -502,11 +529,17 @@ class Config(object): conditional ('if ...') expressions in the configuration (as well as in the C tools). m is rewritten to 'm && MODULES'. """ - return eval_expr(self._parse_expr(self._tokenize(s, True), - s, - None, # filename - None, # linenr - True)) # transform_m + return eval_expr( + self._parse_expr( + self._tokenize( + s, + True, # for_eval_string + None, # filename + None), # linenr + s, + None, # filename + None, # linenr + True)) # transform_m def unset_values(self): """ @@ -612,7 +645,7 @@ class Config(object): # Kconfig parsing # - def _tokenize(self, s, for_eval, filename=None, linenr=None): + def _tokenize(self, s, for_eval_string, filename, linenr): """ Returns a _Feed instance containing tokens derived from the string 's'. Registers any new symbols encountered (via _lookup_sym()). @@ -620,7 +653,7 @@ class Config(object): Tries to be reasonably speedy by processing chunks of text via regexes and string operations where possible. This is a hotspot during parsing. - for_eval: + for_eval_string: True when parsing an expression for a call to Config.eval_string(), in which case we should not treat the first token specially nor register new symbols. @@ -629,7 +662,7 @@ class Config(object): # Tricky implementation detail: While parsing a token, 'token' refers # to the previous token. See _NOT_REF for why this is needed. - if for_eval: + if for_eval_string: token = None tokens = [] @@ -678,13 +711,14 @@ class Config(object): token = keyword elif token not in _STRING_LEX: - # It's a symbol + # It's a non-const symbol... if name in ("n", "m", "y"): - # Always represent n, m, y as strings (constant - # symbols). This simplifies the expression logic. - token = name + # ...except we translate n, m, and y into the + # corresponding constant symbols, like the C + # implementation + token = self._lookup_const_sym(name, for_eval_string) else: - token = self._lookup_sym(name, for_eval) + token = self._lookup_sym(name, for_eval_string) else: # It's a case of missing quotes. For example, the @@ -699,7 +733,7 @@ class Config(object): token = name else: - # Not an identifier/keyword + # Not keyword/non-const symbol # Note: _id_keyword_match and _initial_token_match strip # trailing whitespace, making it safe to assume s[i] is the @@ -722,7 +756,7 @@ class Config(object): end = s.find(c, i) if end == -1: _tokenization_error(s, filename, linenr) - token = s[i:end] + val = s[i:end] i = end + 1 else: # Slow path: This could probably be sped up, but it's a @@ -744,7 +778,17 @@ class Config(object): val += c i += 1 i += 1 - token = val + + # This is the only place where we don't survive with a + # single token of lookback, hence the kludge: + # 'option env="FOO"' does not refer to a constant symbol + # named "FOO". + token = \ + val \ + if token in _STRING_LEX or \ + (not for_eval_string and \ + tokens[0] == _T_OPTION) else \ + self._lookup_const_sym(val, for_eval_string) elif c == "&": # Invalid characters are ignored @@ -935,10 +979,10 @@ class Config(object): node.filename = line_feeder.filename node.linenr = line_feeder.linenr node.dep = \ - _make_and(parent.dep, - self._parse_expr(tokens, line, - line_feeder.filename, - line_feeder.linenr, True)) + self._make_and(parent.dep, + self._parse_expr(tokens, line, + line_feeder.filename, + line_feeder.linenr, True)) self._parse_block(line_feeder, _T_ENDIF, @@ -956,7 +1000,7 @@ class Config(object): node = MenuNode() node.config = self node.item = MENU - node.visibility = "y" + node.visibility = self.y node.parent = parent node.filename = line_feeder.filename node.linenr = line_feeder.linenr @@ -968,7 +1012,8 @@ class Config(object): self._parse_block(line_feeder, _T_ENDMENU, node, # parent - _make_and(visible_if_deps, node.visibility), + self._make_and(visible_if_deps, + node.visibility), prev_line, node) # prev_node node.list = node.next @@ -1033,7 +1078,7 @@ class Config(object): prev_node.next = prev_node = node elif t0 == _T_MAINMENU: - self.top_node.prompt = (tokens.next(), "y") + self.top_node.prompt = (tokens.next(), self.y) self.top_node.filename = line_feeder.filename self.top_node.linenr = line_feeder.linenr @@ -1044,16 +1089,16 @@ class Config(object): def _parse_cond(self, tokens, line, filename, linenr): """ Parses an optional 'if ' construct and returns the parsed , - or "y" if the next token is not _T_IF + or self.y if the next token is not _T_IF """ return self._parse_expr(tokens, line, filename, linenr, True) \ - if tokens.check(_T_IF) else "y" + if tokens.check(_T_IF) else self.y def _parse_val_and_cond(self, tokens, line, filename, linenr): """ Parses ' if ' constructs, where the 'if' part is - optional. Returns a tuple containing the parsed expressions, with "y" - as the second element if the 'if' part is missing. + optional. Returns a tuple containing the parsed expressions, with + config.y as the second element if the 'if' part is missing. """ return (self._parse_expr(tokens, line, filename, linenr, False), self._parse_cond(tokens, line, filename, linenr)) @@ -1092,7 +1137,7 @@ class Config(object): # Menu node dependency from 'depends on'. Will get propagated to the # properties above. - node.dep = "y" + node.dep = self.y # The cached (line, tokens) tuple that we return last_line = None @@ -1117,9 +1162,9 @@ class Config(object): filename, linenr) node.dep = \ - _make_and(node.dep, - self._parse_expr(tokens, line, filename, - linenr, True)) + self._make_and(node.dep, + self._parse_expr(tokens, line, filename, + linenr, True)) elif t0 == _T_HELP: # Find first non-blank (not all-space) line and get its @@ -1164,34 +1209,24 @@ class Config(object): _parse_error(line, "only symbols can select", filename, linenr) - # HACK: We always represent n/m/y using the constant symbol - # "n"/"m"/"y" forms, but that causes a crash if a Kconfig file - # does e.g. 'select n' (which is meaningless and probably stems - # from a misunderstanding). Seen in U-Boot. Just skip the - # select. - target = tokens.next() - if target not in ("n", "m", "y"): - selects.append( - (target, - self._parse_cond(tokens, line, filename, linenr))) + selects.append( + (tokens.next(), + self._parse_cond(tokens, line, filename, linenr))) elif t0 == _T_IMPLY: if not isinstance(node.item, Symbol): _parse_error(line, "only symbols can imply", filename, linenr) - # See above - target = tokens.next() - if target not in ("n", "m", "y"): - implies.append( - (target, - self._parse_cond(tokens, line, filename, linenr))) + implies.append( + (tokens.next(), + self._parse_cond(tokens, line, filename, linenr))) elif t0 in (_T_BOOL, _T_TRISTATE, _T_INT, _T_HEX, _T_STRING): node.item._type = _TOKEN_TO_TYPE[t0] if tokens.peek() is not None: - prompt = self._parse_val_and_cond(tokens, line, - filename, linenr) + prompt = (tokens.next(), + self._parse_cond(tokens, line, filename, linenr)) elif t0 == _T_DEFAULT: defaults.append( @@ -1199,17 +1234,16 @@ class Config(object): elif t0 in (_T_DEF_BOOL, _T_DEF_TRISTATE): node.item._type = _TOKEN_TO_TYPE[t0] - if tokens.peek() is not None: - defaults.append( - self._parse_val_and_cond(tokens, line, filename, - linenr)) + defaults.append( + self._parse_val_and_cond(tokens, line, filename, + linenr)) elif t0 == _T_PROMPT: # 'prompt' properties override each other within a single # definition of a symbol, but additional prompts can be added # by defining the symbol multiple times - prompt = self._parse_val_and_cond(tokens, line, filename, - linenr) + prompt = (tokens.next(), + self._parse_cond(tokens, line, filename, linenr)) elif t0 == _T_RANGE: ranges.append( @@ -1236,7 +1270,10 @@ class Config(object): "service.".format(node.item.name, env_var), filename, linenr) else: - defaults.append((os.environ[env_var], "y")) + defaults.append( + (self._lookup_const_sym(os.environ[env_var], + False), + self.y)) elif tokens.check(_T_DEFCONFIG_LIST): if self.defconfig_list is None: @@ -1283,9 +1320,9 @@ class Config(object): filename, linenr) node.visibility = \ - _make_and(node.visibility, - self._parse_expr(tokens, line, filename, linenr, - True)) + self._make_and(node.visibility, + self._parse_expr(tokens, line, filename, + linenr, True)) elif t0 == _T_OPTIONAL: if not isinstance(node.item, Choice): @@ -1306,54 +1343,57 @@ class Config(object): # from node.dep propagated. # First propagate parent dependencies to node.dep - node.dep = _make_and(node.dep, node.parent.dep) + node.dep = self._make_and(node.dep, node.parent.dep) if isinstance(node.item, (Symbol, Choice)): if isinstance(node.item, Symbol): - node.item.direct_deps = \ - _make_or(node.item.direct_deps, node.dep) + node.item.direct_dep = \ + self._make_or(node.item.direct_dep, node.dep) # Set the prompt, with dependencies propagated if prompt is not None: node.prompt = (prompt[0], - _make_and(_make_and(prompt[1], node.dep), - visible_if_deps)) + self._make_and(self._make_and(prompt[1], + node.dep), + visible_if_deps)) else: node.prompt = None # Add the new defaults, with dependencies propagated for val_expr, cond_expr in defaults: node.item.defaults.append( - (val_expr, _make_and(cond_expr, node.dep))) + (val_expr, self._make_and(cond_expr, node.dep))) # Add the new ranges, with dependencies propagated for low, high, cond_expr in ranges: node.item.ranges.append( - (low, high, _make_and(cond_expr, node.dep))) + (low, high, self._make_and(cond_expr, node.dep))) # Handle selects for target, cond_expr in selects: # Only stored for convenience. Not used during evaluation. node.item.selects.append( - (target, _make_and(cond_expr, node.dep))) + (target, self._make_and(cond_expr, node.dep))) # Modify the dependencies of the selected symbol target.rev_dep = \ - _make_or(target.rev_dep, - _make_and(node.item, - _make_and(cond_expr, node.dep))) + self._make_or(target.rev_dep, + self._make_and(node.item, + self._make_and(cond_expr, + node.dep))) # Handle implies for target, cond_expr in implies: # Only stored for convenience. Not used during evaluation. node.item.implies.append( - (target, _make_and(cond_expr, node.dep))) + (target, self._make_and(cond_expr, node.dep))) # Modify the dependencies of the implied symbol target.weak_rev_dep = \ - _make_or(target.weak_rev_dep, - _make_and(node.item, - _make_and(cond_expr, node.dep))) + self._make_or(target.weak_rev_dep, + self._make_and(node.item, + self._make_and(cond_expr, + node.dep))) # Return cached non-property line return last_line @@ -1438,7 +1478,7 @@ class Config(object): def _parse_factor(self, feed, line, filename, linenr, transform_m): token = feed.next() - if isinstance(token, (Symbol, str)): + if isinstance(token, Symbol): # Plain symbol or relation next_token = feed.peek() @@ -1448,8 +1488,8 @@ class Config(object): # For conditional expressions ('depends on ', # '... if ', etc.), "m" and m are rewritten to # "m" && MODULES. - if transform_m and token == "m": - return (AND, "m", self.modules) + if transform_m and token is self.m: + return (AND, self.m, self.modules) return token @@ -1473,12 +1513,12 @@ class Config(object): # Symbol lookup # - def _lookup_sym(self, name, for_eval=False): + def _lookup_sym(self, name, for_eval_string): """ Fetches the symbol 'name' from the symbol table, creating and - registering it if it does not exist. If 'for_eval' is True, the symbol - won't be added to the symbol table if it does not exist. This is for - Config.eval_string(). + registering it if it does not exist. TODO If 'for_eval_string' is True, + the symbol won't be added to the symbol table if it does not exist. + This is for Config.eval_string(). """ if name in self.syms: return self.syms[name] @@ -1486,10 +1526,32 @@ class Config(object): sym = Symbol() sym.config = self sym.name = name - if for_eval: + sym.is_constant = False + sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n + + if for_eval_string: self._warn("no symbol {} in configuration".format(name)) else: self.syms[name] = sym + + return sym + + def _lookup_const_sym(self, name, for_eval_string): + """ + TODO: say something + """ + if name in self.const_syms: + return self.const_syms[name] + + sym = Symbol() + sym.config = self + sym.name = name + sym.is_constant = True + sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n + + if not for_eval_string: + self.const_syms[name] = sym + return sym # @@ -1534,6 +1596,7 @@ class Config(object): elif (node.item == MENU and eval_expr(node.dep) != "n" and eval_expr(node.visibility) != "n") or \ (node.item == COMMENT and eval_expr(node.dep) != "n"): + add_fn("\n#\n# {}\n#\n".format(node.prompt[0])) # Iterative tree walk using parent pointers @@ -1557,21 +1620,14 @@ class Config(object): def _build_dep(self): """ - Populates the Symbol._direct_dependents sets, linking the symbol to the - symbols that immediately depend on it in the sense that changing the - value of the symbol might affect the values of those other symbols. - This is used for caching/invalidation purposes. The calculated sets - might be larger than necessary as we don't do any complicated analysis - of the expressions. - """ + Populates the Symbol._direct_dependents sets, which link symbols to the + symbols that immediately depend on them in the sense that changing the + value of the symbol might affect the values of the dependent symbols. + This is used for caching/invalidation purposes. - # Adds 'sym' as a directly dependent symbol to all symbols that appear - # in the expression 'expr' - def add_expr_deps(expr, sym): - res = [] - _expr_syms(expr, res) - for expr_sym in res: - expr_sym._direct_dependents.add(sym) + The calculated sets might be larger than necessary as we don't do any + complex analysis of the expressions. + """ # The directly dependent symbols of a symbol S are: # @@ -1579,13 +1635,12 @@ class Config(object): # condition), weak_rev_dep (imply condition), or ranges depend on S # # - Any symbol that has S as a direct dependency (has S in - # direct_deps). This is needed to get invalidation right for + # direct_dep). This is needed to get invalidation right for # 'imply'. # - # - Any symbols that belong to the same choice statement as S - # (these won't be included in S._direct_dependents as that makes the - # dependency graph unwieldy, but S._get_dependent() will include - # them) + # - Any symbols that belong to the same choice statement as S. These + # won't be included in S._direct_dependents as it creates dependency + # loops, but S._get_dependent() includes them. # # - Any symbols in a choice statement that depends on S @@ -1597,28 +1652,29 @@ class Config(object): for sym in self.defined_syms: for node in sym.nodes: if node.prompt is not None: - add_expr_deps(node.prompt[1], sym) + _make_depend_on(sym, node.prompt[1]) for value, cond in sym.defaults: - add_expr_deps(value, sym) - add_expr_deps(cond, sym) + _make_depend_on(sym, value) + _make_depend_on(sym, cond) - add_expr_deps(sym.rev_dep, sym) - add_expr_deps(sym.weak_rev_dep, sym) + _make_depend_on(sym, sym.rev_dep) + _make_depend_on(sym, sym.weak_rev_dep) - for l, u, e in sym.ranges: - add_expr_deps(l, sym) - add_expr_deps(u, sym) - add_expr_deps(e, sym) + for low, high, cond in sym.ranges: + _make_depend_on(sym, low) + _make_depend_on(sym, high) + _make_depend_on(sym, cond) - add_expr_deps(sym.direct_deps, sym) + _make_depend_on(sym, sym.direct_dep) if sym.choice is not None: for node in sym.choice.nodes: if node.prompt is not None: - add_expr_deps(node.prompt[1], sym) - for _, e in sym.choice.defaults: - add_expr_deps(e, sym) + _make_depend_on(sym, node.prompt[1]) + + for _, cond in sym.choice.defaults: + _make_depend_on(sym, cond) def _invalidate_all(self): # Undefined symbols never change value and don't need to be @@ -1674,6 +1730,36 @@ class Config(object): 'attempt to assign the value "{}" to the undefined symbol {}' \ .format(val, name), filename, linenr) + def _make_and(self, e1, e2): + """ + Constructs an AND (&&) expression. Performs trivial simplification. + """ + if e1 is self.y: + return e2 + + if e2 is self.y: + return e1 + + if e1 is self.n or e2 is self.n: + return self.n + + return (AND, e1, e2) + + def _make_or(self, e1, e2): + """ + Constructs an OR (||) expression. Performs trivial simplification. + """ + if e1 is self.n: + return e2 + + if e2 is self.n: + return e1 + + if e1 is self.y or e2 is self.y: + return self.y + + return (OR, e1, e2) + class Symbol(object): """ Represents a configuration symbol: @@ -1774,23 +1860,23 @@ class Symbol(object): defaults: List of (default, cond) tuples for the symbol's 'default's. For example, 'default A && B if C || D' is represented as ((AND, A, B), (OR, C, D)). - If there is no condition, 'cond' is None. + If no condition was given, 'cond' is self.config.y. Note that 'depends on' and parent dependencies are propagated to 'default' conditions. selects: List of (symbol, cond) tuples for the symbol's 'select's. For example, - 'select A if B' is represented as (A, B). If there is no condition, - 'cond' is None. + 'select A if B' is represented as (A, B). If no condition was given, + 'cond' is self.config.y. Note that 'depends on' and parent dependencies are propagated to 'select' conditions. implies: List of (symbol, cond) tuples for the symbol's 'imply's. For example, - 'imply A if B' is represented as (A, B). If there is no condition, 'cond' - is None. + 'imply A if B' is represented as (A, B). If no condition was given, + 'cond' is self.config.y. Note that 'depends on' and parent dependencies are propagated to 'imply' conditions. @@ -1798,7 +1884,7 @@ class Symbol(object): ranges: List of (low, high, cond) tuples for the symbol's 'range's. For example, 'range 1 2 if A' is represented as (1, 2, A). If there is no condition, - 'cond' is None. + 'cond' is self.config.y. Note that 'depends on' and parent dependencies are propagated to 'range' conditions. @@ -1816,7 +1902,7 @@ class Symbol(object): weak_rev_dep: Like rev_dep, for imply. - direct_deps: + direct_dep: The 'depends on' dependencies. If a symbol is defined in multiple locations, the dependencies at each location are ORed together. @@ -1854,10 +1940,11 @@ class Symbol(object): "choice", "config", "defaults", - "direct_deps", + "direct_dep", "env_var", "implies", "is_allnoconfig_y", + "is_constant", "name", "nodes", "ranges", @@ -1926,7 +2013,7 @@ class Symbol(object): # Weak reverse dependencies are only considered if our # direct dependencies are met - if eval_expr(self.direct_deps) != "n": + if eval_expr(self.direct_dep) != "n": weak_rev_dep_val = \ eval_expr(self.weak_rev_dep) if weak_rev_dep_val != "n": @@ -1974,13 +2061,10 @@ class Symbol(object): if eval_expr(cond_expr) != "n": has_active_range = True - low_str = _str_val(low_expr) - high_str = _str_val(high_expr) - - low = int(low_str, base) if \ - _is_base_n(low_str, base) else 0 - high = int(high_str, base) if \ - _is_base_n(high_str, base) else 0 + low = int(low_expr.value, base) if \ + _is_base_n(low_expr.value, base) else 0 + high = int(high_expr.value, base) if \ + _is_base_n(high_expr.value, base) else 0 break else: @@ -2009,7 +2093,7 @@ class Symbol(object): # preserved as is. Defaults that do not satisfy a range # constraints are clamped and take on a standard form. - val = _str_val(val_expr) + val = val_expr.value if _is_base_n(val, base): val_num = int(val, base) @@ -2044,7 +2128,7 @@ class Symbol(object): for val_expr, cond_expr in self.defaults: if eval_expr(cond_expr) != "n": self._write_to_conf = True - val = _str_val(val_expr) + val = val_expr.value break self._cached_val = val @@ -2187,7 +2271,7 @@ class Symbol(object): if self is self.config.modules: fields.append("is the modules symbol") - fields.append("direct deps " + eval_expr(self.direct_deps)) + fields.append("direct deps " + eval_expr(self.direct_dep)) fields.append("{} menu node{}" .format(len(self.nodes), @@ -2207,24 +2291,24 @@ class Symbol(object): # These attributes are always set on the instance from outside and # don't need defaults: + # _already_written # config + # direct_dep + # is_constant # name - # _already_written + # rev_dep + # weak_rev_dep self._type = UNKNOWN self.defaults = [] self.selects = [] self.implies = [] self.ranges = [] - self.rev_dep = "n" - self.weak_rev_dep = "n" self.nodes = [] self.user_value = None - self.direct_deps = "n" - # Populated in Config._build_dep() after parsing. Links the symbol to # the symbols that immediately depend on it (in a caching/invalidation # sense). The total set of dependent symbols for the symbol is @@ -2245,9 +2329,8 @@ class Symbol(object): # Flags - self.env_var = None - self.choice = None + self.env_var = None self.is_allnoconfig_y = False # Should the symbol get an entry in .config? Calculated along with the @@ -2488,7 +2571,7 @@ class Choice(object): defaults: List of (symbol, cond) tuples for the choices 'defaults's. For example, 'default A if B && C' is represented as (A, (AND, B, C)). If there is no - condition, 'cond' is None. + condition, 'cond' is self.config.y. Note that 'depends on' and parent dependencies are propagated to 'default' conditions. @@ -2789,7 +2872,7 @@ class MenuNode(object): visibility: The 'visible if' dependencies for the menu node (which must represent a - menu). None if there are no 'visible if' dependencies. 'visible if' + menu). config.y if there are no 'visible if' dependencies. 'visible if' dependencies are recursively propagated to the prompts of symbols and choices within the menu. @@ -2826,18 +2909,23 @@ class MenuNode(object): if isinstance(self.item, Symbol): fields.append("menu node for symbol " + self.item.name) + elif isinstance(self.item, Choice): s = "menu node for choice" if self.item.name is not None: s += " " + self.item.name fields.append(s) + elif self.item == MENU: fields.append("menu node for menu") + elif self.item == COMMENT: fields.append("menu node for comment") + elif self.item is None: fields.append("menu node for if (should not appear in the final " " tree)") + else: raise InternalError("unable to determine type in " "MenuNode.__repr__()") @@ -2923,9 +3011,6 @@ def eval_expr(expr): # regardless of their value return expr.value if expr._type in (BOOL, TRISTATE) else "n" - if isinstance(expr, str): - return expr if expr in ("m", "y") else "n" - if expr[0] == AND: ev1 = eval_expr(expr[1]) @@ -2955,25 +3040,20 @@ def eval_expr(expr): # pythonic way to structure this. oper, op1, op2 = expr - op1_type, op1_str = _type_and_val(op1) - op2_type, op2_str = _type_and_val(op2) # If both operands are strings... - if op1_type == STRING and op2_type == STRING: + if op1._type == STRING and op2._type == STRING: # ...then compare them lexicographically - comp = _strcmp(op1_str, op2_str) + comp = _strcmp(op1.value, op2.value) else: - # Otherwise, try to compare them as numbers + # Otherwise, try to compare them as numbers... try: - comp = int(op1_str, _TYPE_TO_BASE[op1_type]) - \ - int(op2_str, _TYPE_TO_BASE[op2_type]) + comp = int(op1.value, _TYPE_TO_BASE[op1._type]) - \ + int(op2.value, _TYPE_TO_BASE[op2._type]) except ValueError: - # They're not both valid numbers. If the comparison is - # anything but = or !=, return 'n'. Otherwise, reuse - # _strcmp() to check for (in)equality. - if oper not in (EQUAL, UNEQUAL): - return "n" - comp = _strcmp(op1_str, op2_str) + # Fall back on a lexicographic comparison if the operands don't + # parse as numbers + comp = _strcmp(op1.value, op2.value) if oper == EQUAL: res = comp == 0 elif oper == UNEQUAL: res = comp != 0 @@ -2987,6 +3067,26 @@ def eval_expr(expr): _internal_error("Internal error while evaluating expression: " "unknown operation {}.".format(expr[0])) +def expr_str(expr): + if isinstance(expr, Symbol): + return expr.name if not expr.is_constant else '"{}"'.format(expr.name) + + if expr[0] == NOT: + if isinstance(expr[1], Symbol): + return "!" + expr_str(expr[1]) + return "!({})".format(expr_str(expr[1])) + + if expr[0] == AND: + return "{} && {}".format(_format_and_op(expr[1]), + _format_and_op(expr[2])) + + if expr[0] == OR: + return "{} || {}".format(expr_str(expr[1]), expr_str(expr[2])) + + # Relation + return "{} {} {}".format(expr_str(expr[1]), + _RELATION_TO_STR[expr[0]], + expr_str(expr[2])) # # Internal classes @@ -3109,36 +3209,6 @@ def _get_visibility(sc): return vis -def _make_and(e1, e2): - """ - Constructs an AND (&&) expression. Performs trivial simplification. - """ - if e1 == "y": - return e2 - - if e2 == "y": - return e1 - - if e1 == "n" or e2 == "n": - return "n" - - return (AND, e1, e2) - -def _make_or(e1, e2): - """ - Constructs an OR (||) expression. Performs trivial simplification. - """ - if e1 == "n": - return e2 - - if e2 == "n": - return e1 - - if e1 == "y" or e2 == "y": - return "y" - - return (OR, e1, e2) - def _tri_min(v1, v2): """ Returns the smallest tristate value among v1 and v2. @@ -3151,88 +3221,40 @@ def _tri_max(v1, v2): """ return v1 if _TRI_TO_INT[v1] >= _TRI_TO_INT[v2] else v2 -def _expr_syms_rec(expr, res): +def _make_depend_on(sym, expr): """ - _expr_syms() helper. Recurses through expressions. + Adds 'sym' as a dependency to all symbols in 'expr'. Constant symbols in + 'expr' are skipped as they can never change value anyway. """ if isinstance(expr, Symbol): - res.append(expr) - elif isinstance(expr, str): - return + if not expr.is_constant: + expr._direct_dependents.add(sym) + elif expr[0] in (AND, OR): - _expr_syms_rec(expr[1], res) - _expr_syms_rec(expr[2], res) + _make_depend_on(sym, expr[1]) + _make_depend_on(sym, expr[2]) + elif expr[0] == NOT: - _expr_syms_rec(expr[1], res) + _make_depend_on(sym, expr[1]) + elif expr[0] in _RELATIONS: - if isinstance(expr[1], Symbol): - res.append(expr[1]) - if isinstance(expr[2], Symbol): - res.append(expr[2]) + if not expr[1].is_constant: + expr[1]._direct_dependents.add(sym) + if not expr[2].is_constant: + expr[2]._direct_dependents.add(sym) + else: _internal_error("Internal error while fetching symbols from an " "expression with token stream {}.".format(expr)) -def _expr_syms(expr, res): - """ - append()s the symbols in 'expr' to 'res'. Does not remove duplicates. - """ - if expr is not None: - _expr_syms_rec(expr, res) - -def _str_val(obj): - """ - Returns the value of obj as a string. If obj is not a string (constant - symbol), it must be a Symbol. - """ - return obj if isinstance(obj, str) else obj.value - def _format_and_op(expr): """ - _expr_to_str() helper. Returns the string representation of 'expr', which - is assumed to be an operand to AND, with parentheses added if needed. + expr_str() helper. Returns the string representation of 'expr', which is + assumed to be an operand to AND, with parentheses added if needed. """ if isinstance(expr, tuple) and expr[0] == OR: - return "({})".format(_expr_to_str(expr)) - return _expr_to_str(expr) - -def _expr_to_str(expr): - if isinstance(expr, str): - if expr in ("n", "m", "y"): - # Don't print spammy quotes for these - return expr - return '"{}"'.format(expr) - - if isinstance(expr, Symbol): - return expr.name - - if expr[0] == NOT: - if isinstance(expr[1], (str, Symbol)): - return "!" + _expr_to_str(expr[1]) - return "!({})".format(_expr_to_str(expr[1])) - - if expr[0] == AND: - return "{} && {}".format(_format_and_op(expr[1]), - _format_and_op(expr[2])) - - if expr[0] == OR: - return "{} || {}".format(_expr_to_str(expr[1]), - _expr_to_str(expr[2])) - - # Relation - return "{} {} {}".format(_expr_to_str(expr[1]), - _RELATION_TO_STR[expr[0]], - _expr_to_str(expr[2])) - -def _type_and_val(obj): - """ - Helper to hack around the fact that we don't represent plain strings as - Symbols. Takes either a plain string or a Symbol and returns a (, - ) tuple. - """ - return (obj._type, obj.value) \ - if not isinstance(obj, str) \ - else (STRING, obj) + return "({})".format(expr_str(expr)) + return expr_str(expr) def _indentation(line): """ @@ -3335,8 +3357,8 @@ def _sym_choice_str(sc): if node.prompt is not None: prompt_str = 'prompt "{}"'.format(node.prompt[0]) - if node.prompt[1] != "y": - prompt_str += " if " + _expr_to_str(node.prompt[1]) + if node.prompt[1] is not sc.config.y: + prompt_str += " if " + expr_str(node.prompt[1]) indent_add(prompt_str) if node is sc.nodes[0]: @@ -3353,16 +3375,16 @@ def _sym_choice_str(sc): if isinstance(sc, Symbol): for range_ in sc.ranges: range_string = "range {} {}" \ - .format(_expr_to_str(range_[0]), - _expr_to_str(range_[1])) - if range_[2] != "y": - range_string += " if " + _expr_to_str(range_[2]) + .format(expr_str(range_[0]), + expr_str(range_[1])) + if range_[2] is not sc.config.y: + range_string += " if " + expr_str(range_[2]) indent_add(range_string) for default in sc.defaults: - default_string = "default " + _expr_to_str(default[0]) - if default[1] != "y": - default_string += " if " + _expr_to_str(default[1]) + default_string = "default " + expr_str(default[0]) + if default[1] is not sc.config.y: + default_string += " if " + expr_str(default[1]) indent_add(default_string) if isinstance(sc, Choice) and sc.is_optional: @@ -3371,14 +3393,14 @@ def _sym_choice_str(sc): if isinstance(sc, Symbol): for select in sc.selects: select_string = "select " + select[0].name - if select[1] != "y": - select_string += " if " + _expr_to_str(select[1]) + if select[1] is not sc.config.y: + select_string += " if " + expr_str(select[1]) indent_add(select_string) for imply in sc.implies: imply_string = "imply " + imply[0].name - if imply[1] != "y": - imply_string += " if " + _expr_to_str(imply[1]) + if imply[1] is not sc.config.y: + imply_string += " if " + expr_str(imply[1]) indent_add(imply_string) if node.help is not None: @@ -3394,45 +3416,36 @@ def _sym_choice_str(sc): # Menu manipulation -def _eq_to_sym(eq): - """ - _expr_depends_on() helper. For (in)equalities of the form sym = y/m or - sym != n, returns sym. For other (in)equalities, returns None. - """ - relation, left, right = eq - - # Make sure the symbol (if any) appears to the left - if not isinstance(left, Symbol): - left, right = right, left - if not isinstance(left, Symbol): - return None - if (relation == EQUAL and right in ("m", "y")) or \ - (relation == UNEQUAL and right == "n"): - return left - return None - def _expr_depends_on(expr, sym): """ Reimplementation of expr_depends_symbol() from mconf.c. Used to determine if a submenu should be implicitly created, which influences what items inside choice statements are considered choice items. """ - if expr is None: - return False + if isinstance(expr, Symbol): + return expr is sym - def rec(expr): - if isinstance(expr, str): + if expr[0] in (EQUAL, UNEQUAL): + # Check for one of the following: + # sym = m/y, m/y = sym, sym != n, n != sym + + left, right = expr[1:] + + if right is sym: + left, right = right, left + + if left is not sym: return False - if isinstance(expr, Symbol): - return expr is sym - if expr[0] in (EQUAL, UNEQUAL): - return _eq_to_sym(expr) is sym - if expr[0] == AND: - return rec(expr[1]) or rec(expr[2]) - return False + return (expr[0] == EQUAL and right is sym.config.m or \ + right is sym.config.y) or \ + (expr[0] == UNEQUAL and right is sym.config.n) - return rec(expr) + if expr[0] == AND: + return _expr_depends_on(expr[1], sym) or \ + _expr_depends_on(expr[2], sym) + + return False def _has_auto_menu_dep(node1, node2): """ @@ -3440,7 +3453,6 @@ def _has_auto_menu_dep(node1, node2): has a prompt, we check its condition. Otherwise, we look directly at node2.dep. """ - if node2.prompt: return _expr_depends_on(node2.prompt[1], node1.item) @@ -3776,8 +3788,8 @@ _DEFAULT_VALUE = { _NO_CACHED_SELECTION = object() # Used in comparisons. 0 means the base is inferred from the format of the -# string. The entries for BOOL and TRISTATE are a convenience - they should -# never convert to valid numbers. +# string. The entries for BOOL and TRISTATE are an implementation convenience: +# They should never convert to valid numbers. _TYPE_TO_BASE = { BOOL: 0, HEX: 16, diff --git a/testsuite.py b/testsuite.py index 4eba62a..7859369 100644 --- a/testsuite.py +++ b/testsuite.py @@ -242,22 +242,22 @@ def run_selftests(): True, True, True) - print("Testing string literal (constant symbol) lexing") + print("Testing string literal lexing") # Dummy empty configuration just to get a Config object c = kconfiglib.Config("Kconfiglib/tests/empty") def verify_string_lex(s, res): """ - Verifies that the string (constant symbol) token 'res' is produced from + Verifies that a constant symbol with the name 'res' is produced from lexing 's'. Strips the first and last characters from 's' so that readable raw strings can be used as input """ s = s[1:-1] - token = c._tokenize(s, for_eval = True).next() - verify(token == res, - "expected {} to produced the string token {}, produced {}" - .format(s, token, res)) + token = c._tokenize(s, True, None, None).next() + verify(token.name == res, + 'expected {} to produced the constant symbol {}, produced {}' + .format(s, token.name, res)) verify_string_lex(r""" "" """, "") verify_string_lex(r""" '' """, "") @@ -295,7 +295,7 @@ def run_selftests(): """ s = s[1:-1] try: - c._tokenize(s, for_eval = True) + c._tokenize(s, True, None, None) except kconfiglib.KconfigSyntaxError: pass else: @@ -475,15 +475,16 @@ def run_selftests(): verify_eval("'ab' < 'aa'", "n") verify_eval("'ab' > 'aa'", "y") - # If one operand is numeric and the other not a valid number, we get 'n' - verify_eval("INT_37 < oops ", "n") - verify_eval("INT_37 <= oops ", "n") - verify_eval("INT_37 > oops ", "n") - verify_eval("INT_37 >= oops ", "n") - verify_eval("oops < INT_37", "n") - verify_eval("oops <= INT_37", "n") - verify_eval("oops > INT_37", "n") - verify_eval("oops >= INT_37", "n") + # Comparisons where one of the operands doesn't parse as a number also give + # a lexicographic comparison + verify_eval("INT_37 < '37a' ", "y") + verify_eval("'37a' > INT_37", "y") + verify_eval("INT_37 <= '37a' ", "y") + verify_eval("'37a' >= INT_37", "y") + verify_eval("INT_37 >= '37a' ", "n") + verify_eval("INT_37 > '37a' ", "n") + verify_eval("'37a' < INT_37", "n") + verify_eval("'37a' <= INT_37", "n") def verify_eval_bad(expr): try: @@ -1230,7 +1231,7 @@ g os.environ["ENV_VAR"] = "foo" # Contains reference to undefined environment variable, so disable warnings - c = kconfiglib.Config("Kconfiglib/tests/Kmisc", warn = False) + c = kconfiglib.Config("Kconfiglib/tests/Kmisc", warn=False) print("Testing is_optional...") @@ -1371,12 +1372,12 @@ g verify_value("STRING", "") # Assign BOOL - c.load_config("Kconfiglib/tests/config_set_bool", replace = False) + c.load_config("Kconfiglib/tests/config_set_bool", replace=False) verify_value("BOOL", "y") verify_value("STRING", "") # Assign STRING - c.load_config("Kconfiglib/tests/config_set_string", replace = False) + c.load_config("Kconfiglib/tests/config_set_string", replace=False) verify_value("BOOL", "y") verify_value("STRING", "foo bar") @@ -1399,8 +1400,8 @@ g print("Testing .config...") - c1 = kconfiglib.Config("Kconfiglib/tests/Kmisc", warn = False) - c2 = kconfiglib.Config("Kconfiglib/tests/Kmisc", warn = False) + c1 = kconfiglib.Config("Kconfiglib/tests/Kmisc", warn=False) + c2 = kconfiglib.Config("Kconfiglib/tests/Kmisc", warn=False) c1_undef, c1_bool, c1_choice, c1_menu, c1_comment = c1.syms["BOOL"], \ c1.syms["NOT_DEFINED_1"], get_choices(c1)[0], get_menus(c1)[0], \ @@ -1911,14 +1912,28 @@ def test_call_all(conf, arch): s.visibility s.unset_value() + # TODO: verify that constant symbols do not: + # 1) have a non-empty dep + # 2) have nodes + + # TODO: infinite recursion action + #for _, s in conf.const_syms.items(): + # s.__str__() + # s.__repr__() + # s.assignable + # s.type + # s.value + # s.visibility + # s.unset_value() + # Cheat with internals for c in conf._choices: c.__str__() c.__repr__() c.value - c.assignable + c.assignable c.selection - c.default_selection + c.default_selection c.type c.visibility @@ -1948,20 +1963,24 @@ def test_defconfig(conf, arch): def add_configs_for_arch(arch_): arch_dir = os.path.join("arch", arch_) + # Some arches have a "defconfig" in the root of their arch// # directory root_defconfig = os.path.join(arch_dir, "defconfig") if os.path.exists(root_defconfig): defconfigs.append(root_defconfig) + # Assume all files in the arch//configs directory (if it # exists) are configurations defconfigs_dir = os.path.join(arch_dir, "configs") if not os.path.exists(defconfigs_dir): return + if not os.path.isdir(defconfigs_dir): print("Warning: '{}' is not a directory - skipping" .format(defconfigs_dir)) return + for dirpath, _, filenames in os.walk(defconfigs_dir): for filename in filenames: defconfigs.append(os.path.join(dirpath, filename)) -- cgit v1.2.3 From a1e78132c1a34605653195d71c9112604fca3eda Mon Sep 17 00:00:00 2001 From: Ulf Magnusson Date: Fri, 27 Oct 2017 00:29:15 +0200 Subject: Backup --- kconfiglib.py | 695 +++++++++++++++++++++++++--------------------------------- testsuite.py | 17 +- 2 files changed, 313 insertions(+), 399 deletions(-) diff --git a/kconfiglib.py b/kconfiglib.py index 0d92b48..2dd9b46 100644 --- a/kconfiglib.py +++ b/kconfiglib.py @@ -113,6 +113,8 @@ A || (B && C && D) (OR, A, (AND, B, (AND, C, D))) y config.y "y" config.y +TODO: show example for other constant symbols + As seen in the final two examples, n/m/y are always represented as the constant symbols config.n/m/y, regardless of whether they're written with or without quotes. 'config' represents the Config instance of the configuration the @@ -122,17 +124,13 @@ A missing expression (e.g. if the 'if ' part is removed from 'default A if ') is represented as config.y. The standard __str__() functions avoid printing 'if y' conditions to give cleaner output. -Implementation note -------------------- - -TODO: blah blah constant symbols - Send bug reports, suggestions, and questions to ulfalizer a.t Google's email service (or open a ticket on the GitHub page). """ # TODO: document n/m/y +# TODO: consistent docstring format import errno import os @@ -144,7 +142,6 @@ import sys # # Public classes # Public functions -# Internal classes # Internal functions # Public global constants # Internal global constants @@ -253,8 +250,8 @@ class Config(object): "_choices", "_print_undef_assign", "_print_warnings", - "_set_re", - "_unset_re", + "_set_re_match", + "_unset_re_match", "config_prefix", "const_syms", "defconfig_list", @@ -267,6 +264,18 @@ class Config(object): "syms", "top_node", "y", + + # These are used during parsing + "_line", + "_lines", + "_filename", + "_linenr", + "_file_len", + "_filestack", + "_tokens", + "_tokens_i", + "_tokens_len", + "_has_tokens", ) # @@ -305,11 +314,13 @@ class Config(object): if self.config_prefix is None: self.config_prefix = "CONFIG_" - # Regular expressions for parsing .config files - self._set_re = re.compile(r"{}(\w+)=(.*)" - .format(self.config_prefix)) - self._unset_re = re.compile(r"# {}(\w+) is not set" - .format(self.config_prefix)) + # Regular expressions for parsing .config files, with the get() method + # assigned directly as a small optimization (microscopic in this case, + # but it's consistent with the other regexes) + self._set_re_match = re.compile(r"{}(\w+)=(.*)" + .format(self.config_prefix)).match + self._unset_re_match = re.compile(r"# {}(\w+) is not set" + .format(self.config_prefix)).match self._print_warnings = warn self._print_undef_assign = False @@ -366,11 +377,16 @@ class Config(object): self.top_node.linenr = 1 # Parse the Kconfig files - self._parse_block(_FileFeed(self._open(filename), filename), - None, # end_token + + self._has_tokens = False + self._tokens_i = 0 + + self._filestack = [] + + self._enter_file_empty_stack(filename) + self._parse_block(None, # end_token self.top_node, # parent self.y, # visible_if_deps - None, # prev_line self.top_node) # prev_node self.top_node.list = self.top_node.next @@ -432,8 +448,8 @@ class Config(object): self._invalidate_all() # Small optimizations - set_re_match = self._set_re.match - unset_re_match = self._unset_re.match + set_re_match = self._set_re_match + unset_re_match = self._unset_re_match syms = self.syms for linenr, line in enumerate(f, 1): @@ -452,8 +468,8 @@ class Config(object): if sym._type == STRING and val.startswith('"'): if len(val) < 2 or val[-1] != '"': - self._warn("malformed string literal", filename, - linenr) + self._warn("malformed string literal", + filename, linenr) continue # Strip quotes and remove escapings. The unescaping # procedure should be safe since " can only appear as @@ -529,17 +545,11 @@ class Config(object): conditional ('if ...') expressions in the configuration (as well as in the C tools). m is rewritten to 'm && MODULES'. """ - return eval_expr( - self._parse_expr( - self._tokenize( - s, - True, # for_eval_string - None, # filename - None), # linenr - s, - None, # filename - None, # linenr - True)) # transform_m + # TODO: explain + self._line = s + self._filename = None + self._tokenize(True) + return eval_expr(self._parse_expr(True)) def unset_values(self): """ @@ -645,10 +655,10 @@ class Config(object): # Kconfig parsing # - def _tokenize(self, s, for_eval_string, filename, linenr): + def _tokenize(self, for_eval_string): """ - Returns a _Feed instance containing tokens derived from the string 's'. - Registers any new symbols encountered (via _lookup_sym()). + Parses Config._line, putting the tokens in Config._tokens. Registers + any new symbols encountered (via _lookup_sym()). Tries to be reasonably speedy by processing chunks of text via regexes and string operations where possible. This is a hotspot during parsing. @@ -659,13 +669,14 @@ class Config(object): register new symbols. """ + s = self._line + # Tricky implementation detail: While parsing a token, 'token' refers # to the previous token. See _NOT_REF for why this is needed. if for_eval_string: + self._tokens = [] token = None - tokens = [] - # The current index in the string being tokenized i = 0 @@ -673,19 +684,24 @@ class Config(object): # See comment at _initial_token_re_match definition initial_token_match = _initial_token_re_match(s) if not initial_token_match: - return None + self._tokens_len = 0 + return keyword = _get_keyword(initial_token_match.group(1)) + if keyword == _T_HELP: # Avoid junk after "help", e.g. "---", being registered as a # symbol - return _Feed((_T_HELP,)) + self._tokens = (_T_HELP,) + self._tokens_len = 1 # TODO: why is this needed? + self._tokens_i = 0 + return + if keyword is None: - # We expect a keyword as the first token - _tokenization_error(s, filename, linenr) + self._parse_error("expected keyword as first token") token = keyword - tokens = [keyword] + self._tokens = [keyword] # The current index in the string being tokenized i = initial_token_match.end() @@ -755,7 +771,8 @@ class Config(object): # can just find the matching quote. end = s.find(c, i) if end == -1: - _tokenization_error(s, filename, linenr) + self._parse_error("unterminated string") + val = s[i:end] i = end + 1 else: @@ -763,20 +780,25 @@ class Config(object): # very unusual case anyway. quote = c val = "" + while 1: if i >= len(s): - _tokenization_error(s, filename, linenr) + self._parse_error("unterminated string") + c = s[i] if c == quote: break + if c == "\\": if i + 1 >= len(s): - _tokenization_error(s, filename, linenr) + self._parse_error("unterminated string") + val += s[i + 1] i += 2 else: val += c i += 1 + i += 1 # This is the only place where we don't survive with a @@ -787,18 +809,22 @@ class Config(object): val \ if token in _STRING_LEX or \ (not for_eval_string and \ - tokens[0] == _T_OPTION) else \ + self._tokens[0] == _T_OPTION) else \ self._lookup_const_sym(val, for_eval_string) elif c == "&": # Invalid characters are ignored - if i >= len(s) or s[i] != "&": continue + if i >= len(s) or s[i] != "&": + continue + token = _T_AND i += 1 elif c == "|": # Invalid characters are ignored - if i >= len(s) or s[i] != "|": continue + if i >= len(s) or s[i] != "|": + continue + token = _T_OR i += 1 @@ -818,7 +844,8 @@ class Config(object): elif c == ")": token = _T_CLOSE_PAREN - elif c == "#": break # Comment + elif c == "#": + break # Very rare elif c == "<": @@ -844,20 +871,86 @@ class Config(object): while i < len(s) and s[i].isspace(): i += 1 - tokens.append(token) + self._tokens.append(token) + + self._tokens_i = 0 + self._tokens_len = len(self._tokens) - return _Feed(tokens) + def _next_token(self): + if self._tokens_i >= self._tokens_len: + return None + token = self._tokens[self._tokens_i] + self._tokens_i += 1 + return token + + def _peek_token(self): + return None if self._tokens_i >= self._tokens_len \ + else self._tokens[self._tokens_i] + + def _check_token(self, token): + if self._tokens_i < self._tokens_len and \ + self._tokens[self._tokens_i] == token: + self._tokens_i += 1 + return True + return False - def _parse_block(self, line_feeder, end_token, parent, visible_if_deps, - prev_line, prev_node): + def _enter_file_empty_stack(self, filename): + try: + # TODO: comment + f = self._open(filename) + except IOError as e: + # Extend the error message a bit in this case + raise IOError( + "{}:{}: {} Also note that e.g. $FOO in a 'source' " + "statement does not refer to the environment " + "variable FOO, but rather to the Kconfig Symbol FOO " + "(which would commonly have 'option env=\"FOO\"' in " + "its definition)." + .format(self._filename, self._linenr, e.message)) + + self._filename = filename + self._linenr = 0 + with f: + self._lines = f.readlines() + self._file_len = len(self._lines) + + def _enter_file(self, filename): + self._filestack.append((self._filename, self._linenr, self._lines, + self._file_len)) + self._enter_file_empty_stack(filename) + + def _leave_file(self): + self._filename, self._linenr, self._lines, self._file_len = \ + self._filestack.pop() + + def _next_line(self): + if self._linenr >= self._file_len: + return False + + line = self._lines[self._linenr] + self._linenr += 1 + + while line.endswith("\\\n"): + line = line[:-2] + self._lines[self._linenr] + self._linenr += 1 + + self._line = line + return True + + def _next_line_no_join(self): + if self._linenr >= self._file_len: + return None + + self._line = self._lines[self._linenr] + self._linenr += 1 + + return self._line + + def _parse_block(self, end_token, parent, visible_if_deps, prev_node): """ Parses a block, which is the contents of either a file or an if, menu, or choice statement. - line_feeder: - A _FileFeed instance feeding lines from a file. The Kconfig language - is line-based in practice. - end_token: The token that ends the block, e.g. _T_ENDIF ("endif") for ifs. None for files. @@ -890,35 +983,31 @@ class Config(object): """ while 1: - if prev_line is not None: - line, tokens = prev_line - else: - line = line_feeder.next() - if line is None: + if not self._has_tokens: + # Also advances to the next line + if not self._next_line(): if end_token is not None: raise KconfigSyntaxError("Unexpected end of file " + - line_feeder.filename) + self._filename) # We have reached the end of the file. Terminate the final # node and return it. prev_node.next = None return prev_node - tokens = self._tokenize(line, False, line_feeder.filename, - line_feeder.linenr) - if tokens is None: - continue + self._tokenize(False) - t0 = tokens.next() + self._has_tokens = False - # Cases are ordered roughly by frequency, which speeds things up a - # bit + t0 = self._next_token() + if t0 is None: + continue if t0 in (_T_CONFIG, _T_MENUCONFIG): # The tokenizer will automatically allocate a new Symbol object # for any new names it encounters, so we don't need to worry # about that here. - sym = tokens.next() + sym = self._next_token() node = MenuNode() node.config = self @@ -926,12 +1015,11 @@ class Config(object): node.help = None node.list = None node.parent = parent - node.filename = line_feeder.filename - node.linenr = line_feeder.linenr + node.filename = self._filename + node.linenr = self._linenr node.is_menuconfig = (t0 == _T_MENUCONFIG) - prev_line = self._parse_properties(line_feeder, node, - visible_if_deps) + self._parse_properties(node, visible_if_deps) sym.nodes.append(node) self.defined_syms.append(sym) @@ -941,29 +1029,15 @@ class Config(object): prev_node.next = prev_node = node elif t0 == _T_SOURCE: - kconfig_file = tokens.next() + kconfig_file = self._next_token() exp_kconfig_file = self._expand_sym_refs(kconfig_file) - try: - f = self._open(exp_kconfig_file) - except IOError as e: - # Extend the error message a bit in this case - raise IOError( - "{}:{}: {} Also note that e.g. $FOO in a 'source' " - "statement does not refer to the environment " - "variable FOO, but rather to the Kconfig Symbol FOO " - "(which would commonly have 'option env=\"FOO\"' in " - "its definition)." - .format(line_feeder.filename, line_feeder.linenr, - e.message)) - - prev_node = self._parse_block(_FileFeed(f, exp_kconfig_file), - None, # end_token + self._enter_file(exp_kconfig_file) + prev_node = self._parse_block(None, # end_token parent, visible_if_deps, - None, # prev_line prev_node) - prev_line = None + self._leave_file() elif t0 == end_token: # We have reached the end of the block. Terminate the final @@ -976,24 +1050,17 @@ class Config(object): node.item = None node.prompt = None node.parent = parent - node.filename = line_feeder.filename - node.linenr = line_feeder.linenr + node.filename = self._filename + node.linenr = self._linenr node.dep = \ - self._make_and(parent.dep, - self._parse_expr(tokens, line, - line_feeder.filename, - line_feeder.linenr, True)) + self._make_and(parent.dep, self._parse_expr(True)) - self._parse_block(line_feeder, - _T_ENDIF, + self._parse_block(_T_ENDIF, node, # parent visible_if_deps, - None, # prev_line node) # prev_node node.list = node.next - prev_line = None - prev_node.next = prev_node = node elif t0 == _T_MENU: @@ -1002,24 +1069,20 @@ class Config(object): node.item = MENU node.visibility = self.y node.parent = parent - node.filename = line_feeder.filename - node.linenr = line_feeder.linenr + node.filename = self._filename + node.linenr = self._linenr - prev_line = self._parse_properties(line_feeder, node, - visible_if_deps) - node.prompt = (tokens.next(), node.dep) + prompt = self._next_token() + self._parse_properties(node, visible_if_deps) + node.prompt = (prompt, node.dep) - self._parse_block(line_feeder, - _T_ENDMENU, + self._parse_block(_T_ENDMENU, node, # parent self._make_and(visible_if_deps, node.visibility), - prev_line, node) # prev_node node.list = node.next - prev_line = None - prev_node.next = prev_node = node elif t0 == _T_COMMENT: @@ -1028,17 +1091,17 @@ class Config(object): node.item = COMMENT node.list = None node.parent = parent - node.filename = line_feeder.filename - node.linenr = line_feeder.linenr + node.filename = self._filename + node.linenr = self._linenr - prev_line = self._parse_properties(line_feeder, node, - visible_if_deps) - node.prompt = (tokens.next(), node.dep) + prompt = self._next_token() + self._parse_properties(node, visible_if_deps) + node.prompt = (prompt, node.dep) prev_node.next = prev_node = node elif t0 == _T_CHOICE: - name = tokens.next() + name = self._next_token() if name is None: choice = Choice() self._choices.append(choice) @@ -1058,61 +1121,41 @@ class Config(object): node.item = choice node.help = None node.parent = parent - node.filename = line_feeder.filename - node.linenr = line_feeder.linenr + node.filename = self._filename + node.linenr = self._linenr - prev_line = self._parse_properties(line_feeder, node, - visible_if_deps) - self._parse_block(line_feeder, - _T_ENDCHOICE, + self._parse_properties(node, visible_if_deps) + self._parse_block(_T_ENDCHOICE, node, # parent visible_if_deps, - prev_line, node) # prev_node node.list = node.next - prev_line = None - choice.nodes.append(node) prev_node.next = prev_node = node elif t0 == _T_MAINMENU: - self.top_node.prompt = (tokens.next(), self.y) - self.top_node.filename = line_feeder.filename - self.top_node.linenr = line_feeder.linenr + self.top_node.prompt = (self._next_token(), self.y) + self.top_node.filename = self._filename + self.top_node.linenr = self._linenr else: - _parse_error(line, "unrecognized construct", - line_feeder.filename, line_feeder.linenr) + self._parse_error("unrecognized construct") - def _parse_cond(self, tokens, line, filename, linenr): + def _parse_cond(self): """ Parses an optional 'if ' construct and returns the parsed , or self.y if the next token is not _T_IF """ - return self._parse_expr(tokens, line, filename, linenr, True) \ - if tokens.check(_T_IF) else self.y - - def _parse_val_and_cond(self, tokens, line, filename, linenr): - """ - Parses ' if ' constructs, where the 'if' part is - optional. Returns a tuple containing the parsed expressions, with - config.y as the second element if the 'if' part is missing. - """ - return (self._parse_expr(tokens, line, filename, linenr, False), - self._parse_cond(tokens, line, filename, linenr)) + return self._parse_expr(True) if self._check_token(_T_IF) else self.y - def _parse_properties(self, line_feeder, node, visible_if_deps): + def _parse_properties(self, node, visible_if_deps): """ Parses properties for symbols, menus, choices, and comments. Also takes care of propagating dependencies from the menu node to the properties of the item (this mirrors the inner working of the C tools). - line_feeder: - A _FileFeed instance feeding lines from a file. The Kconfig language - is line-based in practice. - node: The menu node we're parsing properties on. Some properties (prompts, help texts, 'depends on') apply to the Menu node, while the others @@ -1139,39 +1182,29 @@ class Config(object): # properties above. node.dep = self.y - # The cached (line, tokens) tuple that we return - last_line = None - while 1: - line = line_feeder.next() - if line is None: + # Also advances to the next line + if not self._next_line(): break - filename = line_feeder.filename - linenr = line_feeder.linenr + self._tokenize(False) - tokens = self._tokenize(line, False, filename, linenr) - if tokens is None: + t0 = self._next_token() + if t0 is None: continue - t0 = tokens.next() - if t0 == _T_DEPENDS: - if not tokens.check(_T_ON): - _parse_error(line, 'expected "on" after "depends"', - filename, linenr) + if not self._check_token(_T_ON): + self._parse_error('expected "on" after "depends"') - node.dep = \ - self._make_and(node.dep, - self._parse_expr(tokens, line, filename, - linenr, True)) + node.dep = self._make_and(node.dep, self._parse_expr(True)) elif t0 == _T_HELP: # Find first non-blank (not all-space) line and get its # indentation while 1: - line = line_feeder.next_no_join() + line = self._next_line_no_join() if line is None or not line.isspace(): break @@ -1184,7 +1217,7 @@ class Config(object): # If the first non-empty lines has zero indent, there is no # help text node.help = "" - line_feeder.linenr -= 1 + self._linenr -= 1 # Unget the line break # The help text goes on till the first non-empty line with less @@ -1192,7 +1225,7 @@ class Config(object): help_lines = [_deindent(line, indent).rstrip()] while 1: - line = line_feeder.next_no_join() + line = self._next_line_no_join() if line is None or \ (not line.isspace() and _indentation(line) < indent): node.help = "\n".join(help_lines).rstrip() + "\n" @@ -1202,58 +1235,46 @@ class Config(object): if line is None: break - line_feeder.linenr -= 1 + self._linenr -= 1 # Unget the line elif t0 == _T_SELECT: if not isinstance(node.item, Symbol): - _parse_error(line, "only symbols can select", filename, - linenr) + self._parse_error("only symbols can select") - selects.append( - (tokens.next(), - self._parse_cond(tokens, line, filename, linenr))) + selects.append((self._next_token(), self._parse_cond())) elif t0 == _T_IMPLY: if not isinstance(node.item, Symbol): - _parse_error(line, "only symbols can imply", filename, - linenr) + self._parse_error("only symbols can imply") - implies.append( - (tokens.next(), - self._parse_cond(tokens, line, filename, linenr))) + implies.append((self._next_token(), self._parse_cond())) elif t0 in (_T_BOOL, _T_TRISTATE, _T_INT, _T_HEX, _T_STRING): node.item._type = _TOKEN_TO_TYPE[t0] - if tokens.peek() is not None: - prompt = (tokens.next(), - self._parse_cond(tokens, line, filename, linenr)) + if self._peek_token() is not None: + prompt = (self._next_token(), self._parse_cond()) elif t0 == _T_DEFAULT: - defaults.append( - self._parse_val_and_cond(tokens, line, filename, linenr)) + defaults.append((self._parse_expr(False), self._parse_cond())) elif t0 in (_T_DEF_BOOL, _T_DEF_TRISTATE): node.item._type = _TOKEN_TO_TYPE[t0] - defaults.append( - self._parse_val_and_cond(tokens, line, filename, - linenr)) + defaults.append((self._parse_expr(False), self._parse_cond())) elif t0 == _T_PROMPT: # 'prompt' properties override each other within a single # definition of a symbol, but additional prompts can be added # by defining the symbol multiple times - prompt = (tokens.next(), - self._parse_cond(tokens, line, filename, linenr)) + prompt = (self._next_token(), self._parse_cond()) elif t0 == _T_RANGE: - ranges.append( - (tokens.next(), - tokens.next(), - self._parse_cond(tokens, line, filename, linenr))) + ranges.append((self._next_token(), + self._next_token(), + self._parse_cond())) elif t0 == _T_OPTION: - if tokens.check(_T_ENV) and tokens.check(_T_EQUAL): - env_var = tokens.next() + if self._check_token(_T_ENV) and self._check_token(_T_EQUAL): + env_var = self._next_token() node.item.env_var = env_var @@ -1268,24 +1289,24 @@ class Config(object): "message, that might be an error, and you " "should email ulfalizer a.t Google's email " "service.".format(node.item.name, env_var), - filename, linenr) + self._filename, self._linenr) else: defaults.append( (self._lookup_const_sym(os.environ[env_var], False), self.y)) - elif tokens.check(_T_DEFCONFIG_LIST): + elif self._check_token(_T_DEFCONFIG_LIST): if self.defconfig_list is None: self.defconfig_list = node.item else: self._warn("'option defconfig_list' set on multiple " "symbols ({0} and {1}). Only {0} will be " - "used." - .format(self.defconfig_list.name, - node.item.name)) + "used.".format(self.defconfig_list.name, + node.item.name), + self._filename, self._linenr) - elif tokens.check(_T_MODULES): + elif self._check_token(_T_MODULES): # To reduce warning spam, only warn if 'option modules' is # set on some symbol that isn't MODULES, which should be # safe. I haven't run into any projects that make use @@ -1300,42 +1321,34 @@ class Config(object): "MODULES, like older versions of the C " "implementation did when 'option modules' " "wasn't used.)", - filename, linenr) + self._filename, self._linenr) - elif tokens.check(_T_ALLNOCONFIG_Y): + elif self._check_token(_T_ALLNOCONFIG_Y): if not isinstance(node.item, Symbol): - _parse_error(line, - "the 'allnoconfig_y' option is only " - "valid for symbols", - filename, linenr) + self._parse_error("the 'allnoconfig_y' option is only " + "valid for symbols") node.item.is_allnoconfig_y = True else: - _parse_error(line, "unrecognized option", filename, linenr) + self._parse_error("unrecognized option") elif t0 == _T_VISIBLE: - if not tokens.check(_T_IF): - _parse_error(line, 'expected "if" after "visible"', - filename, linenr) + if not self._check_token(_T_IF): + self._parse_error('expected "if" after "visible"') node.visibility = \ - self._make_and(node.visibility, - self._parse_expr(tokens, line, filename, - linenr, True)) + self._make_and(node.visibility, self._parse_expr(True)) elif t0 == _T_OPTIONAL: if not isinstance(node.item, Choice): - _parse_error(line, - '"optional" is only valid for choices', - filename, - linenr) + self._parse_error('"optional" is only valid for choices') node.item.is_optional = True else: - tokens.i = 0 - last_line = (line, tokens) + self._tokens_i = 0 + self._has_tokens = True break # Done parsing properties. Now add the new @@ -1395,13 +1408,10 @@ class Config(object): self._make_and(cond_expr, node.dep))) - # Return cached non-property line - return last_line - - def _parse_expr(self, feed, line, filename, linenr, transform_m): + def _parse_expr(self, transform_m): """ - Parses an expression from the tokens in 'feed' using a simple top-down - approach. The result has the form + Parses an expression from the tokens in Config._tokens using a simple + top-down approach. The result has the form '( )' where is e.g. kconfiglib.AND. If there is only one operand (i.e., no && or ||), then the operand is returned directly. This also goes for subexpressions. @@ -1410,20 +1420,6 @@ class Config(object): structure (AND, A, (AND, B, (OR, (NOT, C), (EQUAL, D, 3)))), with the Symbol objects stored directly in the expression. - feed: - _Feed instance containing the tokens for the expression. - - line: - The line containing the expression being parsed. - - filename: - The file containing the expression. None when using - Config.eval_string(). - - linenr: - The line number containing the expression. None when using - Config.eval_string(). - transform_m: True if 'm' should be rewritten to 'm && MODULES'. See the Config.eval_string() documentation. @@ -1453,35 +1449,32 @@ class Config(object): # we end up allocating a ton of lists instead of reusing expressions, # which is bad. - and_expr = self._parse_and_expr(feed, line, filename, linenr, - transform_m) + and_expr = self._parse_and_expr(transform_m) # Return 'and_expr' directly if we have a "single-operand" OR. # Otherwise, parse the expression on the right and make an OR node. # This turns A || B || C || D into (OR, A, (OR, B, (OR, C, D))). return and_expr \ - if not feed.check(_T_OR) else \ - (OR, and_expr, self._parse_expr(feed, line, filename, linenr, - transform_m)) + if not self._check_token(_T_OR) else \ + (OR, and_expr, self._parse_expr(transform_m)) - def _parse_and_expr(self, feed, line, filename, linenr, transform_m): - factor = self._parse_factor(feed, line, filename, linenr, transform_m) + def _parse_and_expr(self, transform_m): + factor = self._parse_factor(transform_m) # Return 'factor' directly if we have a "single-operand" AND. # Otherwise, parse the right operand and make an AND node. This turns # A && B && C && D into (AND, A, (AND, B, (AND, C, D))). return factor \ - if not feed.check(_T_AND) else \ - (AND, factor, self._parse_and_expr(feed, line, filename, - linenr, transform_m)) + if not self._check_token(_T_AND) else \ + (AND, factor, self._parse_and_expr(transform_m)) - def _parse_factor(self, feed, line, filename, linenr, transform_m): - token = feed.next() + def _parse_factor(self, transform_m): + token = self._next_token() if isinstance(token, Symbol): # Plain symbol or relation - next_token = feed.peek() + next_token = self._peek_token() if next_token not in _TOKEN_TO_REL: # Plain symbol @@ -1494,20 +1487,20 @@ class Config(object): return token # Relation - return (_TOKEN_TO_REL[feed.next()], token, feed.next()) + return (_TOKEN_TO_REL[self._next_token()], token, + self._next_token()) if token == _T_NOT: - return (NOT, self._parse_factor(feed, line, filename, linenr, - transform_m)) + return (NOT, self._parse_factor(transform_m)) if token == _T_OPEN_PAREN: - expr_parse = self._parse_expr(feed, line, filename, - linenr, transform_m) - if not feed.check(_T_CLOSE_PAREN): - _parse_error(line, "missing end parenthesis", filename, linenr) + expr_parse = self._parse_expr(transform_m) + if not self._check_token(_T_CLOSE_PAREN): + self._parse_error("missing end parenthesis") + return expr_parse - _parse_error(line, "malformed expression", filename, linenr) + self._parse_error("malformed expression") # # Symbol lookup @@ -1707,29 +1700,9 @@ class Config(object): s[sym_ref_match.end():] # - # Warnings + # Expression construction # - def _warn(self, msg, filename=None, linenr=None): - """For printing general warnings.""" - if self._print_warnings: - _stderr_msg("warning: " + msg, filename, linenr) - - def _warn_undef_assign(self, msg, filename=None, linenr=None): - """ - See the class documentation. - """ - if self._print_undef_assign: - _stderr_msg("warning: " + msg, filename, linenr) - - def _warn_undef_assign_load(self, name, val, filename, linenr): - """ - Special version for load_config(). - """ - self._warn_undef_assign( - 'attempt to assign the value "{}" to the undefined symbol {}' \ - .format(val, name), filename, linenr) - def _make_and(self, e1, e2): """ Constructs an AND (&&) expression. Performs trivial simplification. @@ -1760,6 +1733,39 @@ class Config(object): return (OR, e1, e2) + # + # Errors and warnings + # + + def _parse_error(self, msg): + if self._filename is None: + loc = "" + else: + loc = "{}:{}: ".format(self._filename, self._linenr) + + raise KconfigSyntaxError( + "{}Couldn't parse '{}': {}".format(loc, self._line.rstrip(), msg)) + + def _warn(self, msg, filename=None, linenr=None): + """For printing general warnings.""" + if self._print_warnings: + _stderr_msg("warning: " + msg, filename, linenr) + + def _warn_undef_assign(self, msg, filename=None, linenr=None): + """ + See the class documentation. + """ + if self._print_undef_assign: + _stderr_msg("warning: " + msg, filename, linenr) + + def _warn_undef_assign_load(self, name, val, filename, linenr): + """ + Special version for load_config(). + """ + self._warn_undef_assign( + 'attempt to assign the value "{}" to the undefined symbol {}' \ + .format(val, name), filename, linenr) + class Symbol(object): """ Represents a configuration symbol: @@ -2735,7 +2741,8 @@ class Choice(object): "choice" if self.name is None else "choice " + self.name, _TYPENAME[self.type], "mode " + self.value, - "visibility " + self.visibility] + "visibility " + self.visibility + ] if self.is_optional: fields.append("optional") @@ -3088,87 +3095,6 @@ def expr_str(expr): _RELATION_TO_STR[expr[0]], expr_str(expr[2])) -# -# Internal classes -# - -class _Feed(object): - """ - Class for working with sequences in a stream-like fashion; handy for - tokens. - """ - - __slots__ = ( - 'i', - 'length', - 'items', - ) - - def __init__(self, items): - self.items = items - self.length = len(self.items) - self.i = 0 - - def next(self): - if self.i >= self.length: - return None - item = self.items[self.i] - self.i += 1 - return item - - def peek(self): - return None if self.i >= self.length else self.items[self.i] - - def check(self, token): - """ - Checks if the next token is 'token'. If so, removes it from the token - feed and return True. Otherwise, leaves it in and return False. - """ - if self.i < self.length and self.items[self.i] == token: - self.i += 1 - return True - return False - -class _FileFeed(object): - """ - Feeds lines from a file. Keeps track of the filename and current line - number. Joins any line ending in \\ with the following line. We need to be - careful to get the line number right in the presence of continuation lines. - """ - - __slots__ = ( - 'filename', - 'lines', - 'length', - 'linenr' - ) - - def __init__(self, file_, filename): - self.filename = filename - with file_: - # No interleaving of I/O and processing yet. Don't know if it would - # help. - self.lines = file_.readlines() - self.length = len(self.lines) - self.linenr = 0 - - def next(self): - if self.linenr >= self.length: - return None - line = self.lines[self.linenr] - self.linenr += 1 - while line.endswith("\\\n"): - line = line[:-2] + self.lines[self.linenr] - self.linenr += 1 - return line - - def next_no_join(self): - if self.linenr >= self.length: - return None - line = self.lines[self.linenr] - self.linenr += 1 - return line - # # Internal functions # @@ -3286,28 +3212,13 @@ def _strcmp(s1, s2): """ return (s1 > s2) - (s1 < s2) -def _lines(*args): - """ - Returns a string consisting of all arguments, with newlines inserted - between them. - """ - return "\n".join(args) - def _stderr_msg(msg, filename, linenr): - if filename is not None: - sys.stderr.write("{}:{}: ".format(filename, linenr)) - sys.stderr.write(msg + "\n") - -def _tokenization_error(s, filename, linenr): - loc = "" if filename is None else "{}:{}: ".format(filename, linenr) - raise KconfigSyntaxError("{}Couldn't tokenize '{}'" - .format(loc, s.strip())) + if filename is None: + s = msg + else: + s = "{}:{}: ".format(filename, linenr) -def _parse_error(s, msg, filename, linenr): - loc = "" if filename is None else "{}:{}: ".format(filename, linenr) - raise KconfigSyntaxError("{}Couldn't parse '{}'{}" - .format(loc, s.strip(), - "." if msg is None else ": " + msg)) + sys.stderr.write(msg + "\n") def _internal_error(msg): raise InternalError( @@ -3419,7 +3330,7 @@ def _sym_choice_str(sc): def _expr_depends_on(expr, sym): """ Reimplementation of expr_depends_symbol() from mconf.c. Used to - determine if a submenu should be implicitly created, which influences + determine if a submenu should be implicitly created. This also influences what items inside choice statements are considered choice items. """ if isinstance(expr, Symbol): @@ -3504,7 +3415,7 @@ def _flatten(node): node = node.next -def _remove_if(node): +def _remove_ifs(node): """ Removes 'if' nodes (which can be recognized by MenuNode.item being None), which are assumed to already have been flattened. The C implementation @@ -3580,7 +3491,7 @@ def _finalize_tree(node): # We have a node with finalized children. Do final steps to finalize # this node. _flatten(node.list) - _remove_if(node) + _remove_ifs(node) # Empty choices (node.list None) are possible, so this needs to go outside if isinstance(node.item, Choice): @@ -3671,8 +3582,8 @@ def _finalize_tree(node): _T_VISIBLE, ) = range(44) -# Keyword to token map. Note that the get() method is assigned directly as a -# small optimization. +# Keyword to token map, with the get() method assigned directly as a small +# optimization _get_keyword = { "allnoconfig_y": _T_ALLNOCONFIG_Y, "bool": _T_BOOL, diff --git a/testsuite.py b/testsuite.py index 7859369..e81ba1d 100644 --- a/testsuite.py +++ b/testsuite.py @@ -253,11 +253,11 @@ def run_selftests(): lexing 's'. Strips the first and last characters from 's' so that readable raw strings can be used as input """ - s = s[1:-1] - token = c._tokenize(s, True, None, None).next() - verify(token.name == res, + c._line = s[1:-1] + c._tokenize(True) + verify(c._tokens[0].name == res, 'expected {} to produced the constant symbol {}, produced {}' - .format(s, token.name, res)) + .format(s, c._tokens[0].name, res)) verify_string_lex(r""" "" """, "") verify_string_lex(r""" '' """, "") @@ -293,9 +293,9 @@ def run_selftests(): first and last characters from 's' so we can use readable raw strings as input. """ - s = s[1:-1] + c._line = s[1:-1] try: - c._tokenize(s, True, None, None) + c._tokenize(True) except kconfiglib.KconfigSyntaxError: pass else: @@ -812,7 +812,8 @@ g verify_locations(c.syms["MULTI_DEF"].nodes, "tests/Klocation:6", "tests/Klocation:16", - "tests/Klocation_included:3") + "tests/Klocation_included:3", + "tests/Klocation:32") verify_locations(c.named_choices["CHOICE"].nodes, "tests/Klocation_included:5") @@ -1916,6 +1917,8 @@ def test_call_all(conf, arch): # 1) have a non-empty dep # 2) have nodes + # TODO: Look for weird stuff in the dictionaries + # TODO: infinite recursion action #for _, s in conf.const_syms.items(): # s.__str__() -- cgit v1.2.3 From 9b1cc0dbad8e19930d5e1c6365745e14c7fa6076 Mon Sep 17 00:00:00 2001 From: Ulf Magnusson Date: Fri, 27 Oct 2017 15:34:12 +0200 Subject: Backup --- kconfiglib.py | 217 ++++++++++++++++++++++++++-------------------------------- 1 file changed, 98 insertions(+), 119 deletions(-) diff --git a/kconfiglib.py b/kconfiglib.py index 2dd9b46..d0c46c6 100644 --- a/kconfiglib.py +++ b/kconfiglib.py @@ -265,16 +265,16 @@ class Config(object): "top_node", "y", - # These are used during parsing - "_line", - "_lines", + # Parsing-related + "_parsing_kconfigs", + "_reuse_line", + "_file", "_filename", "_linenr", - "_file_len", "_filestack", + "_line", "_tokens", "_tokens_i", - "_tokens_len", "_has_tokens", ) @@ -351,14 +351,16 @@ class Config(object): sym = self.const_syms[nmy] sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n - self.modules = self._lookup_sym("MODULES", False) + self._parsing_kconfigs = True + + self.modules = self._lookup_sym("MODULES") self.defconfig_list = None # Predefined symbol. DEFCONFIG_LIST uses this. - uname_sym = self._lookup_const_sym("UNAME_RELEASE", False) + uname_sym = self._lookup_const_sym("UNAME_RELEASE") uname_sym._type = STRING uname_sym.defaults.append( - (self._lookup_const_sym(platform.uname()[2], False), + (self._lookup_const_sym(platform.uname()[2]), self.y)) # env_var doubles as the SYMBOL_AUTO flag from the C implementation, so # just set it to something. The naming breaks a bit here, but it's @@ -378,8 +380,8 @@ class Config(object): # Parse the Kconfig files + self._reuse_line = False self._has_tokens = False - self._tokens_i = 0 self._filestack = [] @@ -388,10 +390,11 @@ class Config(object): self.top_node, # parent self.y, # visible_if_deps self.top_node) # prev_node - self.top_node.list = self.top_node.next self.top_node.next = None + self._parsing_kconfigs = False + # Do various post-processing of the menu tree, e.g. to finalize # choices, flatten ifs, and implicitly create menus _finalize_tree(self.top_node) @@ -546,9 +549,13 @@ class Config(object): the C tools). m is rewritten to 'm && MODULES'. """ # TODO: explain - self._line = s self._filename = None - self._tokenize(True) + + self._line = "if " + s + self._tokenize() + self._line = s + del self._tokens[0] + return eval_expr(self._parse_expr(True)) def unset_values(self): @@ -655,55 +662,43 @@ class Config(object): # Kconfig parsing # - def _tokenize(self, for_eval_string): + def _tokenize(self): """ Parses Config._line, putting the tokens in Config._tokens. Registers any new symbols encountered (via _lookup_sym()). Tries to be reasonably speedy by processing chunks of text via regexes and string operations where possible. This is a hotspot during parsing. - - for_eval_string: - True when parsing an expression for a call to Config.eval_string(), - in which case we should not treat the first token specially nor - register new symbols. """ s = self._line # Tricky implementation detail: While parsing a token, 'token' refers - # to the previous token. See _NOT_REF for why this is needed. + # to the previous token. See _STRING_LEX for why this is needed. - if for_eval_string: - self._tokens = [] - token = None - # The current index in the string being tokenized - i = 0 + # See comment at _initial_token_re_match definition + initial_token_match = _initial_token_re_match(s) + if not initial_token_match: + self._tokens = (None,) + self._tokens_i = -1 + return - else: - # See comment at _initial_token_re_match definition - initial_token_match = _initial_token_re_match(s) - if not initial_token_match: - self._tokens_len = 0 - return - - keyword = _get_keyword(initial_token_match.group(1)) - - if keyword == _T_HELP: - # Avoid junk after "help", e.g. "---", being registered as a - # symbol - self._tokens = (_T_HELP,) - self._tokens_len = 1 # TODO: why is this needed? - self._tokens_i = 0 - return - - if keyword is None: - self._parse_error("expected keyword as first token") - - token = keyword - self._tokens = [keyword] - # The current index in the string being tokenized - i = initial_token_match.end() + keyword = _get_keyword(initial_token_match.group(1)) + + if keyword == _T_HELP: + # Avoid junk after "help", e.g. "---", being registered as a + # symbol + self._tokens = (_T_HELP, None) + self._tokens_i = -1 + return + + if keyword is None: + self._parse_error("expected keyword as first token") + + token = keyword + self._tokens = [keyword] + # The current index in the string being tokenized + i = initial_token_match.end() # Main tokenization loop (for tokens past the first one) while i < len(s): @@ -732,9 +727,9 @@ class Config(object): # ...except we translate n, m, and y into the # corresponding constant symbols, like the C # implementation - token = self._lookup_const_sym(name, for_eval_string) + token = self._lookup_const_sym(name) else: - token = self._lookup_sym(name, for_eval_string) + token = self._lookup_sym(name) else: # It's a case of missing quotes. For example, the @@ -802,15 +797,12 @@ class Config(object): i += 1 # This is the only place where we don't survive with a - # single token of lookback, hence the kludge: - # 'option env="FOO"' does not refer to a constant symbol - # named "FOO". - token = \ - val \ - if token in _STRING_LEX or \ - (not for_eval_string and \ - self._tokens[0] == _T_OPTION) else \ - self._lookup_const_sym(val, for_eval_string) + # single token of lookback: 'option env="FOO"' does not + # refer to a constant symbol named "FOO". + token = val \ + if token in _STRING_LEX or \ + self._tokens[0] == _T_OPTION else \ + self._lookup_const_sym(val) elif c == "&": # Invalid characters are ignored @@ -873,23 +865,20 @@ class Config(object): self._tokens.append(token) - self._tokens_i = 0 - self._tokens_len = len(self._tokens) + # TODO: say something about the None-termination + + self._tokens.append(None) + self._tokens_i = -1 def _next_token(self): - if self._tokens_i >= self._tokens_len: - return None - token = self._tokens[self._tokens_i] self._tokens_i += 1 - return token + return self._tokens[self._tokens_i] def _peek_token(self): - return None if self._tokens_i >= self._tokens_len \ - else self._tokens[self._tokens_i] + return self._tokens[self._tokens_i + 1] def _check_token(self, token): - if self._tokens_i < self._tokens_len and \ - self._tokens[self._tokens_i] == token: + if self._tokens[self._tokens_i + 1] == token: self._tokens_i += 1 return True return False @@ -897,7 +886,7 @@ class Config(object): def _enter_file_empty_stack(self, filename): try: # TODO: comment - f = self._open(filename) + self._file = self._open(filename) except IOError as e: # Extend the error message a bit in this case raise IOError( @@ -910,40 +899,33 @@ class Config(object): self._filename = filename self._linenr = 0 - with f: - self._lines = f.readlines() - self._file_len = len(self._lines) def _enter_file(self, filename): - self._filestack.append((self._filename, self._linenr, self._lines, - self._file_len)) + self._filestack.append((self._file, self._filename, self._linenr)) self._enter_file_empty_stack(filename) def _leave_file(self): - self._filename, self._linenr, self._lines, self._file_len = \ - self._filestack.pop() + self._file.close() + self._file, self._filename, self._linenr = self._filestack.pop() def _next_line(self): - if self._linenr >= self._file_len: - return False + # This provides a single line of "unget" if _reuse_line is set to True + if not self._reuse_line: + self._line = self._file.readline() + self._linenr += 1 - line = self._lines[self._linenr] - self._linenr += 1 + self._reuse_line = False - while line.endswith("\\\n"): - line = line[:-2] + self._lines[self._linenr] + while self._line.endswith("\\\n"): + # TODO: Can hang if the file ends with a backslash + self._line = self._line[:-2] + self._file.readline() self._linenr += 1 - self._line = line - return True + return self._line def _next_line_no_join(self): - if self._linenr >= self._file_len: - return None - - self._line = self._lines[self._linenr] + self._line = self._file.readline() self._linenr += 1 - return self._line def _parse_block(self, end_token, parent, visible_if_deps, prev_node): @@ -964,10 +946,6 @@ class Config(object): 'visible if' dependencies from enclosing menus. Propagated to Symbol and Choice prompts. - prev_line: - A "cached" (line, tokens) tuple from having parsed a line earlier - that we realized belonged to a different construct. - prev_node: The previous menu node. New nodes will be added after this one (by modifying its 'next' pointer). @@ -995,7 +973,7 @@ class Config(object): prev_node.next = None return prev_node - self._tokenize(False) + self._tokenize() self._has_tokens = False @@ -1187,7 +1165,7 @@ class Config(object): if not self._next_line(): break - self._tokenize(False) + self._tokenize() t0 = self._next_token() if t0 is None: @@ -1205,10 +1183,10 @@ class Config(object): while 1: line = self._next_line_no_join() - if line is None or not line.isspace(): + if not line or not line.isspace(): break - if line is None: + if not line: node.help = "" break @@ -1217,7 +1195,7 @@ class Config(object): # If the first non-empty lines has zero indent, there is no # help text node.help = "" - self._linenr -= 1 # Unget the line + self._reuse_line = True # "Unget" the line break # The help text goes on till the first non-empty line with less @@ -1226,16 +1204,16 @@ class Config(object): help_lines = [_deindent(line, indent).rstrip()] while 1: line = self._next_line_no_join() - if line is None or \ + if not line or \ (not line.isspace() and _indentation(line) < indent): node.help = "\n".join(help_lines).rstrip() + "\n" break help_lines.append(_deindent(line, indent).rstrip()) - if line is None: + if not line: break - self._linenr -= 1 # Unget the line + self._reuse_line = True # "Unget" the line elif t0 == _T_SELECT: if not isinstance(node.item, Symbol): @@ -1251,6 +1229,7 @@ class Config(object): elif t0 in (_T_BOOL, _T_TRISTATE, _T_INT, _T_HEX, _T_STRING): node.item._type = _TOKEN_TO_TYPE[t0] + if self._peek_token() is not None: prompt = (self._next_token(), self._parse_cond()) @@ -1259,6 +1238,7 @@ class Config(object): elif t0 in (_T_DEF_BOOL, _T_DEF_TRISTATE): node.item._type = _TOKEN_TO_TYPE[t0] + defaults.append((self._parse_expr(False), self._parse_cond())) elif t0 == _T_PROMPT: @@ -1292,8 +1272,7 @@ class Config(object): self._filename, self._linenr) else: defaults.append( - (self._lookup_const_sym(os.environ[env_var], - False), + (self._lookup_const_sym(os.environ[env_var]), self.y)) elif self._check_token(_T_DEFCONFIG_LIST): @@ -1347,7 +1326,7 @@ class Config(object): node.item.is_optional = True else: - self._tokens_i = 0 + self._tokens_i = -1 self._has_tokens = True break @@ -1506,7 +1485,7 @@ class Config(object): # Symbol lookup # - def _lookup_sym(self, name, for_eval_string): + def _lookup_sym(self, name): """ Fetches the symbol 'name' from the symbol table, creating and registering it if it does not exist. TODO If 'for_eval_string' is True, @@ -1522,14 +1501,14 @@ class Config(object): sym.is_constant = False sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n - if for_eval_string: - self._warn("no symbol {} in configuration".format(name)) - else: + if self._parsing_kconfigs: self.syms[name] = sym + else: + self._warn("no symbol {} in configuration".format(name)) return sym - def _lookup_const_sym(self, name, for_eval_string): + def _lookup_const_sym(self, name): """ TODO: say something """ @@ -1542,7 +1521,7 @@ class Config(object): sym.is_constant = True sym.rev_dep = sym.weak_rev_dep = sym.direct_dep = self.n - if not for_eval_string: + if self._parsing_kconfigs: self.const_syms[name] = sym return sym @@ -1556,14 +1535,6 @@ class Config(object): Returns a list containing all .config strings for the configuration. """ - config_strings = [] - add_fn = config_strings.append - - node = self.top_node.list - if node is None: - # Empty configuration - return config_strings - # Symbol._already_written is set to True when a symbol config string is # fetched, so that symbols defined in multiple locations only get one # .config entry. We reset it prior to writing out a new .config. It @@ -1577,6 +1548,14 @@ class Config(object): for sym in self.defined_syms: sym._already_written = False + node = self.top_node.list + if node is None: + # Empty configuration + return [] + + config_strings = [] + add_fn = config_strings.append + while 1: if isinstance(node.item, Symbol): sym = node.item -- cgit v1.2.3 From 481bfd60d0b283f30b906d2edf8228aeb82e8492 Mon Sep 17 00:00:00 2001 From: Ulf Magnusson Date: Sat, 28 Oct 2017 00:46:00 +0200 Subject: Nearly finalize API Probably just some usability tweaks left. Having to do STR_TO_TRI[] for comparisons against 'assignable' values is kinda ugly and confusing. --- examples/allnoconfig.py | 4 +- examples/allyesconfig.py | 10 +- kconfiglib.py | 617 +++++++++++++++++++++++------------------------ testsuite.py | 561 +++++++++++++++++++++--------------------- 4 files changed, 582 insertions(+), 610 deletions(-) diff --git a/examples/allnoconfig.py b/examples/allnoconfig.py index 8d41912..2348da4 100644 --- a/examples/allnoconfig.py +++ b/examples/allnoconfig.py @@ -8,7 +8,7 @@ # # $ make [ARCH=] scriptconfig SCRIPT=Kconfiglib/examples/allnoconfig.py -from kconfiglib import Config, Symbol, tri_less +from kconfiglib import Config, Symbol, STR_TO_TRI import sys def do_allnoconfig(node): @@ -27,7 +27,7 @@ def do_allnoconfig(node): if (sym.choice is None and not sym.is_allnoconfig_y and sym.assignable and - tri_less(sym.assignable[0], sym.value)): + STR_TO_TRI[sym.assignable[0]] < sym.tri_value): # Yup, lower it sym.set_value(sym.assignable[0]) diff --git a/examples/allyesconfig.py b/examples/allyesconfig.py index 32b302c..f91b6d7 100644 --- a/examples/allyesconfig.py +++ b/examples/allyesconfig.py @@ -18,7 +18,7 @@ # # $ make [ARCH=] scriptconfig SCRIPT=Kconfiglib/examples/allyesconfig.py -from kconfiglib import Config, Choice, tri_less +from kconfiglib import Config, Choice, STR_TO_TRI import sys conf = Config(sys.argv[1]) @@ -70,7 +70,7 @@ while 1: for sym in non_choice_syms: # See allnoconfig example. [-1] gives the last (highest) assignable # value. - if sym.assignable and tri_less(sym.value, sym.assignable[-1]): + if sym.assignable and sym.tri_value < STR_TO_TRI[sym.assignable[-1]]: sym.set_value(sym.assignable[-1]) no_changes = False @@ -79,7 +79,7 @@ while 1: for choice in choices: # Handle a choice whose visibility allows it to be in "y" mode - if choice.visibility == "y": + if choice.visibility == 2: selection = choice.default_selection # Does the choice have a default selection that we haven't already @@ -95,12 +95,12 @@ while 1: # This might happen if a choice depends on a symbol that can only be # "m", for example. - elif choice.visibility == "m": + elif choice.visibility == 1: for sym in choice.symbols: # Does the choice have a symbol that can be "m" that we haven't # already set to "m"? - if sym.user_value != "m" and "m" in sym.assignable: + if sym.user_tri_value != 1 and "m" in sym.assignable: # Yup, set it sym.set_value("m") diff --git a/kconfiglib.py b/kconfiglib.py index d0c46c6..25bb380 100644 --- a/kconfiglib.py +++ b/kconfiglib.py @@ -338,7 +338,8 @@ class Config(object): sym.name = nmy sym.is_constant = True sym._type = TRISTATE - sym._cached_val = nmy + sym._cached_tri_val = STR_TO_TRI[nmy] + sym._cached_str_val = nmy self.const_syms[nmy] = sym @@ -416,9 +417,10 @@ class Config(object): """ if self.defconfig_list is None: return None + for filename, cond_expr in self.defconfig_list.defaults: - if eval_expr(cond_expr) != "n": - filename = self._expand_sym_refs(filename.value) + if eval_expr(cond_expr): + filename = self._expand_sym_refs(filename.str_value) try: with self._open(filename) as f: return f.name @@ -474,6 +476,7 @@ class Config(object): self._warn("malformed string literal", filename, linenr) continue + # Strip quotes and remove escapings. The unescaping # procedure should be safe since " can only appear as # \" inside the string. @@ -481,7 +484,7 @@ class Config(object): .replace("\\\\", "\\") if sym.choice is not None: - mode = sym.choice.user_value + mode = sym.choice.user_str_value if mode is not None and mode != val: self._warn("assignment to {} changes mode of " 'containing choice from "{}" to "{}".' @@ -504,10 +507,10 @@ class Config(object): # Done parsing the assignment. Set the value. - if sym.user_value is not None: + if sym.user_str_value is not None: self._warn('{} set more than once. Old value: "{}", new ' 'value: "{}".' - .format(name, sym.user_value, val), + .format(name, sym.user_str_value, val), filename, linenr) sym._set_value_no_invalidate(val, True) @@ -542,7 +545,7 @@ class Config(object): returns "y". This function always yields a tristate value. To get the value of - non-bool, non-tristate symbols, use Symbol.value. + non-bool, non-tristate symbols, use Symbol.str_value. The result of this function is consistent with how evaluation works for conditional ('if ...') expressions in the configuration (as well as in @@ -571,11 +574,12 @@ class Config(object): for sym in self.defined_syms: # We're iterating over all symbols, so no need for symbols to # invalidate their dependent symbols - sym.user_value = None + sym.user_str_value = sym.user_tri_value = None sym._invalidate() for choice in self._choices: - choice.user_value = choice.user_selection = None + choice.user_str_value = choice.user_tri_value = \ + choice.user_selection = None choice._invalidate() def enable_warnings(self): @@ -616,7 +620,7 @@ class Config(object): 'config symbol prefix "{}"'.format(self.config_prefix), "warnings " + ("enabled" if self._print_warnings else "disabled"), "undef. symbol assignment warnings " + - ("enabled" if self._print_undef_assign else "disabled") + ("enabled" if self._print_undef_assign else "disabled"), ) return "<{}>".format(", ".join(fields)) @@ -1007,10 +1011,7 @@ class Config(object): prev_node.next = prev_node = node elif t0 == _T_SOURCE: - kconfig_file = self._next_token() - exp_kconfig_file = self._expand_sym_refs(kconfig_file) - - self._enter_file(exp_kconfig_file) + self._enter_file(self._expand_sym_refs(self._next_token())) prev_node = self._parse_block(None, # end_token parent, visible_if_deps, @@ -1030,8 +1031,7 @@ class Config(object): node.parent = parent node.filename = self._filename node.linenr = self._linenr - node.dep = \ - self._make_and(parent.dep, self._parse_expr(True)) + node.dep = self._make_and(parent.dep, self._parse_expr(True)) self._parse_block(_T_ENDIF, node, # parent @@ -1259,16 +1259,10 @@ class Config(object): node.item.env_var = env_var if env_var not in os.environ: - self._warn("the symbol {0} references the " - "non-existent environment variable {1} " - "(meaning the 'option env=\"{1}\"' will " - "have no effect). If you're using " - "Kconfiglib via 'make (i)scriptconfig', it " - "should have set up the environment " - "correctly for you. If you still got this " - "message, that might be an error, and you " - "should email ulfalizer a.t Google's email " - "service.".format(node.item.name, env_var), + self._warn("'option env=\"{0}\"' on symbol {1} will " + "have no effect, because the environment " + "variable {0} is not set" + .format(node.item.name, env_var), self._filename, self._linenr) else: defaults.append( @@ -1565,9 +1559,9 @@ class Config(object): add_fn(config_string) sym._already_written = True - elif (node.item == MENU and eval_expr(node.dep) != "n" and - eval_expr(node.visibility) != "n") or \ - (node.item == COMMENT and eval_expr(node.dep) != "n"): + elif eval_expr(node.dep) and \ + ((node.item == MENU and eval_expr(node.visibility)) or + node.item == COMMENT): add_fn("\n#\n# {}\n#\n".format(node.prompt[0])) @@ -1669,13 +1663,13 @@ class Config(object): while 1: sym_ref_match = _sym_ref_re_search(s) - if sym_ref_match is None: + if not sym_ref_match: return s sym = self.syms.get(sym_ref_match.group(1)) s = s[:sym_ref_match.start()] + \ - (sym.value if sym is not None else "") + \ + (sym.str_value if sym is not None else "") + \ s[sym_ref_match.end():] # @@ -1772,10 +1766,14 @@ class Symbol(object): menuconfig-like functionality. (Check the implementation of the property if you need to get the original type.) - value: + str_value: + TODO The current value of the symbol. Automatically recalculated as dependencies change. + tri_value: + TODO + assignable: A string containing the tristate values that can be assigned to the symbol, ordered from lowest (n) to highest (y). This corresponds to the @@ -1819,13 +1817,23 @@ class Symbol(object): dependencies) are propagated to the prompt dependencies. Additional dependencies can be specified with e.g. 'bool "foo" if ". - user_value: - The value assigned with Symbol.set_value(), or None if no value has been - assigned. This won't necessarily match 'value' even if set, as - dependencies and prompt visibility take precedence. + user_str_value: + The string value assigned with Symbol.set_value(), or None if no value + has been assigned. This won't necessarily match 'str_value' even if set, + as dependencies and prompt visibility take precedence. + + Note that you should use Symbol.set_value() to change this value (which + will also change user_tri_value). Changing the value directly will break + things, as Kconfiglib might need to invalidate other symbols. Properties + are always read-only. + + The string value is only used in comparisons (e.g. + 'depends on SYMBOL = "foo"'). See user_tri_value. - Note that you should use Symbol.set_value() to change this value. - Properties are always read-only. + user_tri_value: + The tristate value corresponding to user_str_value. The rule is that "n", + "m", and "y" correspond to 0, 1, and 2 for BOOL and TRISTATE symbols. + Other symbol types always evaluate to 0 (n) in a tristate sense. config_string: The .config assignment string that would get written out for the symbol @@ -1917,7 +1925,8 @@ class Symbol(object): "_already_written", "_cached_assignable", "_cached_deps", - "_cached_val", + "_cached_str_val", + "_cached_tri_val", "_cached_vis", "_direct_dependents", "_type", @@ -1935,7 +1944,8 @@ class Symbol(object): "ranges", "rev_dep", "selects", - "user_value", + "user_str_value", + "user_tri_value", "weak_rev_dep", ) @@ -1950,138 +1960,79 @@ class Symbol(object): """ if self._type == TRISTATE and \ - ((self.choice is not None and self.choice.value == "y") or - self.config.modules.value == "n"): + ((self.choice is not None and self.choice.tri_value == 2) or + not self.config.modules.tri_value): return BOOL return self._type @property - def value(self): + def str_value(self): """ See the class documentation. """ - if self._cached_val is not None: - return self._cached_val + if self._cached_str_val is not None: + return self._cached_str_val + + if self._type in (BOOL, TRISTATE): + self._cached_str_val = TRI_TO_STR[self.tri_value] + return self._cached_str_val # As a quirk of Kconfig, undefined symbols get their name as their - # value. This is why things like "FOO = bar" work for seeing if FOO has - # the value "bar". + # string value. This is why things like "FOO = bar" work for seeing if + # FOO has the value "bar". if self._type == UNKNOWN: - self._cached_val = self.name + self._cached_str_val = self.name return self.name - # This will hold the value at the end of the function - val = _DEFAULT_VALUE[self._type] - + val = "" vis = self.visibility - if self._type in (BOOL, TRISTATE): - if self.choice is None: - self._write_to_conf = (vis != "n") + self._write_to_conf = (vis != 0) - if vis != "n" and self.user_value is not None: - # If the symbol is visible and has a user value, we use - # that - val = _tri_min(self.user_value, vis) - - else: - # Otherwise, we look at defaults and weak reverse - # dependencies (implies) - - for default, cond in self.defaults: - cond_val = eval_expr(cond) - if cond_val != "n": - self._write_to_conf = True - val = _tri_min(eval_expr(default), cond_val) - break - - # Weak reverse dependencies are only considered if our - # direct dependencies are met - if eval_expr(self.direct_dep) != "n": - weak_rev_dep_val = \ - eval_expr(self.weak_rev_dep) - if weak_rev_dep_val != "n": - self._write_to_conf = True - val = _tri_max(val, weak_rev_dep_val) - - # Reverse (select-related) dependencies take precedence - rev_dep_val = eval_expr(self.rev_dep) - if rev_dep_val != "n": - self._write_to_conf = True - val = _tri_max(val, rev_dep_val) - - else: - # (bool/tristate) symbol in choice. See _get_visibility() for - # more choice-related logic. - - # Initially - self._write_to_conf = False - - if vis != "n": - mode = self.choice.value - - if mode != "n": - self._write_to_conf = True - - if mode == "y": - val = "y" if self.choice.selection is self else "n" - elif self.user_value in ("m", "y"): - # mode == "m" and self.user_value is not None or - # "n" - val = "m" - - # "m" is promoted to "y" in two circumstances: - # 1) If our type is boolean - # 2) If our weak_rev_dep (from IMPLY) is "y" - if val == "m" and \ - (self.type == BOOL or eval_expr(self.weak_rev_dep) == "y"): - val = "y" - - elif self._type in (INT, HEX): + if self._type in (INT, HEX): base = _TYPE_TO_BASE[self._type] # Check if a range is in effect for low_expr, high_expr, cond_expr in self.ranges: - if eval_expr(cond_expr) != "n": + if eval_expr(cond_expr): has_active_range = True - low = int(low_expr.value, base) if \ - _is_base_n(low_expr.value, base) else 0 - high = int(high_expr.value, base) if \ - _is_base_n(high_expr.value, base) else 0 + low = int(low_expr.str_value, base) if \ + _is_base_n(low_expr.str_value, base) else 0 + high = int(high_expr.str_value, base) if \ + _is_base_n(high_expr.str_value, base) else 0 break else: has_active_range = False - self._write_to_conf = (vis != "n") - - if vis != "n" and self.user_value is not None and \ - _is_base_n(self.user_value, base) and \ + if vis and self.user_str_value is not None and \ + _is_base_n(self.user_str_value, base) and \ (not has_active_range or - low <= int(self.user_value, base) <= high): + low <= int(self.user_str_value, base) <= high): # If the user value is well-formed and satisfies range # contraints, it is stored in exactly the same form as # specified in the assignment (with or without "0x", etc.) - val = self.user_value + val = self.user_str_value else: # No user value or invalid user value. Look at defaults. for val_expr, cond_expr in self.defaults: - if eval_expr(cond_expr) != "n": + if eval_expr(cond_expr): self._write_to_conf = True # Similarly to above, well-formed defaults are # preserved as is. Defaults that do not satisfy a range # constraints are clamped and take on a standard form. - val = val_expr.value + val = val_expr.str_value if _is_base_n(val, base): val_num = int(val, base) + # TODO: move outside? if has_active_range: clamped_val = None @@ -2105,18 +2056,93 @@ class Symbol(object): val = (hex(low) if self._type == HEX else str(low)) elif self._type == STRING: - self._write_to_conf = (vis != "n") - - if vis != "n" and self.user_value is not None: - val = self.user_value + if vis and self.user_str_value is not None: + val = self.user_str_value else: for val_expr, cond_expr in self.defaults: - if eval_expr(cond_expr) != "n": + if eval_expr(cond_expr): self._write_to_conf = True - val = val_expr.value + val = val_expr.str_value break - self._cached_val = val + self._cached_str_val = val + return val + + @property + def tri_value(self): + """ + See the class documentation. + """ + + if self._cached_tri_val is not None: + return self._cached_tri_val + + if self._type not in (BOOL, TRISTATE): + self._cached_tri_val = 0 + return self._cached_tri_val + + val = 0 + vis = self.visibility + + if self.choice is None: + self._write_to_conf = (vis != 0) + + if vis and self.user_tri_value is not None: + # If the symbol is visible and has a user value, we use that + val = min(self.user_tri_value, vis) + + else: + # Otherwise, we look at defaults and weak reverse dependencies + # (implies) + + for default, cond in self.defaults: + cond_val = eval_expr(cond) + if cond_val: + val = min(cond_val, eval_expr(default)) + self._write_to_conf = True + break + + # Weak reverse dependencies are only considered if our + # direct dependencies are met + if eval_expr(self.direct_dep): + weak_rev_dep_val = eval_expr(self.weak_rev_dep) + if weak_rev_dep_val: + val = max(weak_rev_dep_val, val) + self._write_to_conf = True + + # Reverse (select-related) dependencies take precedence + rev_dep_val = eval_expr(self.rev_dep) + if rev_dep_val: + val = max(rev_dep_val, val) + self._write_to_conf = True + + else: + # (bool/tristate) symbol in choice. See _get_visibility() for + # more choice-related logic. + + # Initially + self._write_to_conf = False + + if vis: + mode = self.choice.tri_value + + if mode: + self._write_to_conf = True + + if mode == 2: + val = 2 if self.choice.selection is self else 0 + elif self.user_tri_value: + # mode == 1, user value available and not 0 + val = 1 + + # m is promoted to y in two circumstances: + # 1) If our type is boolean + # 2) If our weak_rev_dep (from IMPLY) is y + if val == 1 and \ + (self.type == BOOL or eval_expr(self.weak_rev_dep) == 2): + val = 2 + + self._cached_tri_val = val return val @property @@ -2152,8 +2178,9 @@ class Symbol(object): # corresponds to the SYMBOL_AUTO flag in the C implementation. return None - # Note: _write_to_conf is determined when the value is calculated - val = self.value + # Note: _write_to_conf is determined when the value is calculated. This + # is a hidden function call due to property magic. + val = self.str_value if not self._write_to_conf: return None @@ -2184,10 +2211,11 @@ class Symbol(object): Equal in effect to assigning the value to the symbol within a .config file. Use the 'assignable' attribute to check which values can currently be assigned. Setting values outside 'assignable' will cause - Symbol.user_value to differ from Symbol.value (be truncated down or - up). Values that are invalid for the type (such as "foo" or "m" for a - BOOL) are ignored (and won't be stored in Symbol.user_value). A warning - is printed for attempts to assign invalid values. + Symbol.user_str/tri_value to differ from Symbol.str/tri_value (be + truncated down or up). Values that are invalid for the type (such as + "foo" or "m" for a BOOL) are ignored (and won't be stored in + Symbol.user_str/tri_value). A warning is printed for attempts to assign + invalid values. The values of other symbols that depend on this symbol are automatically recalculated to reflect the new value. @@ -2208,7 +2236,7 @@ class Symbol(object): Resets the user value of the symbol, as if the symbol had never gotten a user value via Config.load_config() or Symbol.set_value(). """ - self.user_value = None + self.user_str_value = self.user_tri_value = None self._rec_invalidate() def __str__(self): @@ -2229,17 +2257,17 @@ class Symbol(object): def __repr__(self): """ Prints some information about the symbol (including its name, value, - and visibility) when it is evaluated. + visibility, and location(s)) when it is evaluated. """ fields = [ "symbol " + self.name, _TYPENAME[self.type], - 'value "{}"'.format(self.value), - "visibility {}".format(self.visibility) + 'value "{}"'.format(self.str_value), + "visibility " + TRI_TO_STR[self.visibility], ] - if self.user_value is not None: - fields.append('user value "{}"'.format(self.user_value)) + if self.user_str_value is not None: + fields.append('user value "{}"'.format(self.user_str_value)) if self.choice is not None: fields.append("choice symbol") @@ -2256,11 +2284,13 @@ class Symbol(object): if self is self.config.modules: fields.append("is the modules symbol") - fields.append("direct deps " + eval_expr(self.direct_dep)) + fields.append("direct deps " + TRI_TO_STR[eval_expr(self.direct_dep)]) - fields.append("{} menu node{}" - .format(len(self.nodes), - "" if len(self.nodes) == 1 else "s")) + if self.nodes: + for node in self.nodes: + fields.append("{}:{}".format(node.filename, node.linenr)) + else: + fields.append("undefined") return "<{}>".format(", ".join(fields)) @@ -2292,7 +2322,7 @@ class Symbol(object): self.nodes = [] - self.user_value = None + self.user_str_value = self.user_tri_value = None # Populated in Config._build_dep() after parsing. Links the symbol to # the symbols that immediately depend on it (in a caching/invalidation @@ -2302,15 +2332,8 @@ class Symbol(object): # Cached values - # Caches the calculated value - self._cached_val = None - # Caches the visibility - self._cached_vis = None - # Caches the total list of dependent symbols. Calculated in - # _get_dependent(). - self._cached_deps = None - # Caches the 'assignable' attribute - self._cached_assignable = None + self._cached_str_val = self._cached_tri_val = self._cached_vis = \ + self._cached_deps = self._cached_assignable = None # Flags @@ -2332,35 +2355,35 @@ class Symbol(object): vis = self.visibility - if vis == "n": + if not vis: return "" rev_dep_val = eval_expr(self.rev_dep) - if vis == "y": - if rev_dep_val == "n": - if self.type == BOOL or eval_expr(self.weak_rev_dep) == "y": + if vis == 2: + if not rev_dep_val: + if self.type == BOOL or eval_expr(self.weak_rev_dep) == 2: return "ny" return "nmy" - if rev_dep_val == "y": + if rev_dep_val == 2: return "y" - # rev_dep_val == "m" + # rev_dep_val == 1 - if self.type == BOOL or eval_expr(self.weak_rev_dep) == "y": + if self.type == BOOL or eval_expr(self.weak_rev_dep) == 2: return "y" return "my" - # vis == "m" + # vis == 1 - if rev_dep_val == "n": - return "m" if eval_expr(self.weak_rev_dep) != "y" else "y" + if not rev_dep_val: + return "m" if eval_expr(self.weak_rev_dep) != 2 else "y" - if rev_dep_val == "y": + if rev_dep_val == 2: return "y" - # vis == "m", rev_dep == "m" (rare) + # vis == rev_dep_val == 1 return "m" @@ -2399,20 +2422,29 @@ class Symbol(object): "promptless symbol {} will have no effect" .format(value, self.name)) - self.user_value = value + self.user_str_value = value + self.user_tri_value = \ + STR_TO_TRI[value] \ + if self._type in (BOOL, TRISTATE) else \ + 0 + + # TODO: assigning automatically changes choice yada yada - if self.choice is not None and self._type in (BOOL, TRISTATE): - if value == "y": - self.choice.user_value = "y" + if self.choice is not None: + if self.user_tri_value == 2: + self.choice.user_str_value = "y" + self.choice.user_tri_value = 2 self.choice.user_selection = self - elif value == "m": - self.choice.user_value = "m" + elif self.user_tri_value == 1: + self.choice.user_str_value = "m" + self.choice.user_tri_value = 1 def _invalidate(self): """ Marks the symbol as needing to be recalculated. """ - self._cached_val = self._cached_vis = self._cached_assignable = None + self._cached_str_val = self._cached_tri_val = self._cached_vis = \ + self._cached_assignable = None def _rec_invalidate(self): """ @@ -2531,10 +2563,13 @@ class Choice(object): The symbol that would be selected by default, had the user not selected any symbol. Can be None for the same reasons as 'selected'. - user_value: + user_str_value: TODO The value (mode) selected by the user (by assigning some choice symbol or calling Choice.set_value()). This does not necessarily match Choice.value - for the same reasons that Symbol.user_value might not match Symbol.value. + for the same reasons that Symbol.user_str_value might not match + Symbol.value. + + user_tri_value: TODO user_selection: The symbol selected by the user (by setting it to "y"). Ignored if the @@ -2585,7 +2620,8 @@ class Choice(object): "nodes", "syms", "user_selection", - "user_value", + "user_str_value", + "user_tri_value", ) # @@ -2595,25 +2631,32 @@ class Choice(object): @property def type(self): """Returns the type of the choice. See Symbol.type.""" - if self._type == TRISTATE and self.config.modules.value == "n": + if self._type == TRISTATE and not self.config.modules.tri_value: return BOOL return self._type @property - def value(self): + def str_value(self): """ See the class documentation. """ - if self.user_value is not None: - val = _tri_min(self.user_value, self.visibility) + return TRI_TO_STR[self.tri_value] + + @property + def tri_value(self): + """ + See the class documentation. + """ + if self.user_tri_value is not None: + val = min(self.user_tri_value, self.visibility) else: - val = "n" + val = 0 - if val == "n" and not self.is_optional: - val = "m" + if not val and not self.is_optional: + val = 1 # Promote "m" to "y" for boolean choices - return "y" if val == "m" and self.type == BOOL else val + return 2 if val == 1 and self.type == BOOL else val @property def assignable(self): @@ -2645,13 +2688,13 @@ class Choice(object): if self._cached_selection is not _NO_CACHED_SELECTION: return self._cached_selection - if self.value != "y": + if self.tri_value != 2: self._cached_selection = None return None # User choice available? if self.user_selection is not None and \ - self.user_selection.visibility == "y": + self.user_selection.visibility == 2: self._cached_selection = self.user_selection return self.user_selection @@ -2666,14 +2709,12 @@ class Choice(object): See the class documentation. """ for sym, cond_expr in self.defaults: - if (eval_expr(cond_expr) != "n" and - # Must be visible too - sym.visibility != "n"): + if eval_expr(cond_expr) and sym.visibility: return sym # Otherwise, pick the first visible symbol, if any for sym in self.syms: - if sym.visibility != "n": + if sym.visibility: return sym # Couldn't find a default @@ -2692,7 +2733,8 @@ class Choice(object): "which has type {}. Assignment ignored" .format(value, _TYPENAME[self._type])) - self.user_value = value + self.user_str_value = value + self.user_tri_value = STR_TO_TRI[value] if self.syms: # Hackish way to invalidate the choice and all the choice symbols @@ -2703,7 +2745,7 @@ class Choice(object): Resets the user value (mode) and user selection of the Choice, as if the user had never touched the mode or any of the choice symbols. """ - self.user_value = self.user_selection = None + self.user_str_value = self.user_tri_value = self.user_selection = None if self.syms: # Hackish way to invalidate the choice and all the choice symbols self.syms[0]._rec_invalidate() @@ -2716,11 +2758,14 @@ class Choice(object): return _sym_choice_str(self) def __repr__(self): + """ + TODO + """ fields = [ "choice" if self.name is None else "choice " + self.name, _TYPENAME[self.type], - "mode " + self.value, - "visibility " + self.visibility + "mode " + self.str_value, + "visibility " + TRI_TO_STR[self.visibility], ] if self.is_optional: @@ -2729,13 +2774,11 @@ class Choice(object): if self.selection is not None: fields.append("{} selected".format(self.selection.name)) - fields.append("{} menu node{}" - .format(len(self.nodes), - "" if len(self.nodes) == 1 else "s")) + for node in self.nodes: + fields.append("{}:{}".format(node.filename, node.linenr)) return "<{}>".format(", ".join(fields)) - # # Private methods # @@ -2757,17 +2800,15 @@ class Choice(object): self.nodes = [] - self.user_value = None - self.user_selection = None + self.user_str_value = self.user_tri_value = self.user_selection = None # The prompts and default values without any dependencies from # enclosing menus and ifs propagated self.defaults = [] # Cached values + self._cached_vis = self._cached_assignable = None self._cached_selection = _NO_CACHED_SELECTION - self._cached_vis = None - self._cached_assignable = None self.is_optional = False @@ -2778,21 +2819,21 @@ class Choice(object): vis = self.visibility - if vis == "n": + if not vis: return "" - if vis == "y": + if vis == 2: if not self.is_optional: return "y" if self.type == BOOL else "my" return "y" - # vis == "m" + # vis == 1 return "nm" if self.is_optional else "m" def _invalidate(self): - self._cached_selection = _NO_CACHED_SELECTION self._cached_vis = self._cached_assignable = None + self._cached_selection = _NO_CACHED_SELECTION class MenuNode(object): """ @@ -2920,15 +2961,17 @@ class MenuNode(object): if self.prompt is not None: fields.append('prompt "{}" (visibility {})' - .format(self.prompt[0], eval_expr(self.prompt[1]))) + .format(self.prompt[0], + TRI_TO_STR[eval_expr(self.prompt[1])])) if isinstance(self.item, Symbol) and self.is_menuconfig: fields.append("is menuconfig") - fields.append("deps " + eval_expr(self.dep)) + fields.append("deps " + TRI_TO_STR[eval_expr(self.dep)]) if self.item == MENU: - fields.append("'visible if' deps " + eval_expr(self.visibility)) + fields.append("'visible if' deps " + \ + TRI_TO_STR[eval_expr(self.visibility)]) if isinstance(self.item, (Symbol, Choice)) and self.help is not None: fields.append("has help") @@ -2957,65 +3000,26 @@ class InternalError(Exception): # Public functions # -def tri_less(v1, v2): - """ - Returns True if the tristate v1 is less than the tristate v2, where "n", - "m" and "y" are ordered from lowest to highest. - """ - return _TRI_TO_INT[v1] < _TRI_TO_INT[v2] - -def tri_less_eq(v1, v2): - """ - Returns True if the tristate v1 is less than or equal to the tristate v2, - where "n", "m" and "y" are ordered from lowest to highest. - """ - return _TRI_TO_INT[v1] <= _TRI_TO_INT[v2] - -def tri_greater(v1, v2): - """ - Returns True if the tristate v1 is greater than the tristate v2, where "n", - "m" and "y" are ordered from lowest to highest. - """ - return _TRI_TO_INT[v1] > _TRI_TO_INT[v2] - -def tri_greater_eq(v1, v2): - """ - Returns True if the tristate v1 is greater than or equal to the tristate - v2, where "n", "m" and "y" are ordered from lowest to highest. - """ - return _TRI_TO_INT[v1] >= _TRI_TO_INT[v2] - -# Expression evaluation - def eval_expr(expr): """ - Evaluates an expression to "n", "m", or "y". + TODO """ - if isinstance(expr, Symbol): - # Non-bool/tristate symbols are always "n" in a tristate sense, - # regardless of their value - return expr.value if expr._type in (BOOL, TRISTATE) else "n" + return expr.tri_value if expr[0] == AND: - ev1 = eval_expr(expr[1]) - - # Short-circuit the ev1 == "n" case - return "n" if ev1 == "n" else \ - _tri_min(ev1, eval_expr(expr[2])) + v1 = eval_expr(expr[1]) + # Short-circuit the n case as an optimization (~5% faster + # allnoconfig.py and allyesconfig.py, as of writing) + return 0 if not v1 else min(v1, eval_expr(expr[2])) if expr[0] == OR: - ev1 = eval_expr(expr[1]) - - # Short-circuit the ev1 == "y" case - return "y" if ev1 == "y" else \ - _tri_max(ev1, eval_expr(expr[2])) + v1 = eval_expr(expr[1]) + # Short-circuit the y case as an optimization + return 2 if v1 == 2 else max(v1, eval_expr(expr[2])) if expr[0] == NOT: - ev = eval_expr(expr[1]) - return "n" if ev == "y" else \ - "y" if ev == "n" else \ - "m" + return 2 - eval_expr(expr[1]) if expr[0] in _RELATIONS: # Implements <, <=, >, >= comparisons as well. These were added to @@ -3030,16 +3034,16 @@ def eval_expr(expr): # If both operands are strings... if op1._type == STRING and op2._type == STRING: # ...then compare them lexicographically - comp = _strcmp(op1.value, op2.value) + comp = _strcmp(op1.str_value, op2.str_value) else: # Otherwise, try to compare them as numbers... try: - comp = int(op1.value, _TYPE_TO_BASE[op1._type]) - \ - int(op2.value, _TYPE_TO_BASE[op2._type]) + comp = int(op1.str_value, _TYPE_TO_BASE[op1._type]) - \ + int(op2.str_value, _TYPE_TO_BASE[op2._type]) except ValueError: # Fall back on a lexicographic comparison if the operands don't # parse as numbers - comp = _strcmp(op1.value, op2.value) + comp = _strcmp(op1.str_value, op2.str_value) if oper == EQUAL: res = comp == 0 elif oper == UNEQUAL: res = comp != 0 @@ -3048,12 +3052,15 @@ def eval_expr(expr): elif oper == GREATER: res = comp > 0 elif oper == GREATER_EQUAL: res = comp >= 0 - return "y" if res else "n" + return 2*res _internal_error("Internal error while evaluating expression: " "unknown operation {}.".format(expr[0])) def expr_str(expr): + """ + TODO + """ if isinstance(expr, Symbol): return expr.name if not expr.is_constant else '"{}"'.format(expr.name) @@ -3085,47 +3092,35 @@ def _get_visibility(sc): 'make menuconfig'. This function calculates the visibility for the Symbol or Choice 'sc' -- the logic is nearly identical. """ - vis = "n" + vis = 0 for node in sc.nodes: if node.prompt: - vis = _tri_max(vis, eval_expr(node.prompt[1])) + vis = max(vis, eval_expr(node.prompt[1])) if isinstance(sc, Symbol) and sc.choice is not None: if sc.choice._type == TRISTATE and sc._type != TRISTATE and \ - sc.choice.value != "y": + sc.choice.tri_value != 2: # Non-tristate choice symbols in tristate choices depend on the # choice being in mode "y" - return "n" + return 0 - if sc._type == TRISTATE and vis == "m" and sc.choice.value == "y": + if sc._type == TRISTATE and vis == 1 and sc.choice.tri_value == 2: # Choice symbols with visibility "m" are not visible if the # choice has mode "y" - return "n" + return 0 - vis = _tri_min(vis, sc.choice.visibility) + vis = min(vis, sc.choice.visibility) - # Promote "m" to "y" if we're dealing with a non-tristate. This might lead - # to infinite recursion if something really weird is done with MODULES, but + # Promote m to y if we're dealing with a non-tristate. This might lead to + # infinite recursion if something really weird is done with MODULES, but # it's not a problem in practice. - if vis == "m" and \ - (sc._type != TRISTATE or sc.config.modules.value == "n"): - return "y" + if vis == 1 and \ + (sc._type != TRISTATE or not sc.config.modules.tri_value): + return 2 return vis -def _tri_min(v1, v2): - """ - Returns the smallest tristate value among v1 and v2. - """ - return v1 if _TRI_TO_INT[v1] <= _TRI_TO_INT[v2] else v2 - -def _tri_max(v1, v2): - """ - Returns the largest tristate value among v1 and v2. - """ - return v1 if _TRI_TO_INT[v1] >= _TRI_TO_INT[v2] else v2 - def _make_depend_on(sym, expr): """ Adds 'sym' as a dependency to all symbols in 'expr'. Constant symbols in @@ -3192,10 +3187,8 @@ def _strcmp(s1, s2): return (s1 > s2) - (s1 < s2) def _stderr_msg(msg, filename, linenr): - if filename is None: - s = msg - else: - s = "{}:{}: ".format(filename, linenr) + if filename is not None: + msg = "{}:{}: {}".format(filename, linenr, msg) sys.stderr.write(msg + "\n") @@ -3509,6 +3502,18 @@ def _finalize_tree(node): COMMENT, ) = range(2) +TRI_TO_STR = { + 0: "n", + 1: "m", + 2: "y", +} + +STR_TO_TRI = { + "n": 0, + "m": 1, + "y": 2, +} + # # Internal global constants # @@ -3643,12 +3648,12 @@ _sym_ref_re_search = re.compile(r"\$([A-Za-z0-9_]+)").search # Strings to use for types _TYPENAME = { - UNKNOWN: "unknown", - BOOL: "bool", + UNKNOWN: "unknown", + BOOL: "bool", TRISTATE: "tristate", - STRING: "string", - HEX: "hex", - INT: "int", + STRING: "string", + HEX: "hex", + INT: "int", } # Token to type mapping @@ -3662,16 +3667,6 @@ _TOKEN_TO_TYPE = { _T_TRISTATE: TRISTATE, } -# Default values for symbols of different types (the value the symbol gets if -# it is not assigned a user value and none of its 'default' clauses kick in) -_DEFAULT_VALUE = { - BOOL: "n", - TRISTATE: "n", - HEX: "", - INT: "", - STRING: "", -} - # Constant representing that there's no cached choice selection. This is # distinct from a cached None (no selection). We create a unique object (any # will do) for it so we can test with 'is'. diff --git a/testsuite.py b/testsuite.py index e81ba1d..711a0fb 100644 --- a/testsuite.py +++ b/testsuite.py @@ -168,9 +168,9 @@ def run_selftests(): Verifies that a symbol has a particular value. """ sym = c.syms[sym_name] - verify(sym.value == val, + verify(sym.str_value == val, 'expected {} to have the value "{}", had the value "{}"' - .format(sym_name, val, sym.value)) + .format(sym_name, val, sym.str_value)) def assign_and_verify_value(sym_name, val, new_val): """ @@ -178,13 +178,13 @@ def run_selftests(): 'new_val'. """ sym = c.syms[sym_name] - old_val = sym.value + old_val = sym.str_value sym.set_value(val) - verify(sym.value == new_val, + verify(sym.str_value == new_val, 'expected {} to have the value "{}" after being assigned the ' 'value "{}". Instead, the value is "{}". The old value was ' '"{}".' - .format(sym_name, new_val, val, sym.value, old_val)) + .format(sym_name, new_val, val, sym.str_value, old_val)) def assign_and_verify(sym_name, user_val): """ @@ -197,51 +197,19 @@ def run_selftests(): """Assigns a user value to the symbol and verifies the new user value.""" sym = c.syms[sym_name] - sym_old_user_val = sym.user_value + sym_old_user_val = sym.user_str_value sym.set_value(val) - verify(sym.user_value == user_val, + verify(sym.user_str_value == user_val, "{} should have the user value '{}' after being assigned " "the user value '{}'. Instead, the new user value was '{}'. " "The old user value was '{}'." - .format(sym_name, user_val, user_val, sym.user_value, + .format(sym_name, user_val, user_val, sym.user_str_value, sym_old_user_val)) # # Selftests # - print("Testing tristate comparisons") - - def verify_truth_table(comp_fn, *table): - for (x, y), expected in zip((("n", "n"), ("n", "m"), ("n", "y"), - ("m", "n"), ("m", "m"), ("m", "y"), - ("y", "n"), ("y", "m"), ("y", "y")), - table): - verify(comp_fn(x, y) == expected, - "expected {} on ('{}', '{}') to be '{}'". - format(comp_fn, x, y, expected)) - - verify_truth_table(kconfiglib.tri_less, - False, True, True, - False, False, True, - False, False, False) - - verify_truth_table(kconfiglib.tri_less_eq, - True, True, True, - False, True, True, - False, False, True) - - verify_truth_table(kconfiglib.tri_greater, - False, False, False, - True, False, False, - True, True, False) - - verify_truth_table(kconfiglib.tri_greater_eq, - True, False, False, - True, True, False, - True, True, True) - - print("Testing string literal lexing") # Dummy empty configuration just to get a Config object @@ -250,14 +218,12 @@ def run_selftests(): def verify_string_lex(s, res): """ Verifies that a constant symbol with the name 'res' is produced from - lexing 's'. Strips the first and last characters from 's' so that - readable raw strings can be used as input + lexing 's' """ - c._line = s[1:-1] - c._tokenize(True) + c.eval_string(s) verify(c._tokens[0].name == res, - 'expected {} to produced the constant symbol {}, produced {}' - .format(s, c._tokens[0].name, res)) + "expected <{}> to produced the constant symbol <{}>, " + 'produced <{}>'.format(s[1:-1], c._tokens[0].name, res)) verify_string_lex(r""" "" """, "") verify_string_lex(r""" '' """, "") @@ -293,13 +259,12 @@ def run_selftests(): first and last characters from 's' so we can use readable raw strings as input. """ - c._line = s[1:-1] try: - c._tokenize(True) + c.eval_string(s) except kconfiglib.KconfigSyntaxError: pass else: - fail("expected tokenization of {} to fail, didn't".format(s)) + fail("expected tokenization of {} to fail, didn't".format(s[1:-1])) verify_string_bad(r""" " """) verify_string_bad(r""" ' """) @@ -323,168 +288,168 @@ def run_selftests(): "'{}' evaluated to {}, expected {}".format(expr, res, val)) # No modules - verify_eval("n", "n") - verify_eval("m", "n") - verify_eval("y", "y") - verify_eval("'n'", "n") - verify_eval("'m'", "n") - verify_eval("'y'", "y") - verify_eval("M", "y") + verify_eval("n", 0) + verify_eval("m", 0) + verify_eval("y", 2) + verify_eval("'n'", 0) + verify_eval("'m'", 0) + verify_eval("'y'", 2) + verify_eval("M", 2) # Modules - c.syms["MODULES"].set_value("y") - verify_eval("n", "n") - verify_eval("m", "m") - verify_eval("y", "y") - verify_eval("'n'", "n") - verify_eval("'m'", "m") - verify_eval("'y'", "y") - verify_eval("M", "m") - verify_eval("(Y || N) && (m && y)", "m") + c.modules.set_value("y") + verify_eval("n", 0) + verify_eval("m", 1) + verify_eval("y", 2) + verify_eval("'n'", 0) + verify_eval("'m'", 1) + verify_eval("'y'", 2) + verify_eval("M", 1) + verify_eval("(Y || N) && (m && y)", 1) # Non-bool/non-tristate symbols are always "n" in a tristate sense - verify_eval("Y_STRING", "n") - verify_eval("Y_STRING || m", "m") + verify_eval("Y_STRING", 0) + verify_eval("Y_STRING || m", 1) # As are all constants besides "y" and "m" - verify_eval('"foo"', "n") - verify_eval('"foo" || "bar"', "n") - verify_eval('"foo" || m', "m") + verify_eval('"foo"', 0) + verify_eval('"foo" || "bar"', 0) + verify_eval('"foo" || m', 1) # Test equality for symbols - verify_eval("N = N", "y") - verify_eval("N = n", "y") - verify_eval("N = 'n'", "y") - verify_eval("N != N", "n") - verify_eval("N != n", "n") - verify_eval("N != 'n'", "n") - - verify_eval("M = M", "y") - verify_eval("M = m", "y") - verify_eval("M = 'm'", "y") - verify_eval("M != M", "n") - verify_eval("M != m", "n") - verify_eval("M != 'm'", "n") - - verify_eval("Y = Y", "y") - verify_eval("Y = y", "y") - verify_eval("Y = 'y'", "y") - verify_eval("Y != Y", "n") - verify_eval("Y != y", "n") - verify_eval("Y != 'y'", "n") - - verify_eval("N != M", "y") - verify_eval("N != Y", "y") - verify_eval("M != Y", "y") - - verify_eval("Y_STRING = y", "y") - verify_eval("Y_STRING = 'y'", "y") - verify_eval('FOO_BAR_STRING = "foo bar"', "y") - verify_eval('FOO_BAR_STRING != "foo bar baz"', "y") - verify_eval('INT_37 = 37', "y") - verify_eval("INT_37 = '37'", "y") - verify_eval('HEX_0X37 = 0x37', "y") - verify_eval("HEX_0X37 = '0x37'", "y") + verify_eval("N = N", 2) + verify_eval("N = n", 2) + verify_eval("N = 'n'", 2) + verify_eval("N != N", 0) + verify_eval("N != n", 0) + verify_eval("N != 'n'", 0) + + verify_eval("M = M", 2) + verify_eval("M = m", 2) + verify_eval("M = 'm'", 2) + verify_eval("M != M", 0) + verify_eval("M != m", 0) + verify_eval("M != 'm'", 0) + + verify_eval("Y = Y", 2) + verify_eval("Y = y", 2) + verify_eval("Y = 'y'", 2) + verify_eval("Y != Y", 0) + verify_eval("Y != y", 0) + verify_eval("Y != 'y'", 0) + + verify_eval("N != M", 2) + verify_eval("N != Y", 2) + verify_eval("M != Y", 2) + + verify_eval("Y_STRING = y", 2) + verify_eval("Y_STRING = 'y'", 2) + verify_eval('FOO_BAR_STRING = "foo bar"', 2) + verify_eval('FOO_BAR_STRING != "foo bar baz"', 2) + verify_eval('INT_37 = 37', 2) + verify_eval("INT_37 = '37'", 2) + verify_eval('HEX_0X37 = 0x37', 2) + verify_eval("HEX_0X37 = '0x37'", 2) # These should also hold after 31847b67 (kconfig: allow use of relations # other than (in)equality) - verify_eval("HEX_0X37 = '0x037'", "y") - verify_eval("HEX_0X37 = '0x0037'", "y") + verify_eval("HEX_0X37 = '0x037'", 2) + verify_eval("HEX_0X37 = '0x0037'", 2) # Constant symbol comparisons - verify_eval('"foo" != "bar"', "y") - verify_eval('"foo" = "bar"', "n") - verify_eval('"foo" = "foo"', "y") + verify_eval('"foo" != "bar"', 2) + verify_eval('"foo" = "bar"', 0) + verify_eval('"foo" = "foo"', 2) # Undefined symbols get their name as their value c.disable_warnings() - verify_eval("'not_defined' = not_defined", "y") - verify_eval("not_defined_2 = not_defined_2", "y") - verify_eval("not_defined_1 != not_defined_2", "y") + verify_eval("'not_defined' = not_defined", 2) + verify_eval("not_defined_2 = not_defined_2", 2) + verify_eval("not_defined_1 != not_defined_2", 2) # Test less than/greater than # Basic evaluation - verify_eval("INT_37 < 38", "y") - verify_eval("38 < INT_37", "n") - verify_eval("INT_37 < '38'", "y") - verify_eval("'38' < INT_37", "n") - verify_eval("INT_37 < 138", "y") - verify_eval("138 < INT_37", "n") - verify_eval("INT_37 < '138'", "y") - verify_eval("'138' < INT_37", "n") - verify_eval("INT_37 < -138", "n") - verify_eval("-138 < INT_37", "y") - verify_eval("INT_37 < '-138'", "n") - verify_eval("'-138' < INT_37", "y") - verify_eval("INT_37 < 37", "n") - verify_eval("37 < INT_37", "n") - verify_eval("INT_37 < 36", "n") - verify_eval("36 < INT_37", "y") + verify_eval("INT_37 < 38", 2) + verify_eval("38 < INT_37", 0) + verify_eval("INT_37 < '38'", 2) + verify_eval("'38' < INT_37", 0) + verify_eval("INT_37 < 138", 2) + verify_eval("138 < INT_37", 0) + verify_eval("INT_37 < '138'", 2) + verify_eval("'138' < INT_37", 0) + verify_eval("INT_37 < -138", 0) + verify_eval("-138 < INT_37", 2) + verify_eval("INT_37 < '-138'", 0) + verify_eval("'-138' < INT_37", 2) + verify_eval("INT_37 < 37", 0) + verify_eval("37 < INT_37", 0) + verify_eval("INT_37 < 36", 0) + verify_eval("36 < INT_37", 2) # Different formats in comparison - verify_eval("INT_37 < 0x26", "y") # 38 - verify_eval("INT_37 < 0x25", "n") # 37 - verify_eval("INT_37 < 0x24", "n") # 36 - verify_eval("HEX_0X37 < 56", "y") # 0x38 - verify_eval("HEX_0X37 < 55", "n") # 0x37 - verify_eval("HEX_0X37 < 54", "n") # 0x36 + verify_eval("INT_37 < 0x26", 2) # 38 + verify_eval("INT_37 < 0x25", 0) # 37 + verify_eval("INT_37 < 0x24", 0) # 36 + verify_eval("HEX_0X37 < 56", 2) # 0x38 + verify_eval("HEX_0X37 < 55", 0) # 0x37 + verify_eval("HEX_0X37 < 54", 0) # 0x36 # Other int comparisons - verify_eval("INT_37 <= 38", "y") - verify_eval("INT_37 <= 37", "y") - verify_eval("INT_37 <= 36", "n") - verify_eval("INT_37 > 38", "n") - verify_eval("INT_37 > 37", "n") - verify_eval("INT_37 > 36", "y") - verify_eval("INT_37 >= 38", "n") - verify_eval("INT_37 >= 37", "y") - verify_eval("INT_37 >= 36", "y") + verify_eval("INT_37 <= 38", 2) + verify_eval("INT_37 <= 37", 2) + verify_eval("INT_37 <= 36", 0) + verify_eval("INT_37 > 38", 0) + verify_eval("INT_37 > 37", 0) + verify_eval("INT_37 > 36", 2) + verify_eval("INT_37 >= 38", 0) + verify_eval("INT_37 >= 37", 2) + verify_eval("INT_37 >= 36", 2) # Other hex comparisons - verify_eval("HEX_0X37 <= 0x38", "y") - verify_eval("HEX_0X37 <= 0x37", "y") - verify_eval("HEX_0X37 <= 0x36", "n") - verify_eval("HEX_0X37 > 0x38", "n") - verify_eval("HEX_0X37 > 0x37", "n") - verify_eval("HEX_0X37 > 0x36", "y") - verify_eval("HEX_0X37 >= 0x38", "n") - verify_eval("HEX_0X37 >= 0x37", "y") - verify_eval("HEX_0X37 >= 0x36", "y") + verify_eval("HEX_0X37 <= 0x38", 2) + verify_eval("HEX_0X37 <= 0x37", 2) + verify_eval("HEX_0X37 <= 0x36", 0) + verify_eval("HEX_0X37 > 0x38", 0) + verify_eval("HEX_0X37 > 0x37", 0) + verify_eval("HEX_0X37 > 0x36", 2) + verify_eval("HEX_0X37 >= 0x38", 0) + verify_eval("HEX_0X37 >= 0x37", 2) + verify_eval("HEX_0X37 >= 0x36", 2) # A hex holding a value without a "0x" prefix should still be treated as # hexadecimal - verify_eval("HEX_37 < 0x38", "y") - verify_eval("HEX_37 < 0x37", "n") - verify_eval("HEX_37 < 0x36", "n") + verify_eval("HEX_37 < 0x38", 2) + verify_eval("HEX_37 < 0x37", 0) + verify_eval("HEX_37 < 0x36", 0) # Symbol comparisons - verify_eval("INT_37 < HEX_0X37", "y") - verify_eval("INT_37 > HEX_0X37", "n") - verify_eval("HEX_0X37 < INT_37 ", "n") - verify_eval("HEX_0X37 > INT_37 ", "y") - verify_eval("INT_37 < INT_37 ", "n") - verify_eval("INT_37 <= INT_37 ", "y") - verify_eval("INT_37 > INT_37 ", "n") - verify_eval("INT_37 <= INT_37 ", "y") + verify_eval("INT_37 < HEX_0X37", 2) + verify_eval("INT_37 > HEX_0X37", 0) + verify_eval("HEX_0X37 < INT_37 ", 0) + verify_eval("HEX_0X37 > INT_37 ", 2) + verify_eval("INT_37 < INT_37 ", 0) + verify_eval("INT_37 <= INT_37 ", 2) + verify_eval("INT_37 > INT_37 ", 0) + verify_eval("INT_37 <= INT_37 ", 2) # Strings compare lexicographically - verify_eval("'aa' < 'ab'", "y") - verify_eval("'aa' > 'ab'", "n") - verify_eval("'ab' < 'aa'", "n") - verify_eval("'ab' > 'aa'", "y") + verify_eval("'aa' < 'ab'", 2) + verify_eval("'aa' > 'ab'", 0) + verify_eval("'ab' < 'aa'", 0) + verify_eval("'ab' > 'aa'", 2) # Comparisons where one of the operands doesn't parse as a number also give # a lexicographic comparison - verify_eval("INT_37 < '37a' ", "y") - verify_eval("'37a' > INT_37", "y") - verify_eval("INT_37 <= '37a' ", "y") - verify_eval("'37a' >= INT_37", "y") - verify_eval("INT_37 >= '37a' ", "n") - verify_eval("INT_37 > '37a' ", "n") - verify_eval("'37a' < INT_37", "n") - verify_eval("'37a' <= INT_37", "n") + verify_eval("INT_37 < '37a' ", 2) + verify_eval("'37a' > INT_37", 2) + verify_eval("INT_37 <= '37a' ", 2) + verify_eval("'37a' >= INT_37", 2) + verify_eval("INT_37 >= '37a' ", 0) + verify_eval("INT_37 > '37a' ", 0) + verify_eval("'37a' < INT_37", 0) + verify_eval("'37a' <= INT_37", 0) def verify_eval_bad(expr): try: @@ -519,6 +484,8 @@ def run_selftests(): c = kconfiglib.Config("Kconfiglib/tests/Kstr", warn=False) + c.modules.set_value("y") + verify_str(c.syms["UNDEFINED"], """ """) @@ -582,8 +549,10 @@ config INT # We still hardcode the modules symbol. Otherwise OPTIONS would have made # more sense as a name here. - verify_str(c.syms["MODULES"], """ + verify_str(c.modules, """ config MODULES + bool + prompt "MODULES" option modules """) @@ -619,59 +588,58 @@ choice c = kconfiglib.Config("Kconfiglib/tests/Krepr", warn=False) verify_repr(c.syms["UNDEFINED"], """ - + """) verify_repr(c.syms["BASIC"], """ - + """) verify_repr(c.syms["VISIBLE"], """ - + """) verify_repr(c.syms["DIR_DEP_N"], """ - + """) verify_repr(c.syms["OPTIONS"], """ - + """) verify_repr(c.syms["MULTI_DEF"], """ - + """) verify_repr(c.syms["CHOICE_1"], """ - + """) - verify_repr(c.syms["MODULES"], """ - + verify_repr(c.modules, """ + """) - print("Testing Choice.__repr__()") verify_repr(c.named_choices["CHOICE"], """ - + """) c.named_choices["CHOICE"].set_value("y") verify_repr(c.named_choices["CHOICE"], """ - + """) c.syms["CHOICE_2"].set_value("y") verify_repr(c.named_choices["CHOICE"], """ - + """) verify_repr(c.syms["CHOICE_HOOK"].nodes[0].next.item, """ - + """) @@ -830,13 +798,13 @@ g c = kconfiglib.Config("Kconfiglib/tests/Kvisibility") def verify_visibility(item, no_module_vis, module_vis): - c.syms["MODULES"].set_value("n") + c.modules.set_value("n") verify(item.visibility == no_module_vis, "expected {} to have visibility {} without modules, had " "visibility {}". format(repr(item), no_module_vis, item.visibility)) - c.syms["MODULES"].set_value("y") + c.modules.set_value("y") verify(item.visibility == module_vis, "expected {} to have visibility {} with modules, had " "visibility {}". @@ -844,72 +812,70 @@ g # Symbol visibility - verify_visibility(c.syms["NO_PROMPT"], "n", "n") - verify_visibility(c.syms["BOOL_N"], "n", "n") - verify_visibility(c.syms["BOOL_M"], "n", "y") - verify_visibility(c.syms["BOOL_MOD"], "y", "y") - verify_visibility(c.syms["BOOL_Y"], "y", "y") - verify_visibility(c.syms["TRISTATE_M"], "n", "m") - verify_visibility(c.syms["TRISTATE_MOD"], "y", "m") - verify_visibility(c.syms["TRISTATE_Y"], "y", "y") - verify_visibility(c.syms["BOOL_IF_N"], "n", "n") - verify_visibility(c.syms["BOOL_IF_M"], "n", "y") - verify_visibility(c.syms["BOOL_IF_Y"], "y", "y") - verify_visibility(c.syms["BOOL_MENU_N"], "n", "n") - verify_visibility(c.syms["BOOL_MENU_M"], "n", "y") - verify_visibility(c.syms["BOOL_MENU_Y"], "y", "y") - verify_visibility(c.syms["BOOL_CHOICE_N"], "n", "n") + verify_visibility(c.syms["NO_PROMPT"], 0, 0) + verify_visibility(c.syms["BOOL_N"], 0, 0) + verify_visibility(c.syms["BOOL_M"], 0, 2) + verify_visibility(c.syms["BOOL_MOD"], 2, 2) + verify_visibility(c.syms["BOOL_Y"], 2, 2) + verify_visibility(c.syms["TRISTATE_M"], 0, 1) + verify_visibility(c.syms["TRISTATE_MOD"], 2, 1) + verify_visibility(c.syms["TRISTATE_Y"], 2, 2) + verify_visibility(c.syms["BOOL_IF_N"], 0, 0) + verify_visibility(c.syms["BOOL_IF_M"], 0, 2) + verify_visibility(c.syms["BOOL_IF_Y"], 2, 2) + verify_visibility(c.syms["BOOL_MENU_N"], 0, 0) + verify_visibility(c.syms["BOOL_MENU_M"], 0, 2) + verify_visibility(c.syms["BOOL_MENU_Y"], 2, 2) + verify_visibility(c.syms["BOOL_CHOICE_N"], 0, 0) # Non-tristate symbols in tristate choices are only visible if the choice - # is in "y" mode - verify_visibility(c.syms["BOOL_CHOICE_M"], "n", "n") + # is in 2 mode + verify_visibility(c.syms["BOOL_CHOICE_M"], 0, 0) - # Tristate choices start out in "m" mode. When running without modules, - # their type gets adjusted to bool. - verify_visibility(c.syms["BOOL_CHOICE_Y"], "y", "n") + # Tristate choices start out in m mode. When running without modules, their + # type gets adjusted to bool. + verify_visibility(c.syms["BOOL_CHOICE_Y"], 2, 0) c.syms["TRISTATE_CHOICE_M"].set_value("y") c.syms["TRISTATE_CHOICE_Y"].set_value("y") # Still limited by the visibility of the choice - verify_visibility(c.syms["BOOL_CHOICE_M"], "n", "n") + verify_visibility(c.syms["BOOL_CHOICE_M"], 0, 0) # This one should become visible now - verify_visibility(c.syms["BOOL_CHOICE_Y"], "y", "y") - - verify_visibility(c.syms["TRISTATE_IF_N"], "n", "n") - verify_visibility(c.syms["TRISTATE_IF_M"], "n", "m") - verify_visibility(c.syms["TRISTATE_IF_Y"], "y", "y") - verify_visibility(c.syms["TRISTATE_MENU_N"], "n", "n") - verify_visibility(c.syms["TRISTATE_MENU_M"], "n", "m") - verify_visibility(c.syms["TRISTATE_MENU_Y"], "y", "y") - verify_visibility(c.syms["TRISTATE_CHOICE_N"], "n", "n") - verify_visibility(c.syms["TRISTATE_CHOICE_M"], "n", "m") - verify_visibility(c.syms["TRISTATE_CHOICE_Y"], "y", "y") - - verify_visibility(c.named_choices["BOOL_CHOICE_N"], "n", "n") - verify_visibility(c.named_choices["BOOL_CHOICE_M"], "n", "y") - verify_visibility(c.named_choices["BOOL_CHOICE_Y"], "y", "y") - verify_visibility(c.named_choices["TRISTATE_CHOICE_N"], "n", "n") - verify_visibility(c.named_choices["TRISTATE_CHOICE_M"], "n", "m") - verify_visibility(c.named_choices["TRISTATE_CHOICE_Y"], "y", "y") - - verify_visibility(c.named_choices["TRISTATE_CHOICE_IF_M_AND_Y"], - "n", "m") - verify_visibility(c.named_choices["TRISTATE_CHOICE_MENU_N_AND_Y"], - "n", "n") + verify_visibility(c.syms["BOOL_CHOICE_Y"], 2, 2) + + verify_visibility(c.syms["TRISTATE_IF_N"], 0, 0) + verify_visibility(c.syms["TRISTATE_IF_M"], 0, 1) + verify_visibility(c.syms["TRISTATE_IF_Y"], 2, 2) + verify_visibility(c.syms["TRISTATE_MENU_N"], 0, 0) + verify_visibility(c.syms["TRISTATE_MENU_M"], 0, 1) + verify_visibility(c.syms["TRISTATE_MENU_Y"], 2, 2) + verify_visibility(c.syms["TRISTATE_CHOICE_N"], 0, 0) + verify_visibility(c.syms["TRISTATE_CHOICE_M"], 0, 1) + verify_visibility(c.syms["TRISTATE_CHOICE_Y"], 2, 2) + + verify_visibility(c.named_choices["BOOL_CHOICE_N"], 0, 0) + verify_visibility(c.named_choices["BOOL_CHOICE_M"], 0, 2) + verify_visibility(c.named_choices["BOOL_CHOICE_Y"], 2, 2) + verify_visibility(c.named_choices["TRISTATE_CHOICE_N"], 0, 0) + verify_visibility(c.named_choices["TRISTATE_CHOICE_M"], 0, 1) + verify_visibility(c.named_choices["TRISTATE_CHOICE_Y"], 2, 2) + + verify_visibility(c.named_choices["TRISTATE_CHOICE_IF_M_AND_Y"], 0, 1) + verify_visibility(c.named_choices["TRISTATE_CHOICE_MENU_N_AND_Y"], 0, 0) # Menu visibility def verify_menu_visibility(menu, no_module_vis, module_vis): - c["MODULES"].set_value("n") + c.modules.set_value("n") menu_vis = kconfiglib.eval_expr(menu.node.dep) verify(menu_vis == no_module_vis, "menu \"{}\" should have visibility '{}' without modules, " "has visibility '{}'" .format(menu.title, no_module_vis, menu_vis)) - c["MODULES"].set_value("y") + c.modules.set_value("y") menu_vis = kconfiglib.eval_expr(menu.node.dep) verify(menu_vis == module_vis, "menu \"{}\" should have visibility '{}' with modules, " @@ -935,14 +901,14 @@ g menu_visible_if_m_2 = get_menus(c)[13:] def verify_visible_if_visibility(menu, no_module_vis, module_vis): - c["MODULES"].set_value("n") + c.modules.set_value("n") menu_vis = menu.get_visible_if_visibility() verify(menu_vis == no_module_vis, "menu \"{}\" should have 'visible if' visibility '{}' " "without modules, has 'visible if' visibility '{}'". format(menu.title, no_module_vis, menu_vis)) - c["MODULES"].set_value("y") + c.modules.set_value("y") menu_vis = menu.get_visible_if_visibility() verify(menu_vis == module_vis, "menu \"{}\" should have 'visible if' visibility '{}' " @@ -963,15 +929,15 @@ g #verify_visible_if_visibility(menu_visible_if_m_2, "n", "m") # Verify that 'visible if' visibility gets propagated to prompts - verify_visibility(c.syms["VISIBLE_IF_N"], "n", "n") - verify_visibility(c.syms["VISIBLE_IF_M"], "n", "m") - verify_visibility(c.syms["VISIBLE_IF_Y"], "y", "y") - verify_visibility(c.syms["VISIBLE_IF_M_2"], "n", "m") + verify_visibility(c.syms["VISIBLE_IF_N"], 0, 0) + verify_visibility(c.syms["VISIBLE_IF_M"], 0, 1) + verify_visibility(c.syms["VISIBLE_IF_Y"], 2, 2) + verify_visibility(c.syms["VISIBLE_IF_M_2"], 0, 1) # Comment visibility def verify_comment_visibility(comment, no_module_vis, module_vis): - c["MODULES"].set_value("n") + c.modules.set_value("n") # TODO: uninternalize comment_vis = kconfiglib.eval_expr(comment.node.dep) verify(comment_vis == no_module_vis, @@ -979,7 +945,7 @@ g "modules, has visibility '{}'". format(comment.text, no_module_vis, comment_vis)) - c["MODULES"].set_value("y") + c.modules.set_value("y") comment_vis = kconfiglib.eval_expr(comment.node.dep) verify(comment_vis == module_vis, "comment \"{}\" should have visibility '{}' with " @@ -1251,7 +1217,7 @@ g ("BOOL", "TRISTATE", "STRING", "INT", "HEX")] for sym in syms: - verify(sym.user_value is None, + verify(sym.user_str_value is None and sym.user_tri_value is None, "{} should not have a user value to begin with") # Assign valid values for the types @@ -1278,7 +1244,7 @@ g for s in syms: s.unset_value() - verify(s.user_value is None, + verify(s.user_str_value is None and s.user_tri_value is None, "{} should not have a user value after being reset". format(s.name)) @@ -1399,7 +1365,7 @@ g # .config # - print("Testing .config...") + print("Testing Config separation...") c1 = kconfiglib.Config("Kconfiglib/tests/Kmisc", warn=False) c2 = kconfiglib.Config("Kconfiglib/tests/Kmisc", warn=False) @@ -1547,9 +1513,8 @@ g def select_and_verify(sym): choice = get_parent(sym) sym.set_value("y") - verify(choice.value == "y", - 'The mode of the choice should be "y" after selecting a ' - "symbol") + verify(choice.str_value == "y", + "The mode of the choice should be y after selecting a symbol") verify(sym.choice.selection is sym, "{} should be the selected choice symbol" .format(sym.name)) @@ -1568,25 +1533,24 @@ g select_and_verify(choice.syms[i]) def verify_mode(choice, no_modules_mode, modules_mode): - c.syms["MODULES"].set_value("n") - choice_mode = choice.value + c.modules.set_value("n") + choice_mode = choice.tri_value verify(choice_mode == no_modules_mode, - 'Wrong mode for choice {} with no modules. Expected "{}", ' - 'got "{}".'.format(choice.name, no_modules_mode, choice_mode)) + 'Wrong mode for choice {} with no modules. Expected {}, got {}.' + .format(choice.name, no_modules_mode, choice_mode)) - c.syms["MODULES"].set_value("y") - choice_mode = choice.value + c.modules.set_value("y") + choice_mode = choice.tri_value verify(choice_mode == modules_mode, - 'Wrong mode for choice {} with modules. Expected "{}", ' - 'got "{}".'.format(choice.name, modules_mode, - choice_mode)) + 'Wrong mode for choice {} with modules. Expected {}, got {}.' + .format(choice.name, modules_mode, choice_mode)) - verify_mode(choice_bool, "y", "y") - verify_mode(choice_bool_opt, "n", "n") - verify_mode(choice_tristate, "y", "m") - verify_mode(choice_tristate_opt, "n", "n") - verify_mode(choice_bool_m, "y", "y") - verify_mode(choice_tristate_m, "y", "m") + verify_mode(choice_bool, 2, 2) + verify_mode(choice_bool_opt, 0, 0) + verify_mode(choice_tristate, 2, 1) + verify_mode(choice_tristate_opt, 0, 0) + verify_mode(choice_bool_m, 2, 2) + verify_mode(choice_tristate_m, 2, 1) # Test defaults @@ -1606,7 +1570,7 @@ g # Test "y" mode selection - c.syms["MODULES"].set_value("y") + c.modules.set_value("y") select_and_verify_all(choice_bool) select_and_verify_all(choice_bool_opt) @@ -1621,12 +1585,12 @@ g for sym_name in ("T_1", "T_2"): assign_and_verify_value(sym_name, "m", "m") - verify(choice_tristate.value == "m", + verify(choice_tristate.tri_value == 1, 'Selecting {} to "m" should have changed the mode of the ' 'choice to "m"'.format(sym_name)) assign_and_verify_value(sym_name, "y", "y") - verify(choice_tristate.value == "y" and + verify(choice_tristate.tri_value == 2 and choice_tristate.selection is c.syms[sym_name], 'Selecting {} to "y" should have changed the mode of the ' 'choice to "y" and made it the selection'.format(sym_name)) @@ -1638,7 +1602,7 @@ g assign_and_verify_value(sym_name, "n", "n") # "y" should be truncated assign_and_verify_value(sym_name, "y", "m") - verify(choice_tristate_m.value == "m", + verify(choice_tristate_m.tri_value == 1, 'A choice that can only be in "m" mode was not') # Verify that choices with no explicitly specified type get the type of the @@ -1746,8 +1710,10 @@ g "\nSome selftests failed\n") def run_compatibility_tests(): - """Runs tests on configurations from the kernel. Tests compability with the - C implementation by comparing outputs.""" + """ + Runs tests on configurations from the kernel. Tests compability with the + C implementation by comparing outputs. + """ os.environ.pop("ARCH", None) os.environ.pop("SRCARCH", None) @@ -1798,9 +1764,9 @@ def run_compatibility_tests(): if compare_configs: if equal_confs(): - print(" {:14}OK".format(arch)) + print("{:14}OK".format(arch)) else: - print(" {:14}FAIL".format(arch)) + print("{:14}FAIL".format(arch)) fail() if all_passed: @@ -1810,8 +1776,9 @@ def run_compatibility_tests(): print("Some tests failed") def get_arch_srcarch_list(): - """Returns a list of (ARCH, SRCARCH) tuples to test.""" - + """ + Returns a list of (ARCH, SRCARCH) tuples to test. + """ res = [] def add_arch(arch): @@ -1837,16 +1804,17 @@ def get_arch_srcarch_list(): return res def test_load(conf, arch): - """Load all arch Kconfigs to make sure we don't throw any errors""" - print(" {:14}OK".format(arch)) + """ + Load all arch Kconfigs to make sure we don't throw any errors + """ + print("{:14}OK".format(arch)) -# The weird docstring formatting is to get the format right when we print the -# docstring ourselves def test_all_no(conf, arch): """ Verify that our examples/allnoconfig.py script generates the same .config as 'make allnoconfig', for each architecture. Runs the script via - 'make scriptconfig', so kinda slow even in speedy mode.""" + 'make scriptconfig', so kinda slow even in speedy mode. + """ # TODO: Support speedy mode for running the script shell("make scriptconfig SCRIPT=Kconfiglib/examples/allnoconfig.py " @@ -1861,7 +1829,8 @@ def test_all_no_simpler(conf, arch): """ Verify that our examples/allnoconfig_simpler.py script generates the same .config as 'make allnoconfig', for each architecture. Runs the script via - 'make scriptconfig', so kinda slow even in speedy mode.""" + 'make scriptconfig', so kinda slow even in speedy mode. + """ # TODO: Support speedy mode for running the script shell("make scriptconfig SCRIPT=Kconfiglib/examples/allnoconfig_simpler.py " @@ -1876,7 +1845,8 @@ def test_all_yes(conf, arch): """ Verify that our examples/allyesconfig.py script generates the same .config as 'make allyesconfig', for each architecture. Runs the script via - 'make scriptconfig', so kinda slow even in speedy mode.""" + 'make scriptconfig', so kinda slow even in speedy mode. + """ # TODO: Support speedy mode for running the script shell("make scriptconfig SCRIPT=Kconfiglib/examples/allyesconfig.py " @@ -1889,11 +1859,12 @@ def test_all_yes(conf, arch): def test_call_all(conf, arch): """ - Call all public methods on all symbols, menus, choices, and comments for + Call all public methods on all symbols, choices, and TODO menu nodes for all architectures to make sure we never crash or hang. (Nearly all public methods: some are hard to test like this, but are exercised by other - tests.)""" - print(" For {}...".format(arch)) + tests.) + """ + print("For {}...".format(arch)) conf.defconfig_filename conf.mainmenu_text @@ -1909,7 +1880,8 @@ def test_call_all(conf, arch): s.__repr__() s.assignable s.type - s.value + s.str_value + s.tri_value s.visibility s.unset_value() @@ -1933,7 +1905,8 @@ def test_call_all(conf, arch): for c in conf._choices: c.__str__() c.__repr__() - c.value + c.str_value + c.tri_value c.assignable c.selection c.default_selection @@ -1943,7 +1916,8 @@ def test_call_all(conf, arch): def test_config_absent(conf, arch): """ Verify that Kconfiglib generates the same .config as 'make alldefconfig', - for each architecture""" + for each architecture + """ conf.write_config("._config") if speedy: shell("scripts/kconfig/conf --alldefconfig Kconfig") @@ -1959,7 +1933,8 @@ def test_defconfig(conf, arch): run. With logging enabled, this test appends any failures to a file - test_defconfig_fails in the root.""" + test_defconfig_fails in the root. + """ global nconfigs defconfigs = [] @@ -2037,8 +2012,10 @@ def test_defconfig(conf, arch): # def rm_configs(): - """Delete any old ".config" (generated by the C implementation) and - "._config" (generated by us), if present.""" + """ + Delete any old ".config" (generated by the C implementation) and + "._config" (generated by us), if present. + """ def rm_if_exists(f): if os.path.exists(f): os.remove(f) -- cgit v1.2.3 From 840d65fe069297fb1d088b85e3164465ace4d467 Mon Sep 17 00:00:00 2001 From: Ulf Magnusson Date: Sat, 28 Oct 2017 05:18:37 +0200 Subject: Test suite work, cleanup, const sym invalidation fix --- kconfiglib.py | 203 ++++++++++++++++++++++++++++++++++------------------------ testsuite.py | 186 +++++++++++++++++++++++++++++++++++++---------------- 2 files changed, 250 insertions(+), 139 deletions(-) diff --git a/kconfiglib.py b/kconfiglib.py index 25bb380..3262b43 100644 --- a/kconfiglib.py +++ b/kconfiglib.py @@ -360,13 +360,13 @@ class Config(object): # Predefined symbol. DEFCONFIG_LIST uses this. uname_sym = self._lookup_const_sym("UNAME_RELEASE") uname_sym._type = STRING - uname_sym.defaults.append( - (self._lookup_const_sym(platform.uname()[2]), - self.y)) # env_var doubles as the SYMBOL_AUTO flag from the C implementation, so # just set it to something. The naming breaks a bit here, but it's # pretty obscure. uname_sym.env_var = "" + uname_sym.defaults.append( + (self._lookup_const_sym(platform.uname()[2]), + self.y)) self.syms["UNAME_RELEASE"] = uname_sym self.top_node = MenuNode() @@ -418,8 +418,8 @@ class Config(object): if self.defconfig_list is None: return None - for filename, cond_expr in self.defconfig_list.defaults: - if eval_expr(cond_expr): + for filename, cond in self.defconfig_list.defaults: + if expr_value(cond): filename = self._expand_sym_refs(filename.str_value) try: with self._open(filename) as f: @@ -559,7 +559,7 @@ class Config(object): self._line = s del self._tokens[0] - return eval_expr(self._parse_expr(True)) + return expr_value(self._parse_expr(True)) def unset_values(self): """ @@ -921,7 +921,6 @@ class Config(object): self._reuse_line = False while self._line.endswith("\\\n"): - # TODO: Can hang if the file ends with a backslash self._line = self._line[:-2] + self._file.readline() self._linenr += 1 @@ -1346,39 +1345,39 @@ class Config(object): node.prompt = None # Add the new defaults, with dependencies propagated - for val_expr, cond_expr in defaults: + for val_expr, cond in defaults: node.item.defaults.append( - (val_expr, self._make_and(cond_expr, node.dep))) + (val_expr, self._make_and(cond, node.dep))) # Add the new ranges, with dependencies propagated - for low, high, cond_expr in ranges: + for low, high, cond in ranges: node.item.ranges.append( - (low, high, self._make_and(cond_expr, node.dep))) + (low, high, self._make_and(cond, node.dep))) # Handle selects - for target, cond_expr in selects: + for target, cond in selects: # Only stored for convenience. Not used during evaluation. node.item.selects.append( - (target, self._make_and(cond_expr, node.dep))) + (target, self._make_and(cond, node.dep))) # Modify the dependencies of the selected symbol target.rev_dep = \ self._make_or(target.rev_dep, self._make_and(node.item, - self._make_and(cond_expr, + self._make_and(cond, node.dep))) # Handle implies - for target, cond_expr in implies: + for target, cond in implies: # Only stored for convenience. Not used during evaluation. node.item.implies.append( - (target, self._make_and(cond_expr, node.dep))) + (target, self._make_and(cond, node.dep))) # Modify the dependencies of the implied symbol target.weak_rev_dep = \ self._make_or(target.weak_rev_dep, self._make_and(node.item, - self._make_and(cond_expr, + self._make_and(cond, node.dep))) def _parse_expr(self, transform_m): @@ -1559,8 +1558,8 @@ class Config(object): add_fn(config_string) sym._already_written = True - elif eval_expr(node.dep) and \ - ((node.item == MENU and eval_expr(node.visibility)) or + elif expr_value(node.dep) and \ + ((node.item == MENU and expr_value(node.visibility)) or node.item == COMMENT): add_fn("\n#\n# {}\n#\n".format(node.prompt[0])) @@ -1994,8 +1993,8 @@ class Symbol(object): base = _TYPE_TO_BASE[self._type] # Check if a range is in effect - for low_expr, high_expr, cond_expr in self.ranges: - if eval_expr(cond_expr): + for low_expr, high_expr, cond in self.ranges: + if expr_value(cond): has_active_range = True low = int(low_expr.str_value, base) if \ @@ -2020,8 +2019,8 @@ class Symbol(object): else: # No user value or invalid user value. Look at defaults. - for val_expr, cond_expr in self.defaults: - if eval_expr(cond_expr): + for val_expr, cond in self.defaults: + if expr_value(cond): self._write_to_conf = True # Similarly to above, well-formed defaults are @@ -2059,8 +2058,8 @@ class Symbol(object): if vis and self.user_str_value is not None: val = self.user_str_value else: - for val_expr, cond_expr in self.defaults: - if eval_expr(cond_expr): + for val_expr, cond in self.defaults: + if expr_value(cond): self._write_to_conf = True val = val_expr.str_value break @@ -2096,22 +2095,22 @@ class Symbol(object): # (implies) for default, cond in self.defaults: - cond_val = eval_expr(cond) + cond_val = expr_value(cond) if cond_val: - val = min(cond_val, eval_expr(default)) + val = min(cond_val, expr_value(default)) self._write_to_conf = True break # Weak reverse dependencies are only considered if our # direct dependencies are met - if eval_expr(self.direct_dep): - weak_rev_dep_val = eval_expr(self.weak_rev_dep) + if expr_value(self.direct_dep): + weak_rev_dep_val = expr_value(self.weak_rev_dep) if weak_rev_dep_val: val = max(weak_rev_dep_val, val) self._write_to_conf = True # Reverse (select-related) dependencies take precedence - rev_dep_val = eval_expr(self.rev_dep) + rev_dep_val = expr_value(self.rev_dep) if rev_dep_val: val = max(rev_dep_val, val) self._write_to_conf = True @@ -2139,7 +2138,7 @@ class Symbol(object): # 1) If our type is boolean # 2) If our weak_rev_dep (from IMPLY) is y if val == 1 and \ - (self.type == BOOL or eval_expr(self.weak_rev_dep) == 2): + (self.type == BOOL or expr_value(self.weak_rev_dep) == 2): val = 2 self._cached_tri_val = val @@ -2259,38 +2258,51 @@ class Symbol(object): Prints some information about the symbol (including its name, value, visibility, and location(s)) when it is evaluated. """ - fields = [ - "symbol " + self.name, - _TYPENAME[self.type], - 'value "{}"'.format(self.str_value), - "visibility " + TRI_TO_STR[self.visibility], - ] + fields = [] - if self.user_str_value is not None: - fields.append('user value "{}"'.format(self.user_str_value)) + fields.append("symbol " + self.name) + fields.append(_TYPENAME[self.type]) - if self.choice is not None: - fields.append("choice symbol") + for node in self.nodes: + if node.prompt is not None: + fields.append('"{}"'.format(node.prompt[0])) - if self.is_allnoconfig_y: - fields.append("allnoconfig_y") + fields.append('value "{}"'.format(self.str_value)) - if self is self.config.defconfig_list: - fields.append("is the defconfig_list symbol") + if not self.is_constant: + # These aren't helpful to show for constant symbols - if self.env_var is not None: - fields.append("from environment variable " + self.env_var) + if self.user_str_value is not None: + fields.append('user value "{}"'.format(self.user_str_value)) - if self is self.config.modules: - fields.append("is the modules symbol") + fields.append("visibility " + TRI_TO_STR[self.visibility]) - fields.append("direct deps " + TRI_TO_STR[eval_expr(self.direct_dep)]) + if self.choice is not None: + fields.append("choice symbol") + + if self.is_allnoconfig_y: + fields.append("allnoconfig_y") + + if self is self.config.defconfig_list: + fields.append("is the defconfig_list symbol") + + if self.env_var is not None: + fields.append("from environment variable " + self.env_var) + + if self is self.config.modules: + fields.append("is the modules symbol") + + fields.append("direct deps " + + TRI_TO_STR[expr_value(self.direct_dep)]) if self.nodes: for node in self.nodes: fields.append("{}:{}".format(node.filename, node.linenr)) else: - fields.append("undefined") + if self.is_constant: + fields.append("constant") + else: + fields.append("undefined") return "<{}>".format(", ".join(fields)) @@ -2358,11 +2370,11 @@ class Symbol(object): if not vis: return "" - rev_dep_val = eval_expr(self.rev_dep) + rev_dep_val = expr_value(self.rev_dep) if vis == 2: if not rev_dep_val: - if self.type == BOOL or eval_expr(self.weak_rev_dep) == 2: + if self.type == BOOL or expr_value(self.weak_rev_dep) == 2: return "ny" return "nmy" @@ -2371,14 +2383,14 @@ class Symbol(object): # rev_dep_val == 1 - if self.type == BOOL or eval_expr(self.weak_rev_dep) == 2: + if self.type == BOOL or expr_value(self.weak_rev_dep) == 2: return "y" return "my" # vis == 1 if not rev_dep_val: - return "m" if eval_expr(self.weak_rev_dep) != 2 else "y" + return "m" if expr_value(self.weak_rev_dep) != 2 else "y" if rev_dep_val == 2: return "y" @@ -2451,10 +2463,14 @@ class Symbol(object): Invalidates the symbol and all symbols and choices that (possibly indirectly) depend on it """ - self._invalidate() + # Constant symbols must never be invalidated, because they lose their + # value. They never appear as dependencies, but can still be manually + # assigned a user value (and that's OK, though pointless). + if not self.is_constant: + self._invalidate() - for item in self._get_dependent(): - item._invalidate() + for item in self._get_dependent(): + item._invalidate() def _get_dependent(self): """ @@ -2708,8 +2724,8 @@ class Choice(object): """ See the class documentation. """ - for sym, cond_expr in self.defaults: - if eval_expr(cond_expr) and sym.visibility: + for sym, cond in self.defaults: + if expr_value(cond) and sym.visibility: return sym # Otherwise, pick the first visible symbol, if any @@ -2761,19 +2777,38 @@ class Choice(object): """ TODO """ - fields = [ - "choice" if self.name is None else "choice " + self.name, - _TYPENAME[self.type], - "mode " + self.str_value, - "visibility " + TRI_TO_STR[self.visibility], - ] + fields = [] - if self.is_optional: - fields.append("optional") + fields.append("choice" if self.name is None else \ + "choice " + self.name) + fields.append(_TYPENAME[self.type]) + + for node in self.nodes: + if node.prompt is not None: + fields.append('"{}"'.format(node.prompt[0])) + + fields.append("mode " + self.str_value) + + if self.user_str_value is not None: + fields.append('user mode {}'.format(self.user_str_value)) if self.selection is not None: fields.append("{} selected".format(self.selection.name)) + if self.user_selection is not None: + user_sel_str = "{} selected by user" \ + .format(self.user_selection.name) + + if self.selection is not self.user_selection: + user_sel_str += " (overriden)" + + fields.append(user_sel_str) + + fields.append("visibility " + TRI_TO_STR[self.visibility]) + + if self.is_optional: + fields.append("optional") + for node in self.nodes: fields.append("{}:{}".format(node.filename, node.linenr)) @@ -2932,46 +2967,42 @@ class MenuNode(object): ) def __repr__(self): + """ + TODO + """ fields = [] if isinstance(self.item, Symbol): fields.append("menu node for symbol " + self.item.name) - elif isinstance(self.item, Choice): s = "menu node for choice" if self.item.name is not None: s += " " + self.item.name fields.append(s) - elif self.item == MENU: fields.append("menu node for menu") - elif self.item == COMMENT: fields.append("menu node for comment") - elif self.item is None: fields.append("menu node for if (should not appear in the final " " tree)") - else: raise InternalError("unable to determine type in " "MenuNode.__repr__()") - fields.append("{}:{}".format(self.filename, self.linenr)) - if self.prompt is not None: fields.append('prompt "{}" (visibility {})' .format(self.prompt[0], - TRI_TO_STR[eval_expr(self.prompt[1])])) + TRI_TO_STR[expr_value(self.prompt[1])])) if isinstance(self.item, Symbol) and self.is_menuconfig: fields.append("is menuconfig") - fields.append("deps " + TRI_TO_STR[eval_expr(self.dep)]) + fields.append("deps " + TRI_TO_STR[expr_value(self.dep)]) if self.item == MENU: fields.append("'visible if' deps " + \ - TRI_TO_STR[eval_expr(self.visibility)]) + TRI_TO_STR[expr_value(self.visibility)]) if isinstance(self.item, (Symbol, Choice)) and self.help is not None: fields.append("has help") @@ -2982,6 +3013,8 @@ class MenuNode(object): if self.next is not None: fields.append("has next") + fields.append("{}:{}".format(self.filename, self.linenr)) + return "<{}>".format(", ".join(fields)) class KconfigSyntaxError(Exception): @@ -3000,7 +3033,7 @@ class InternalError(Exception): # Public functions # -def eval_expr(expr): +def expr_value(expr): """ TODO """ @@ -3008,18 +3041,18 @@ def eval_expr(expr): return expr.tri_value if expr[0] == AND: - v1 = eval_expr(expr[1]) + v1 = expr_value(expr[1]) # Short-circuit the n case as an optimization (~5% faster # allnoconfig.py and allyesconfig.py, as of writing) - return 0 if not v1 else min(v1, eval_expr(expr[2])) + return 0 if not v1 else min(v1, expr_value(expr[2])) if expr[0] == OR: - v1 = eval_expr(expr[1]) + v1 = expr_value(expr[1]) # Short-circuit the y case as an optimization - return 2 if v1 == 2 else max(v1, eval_expr(expr[2])) + return 2 if v1 == 2 else max(v1, expr_value(expr[2])) if expr[0] == NOT: - return 2 - eval_expr(expr[1]) + return 2 - expr_value(expr[1]) if expr[0] in _RELATIONS: # Implements <, <=, >, >= comparisons as well. These were added to @@ -3096,7 +3129,7 @@ def _get_visibility(sc): for node in sc.nodes: if node.prompt: - vis = max(vis, eval_expr(node.prompt[1])) + vis = max(vis, expr_value(node.prompt[1])) if isinstance(sc, Symbol) and sc.choice is not None: if sc.choice._type == TRISTATE and sc._type != TRISTATE and \ diff --git a/testsuite.py b/testsuite.py index 711a0fb..4af7c19 100644 --- a/testsuite.py +++ b/testsuite.py @@ -56,7 +56,7 @@ def fail(msg=None): global all_passed all_passed = False if msg is not None: - print("Fail: " + msg) + print("fail: " + msg) def verify(cond, msg): if not cond: @@ -587,6 +587,18 @@ choice c = kconfiglib.Config("Kconfiglib/tests/Krepr", warn=False) + verify_repr(c.n, """ + +""") + + verify_repr(c.m, """ + +""") + + verify_repr(c.y, """ + +""") + verify_repr(c.syms["UNDEFINED"], """ """) @@ -596,7 +608,13 @@ choice """) verify_repr(c.syms["VISIBLE"], """ - + +""") + + c.syms["VISIBLE"].set_value("y") + + verify_repr(c.syms["VISIBLE"], """ + """) verify_repr(c.syms["DIR_DEP_N"], """ @@ -612,7 +630,7 @@ choice """) verify_repr(c.syms["CHOICE_1"], """ - + """) verify_repr(c.modules, """ @@ -623,66 +641,72 @@ choice print("Testing Choice.__repr__()") verify_repr(c.named_choices["CHOICE"], """ - + """) c.named_choices["CHOICE"].set_value("y") verify_repr(c.named_choices["CHOICE"], """ - + """) c.syms["CHOICE_2"].set_value("y") verify_repr(c.named_choices["CHOICE"], """ - + +""") + + c.syms["CHOICE_1"].set_value("m") + + verify_repr(c.named_choices["CHOICE"], """ + """) verify_repr(c.syms["CHOICE_HOOK"].nodes[0].next.item, """ - + """) print("Testing MenuNode.__repr__()") verify_repr(c.syms["BASIC"].nodes[0], """ - + """) verify_repr(c.syms["DIR_DEP_N"].nodes[0], """ - + """) verify_repr(c.syms["MULTI_DEF"].nodes[0], """ - + """) verify_repr(c.syms["MULTI_DEF"].nodes[1], """ - + """) verify_repr(c.syms["MENUCONFIG"].nodes[0], """ - + """) verify_repr(c.named_choices["CHOICE"].nodes[0], """ - + """) verify_repr(c.syms["CHOICE_HOOK"].nodes[0].next, """ - + """) verify_repr(c.syms["NO_VISIBLE_IF_HOOK"].nodes[0].next, """ - + """) verify_repr(c.syms["VISIBLE_IF_HOOK"].nodes[0].next, """ - + """) verify_repr(c.syms["COMMENT_HOOK"].nodes[0].next, """ - + """) @@ -869,14 +893,14 @@ g def verify_menu_visibility(menu, no_module_vis, module_vis): c.modules.set_value("n") - menu_vis = kconfiglib.eval_expr(menu.node.dep) + menu_vis = kconfiglib.expr_value(menu.node.dep) verify(menu_vis == no_module_vis, "menu \"{}\" should have visibility '{}' without modules, " "has visibility '{}'" .format(menu.title, no_module_vis, menu_vis)) c.modules.set_value("y") - menu_vis = kconfiglib.eval_expr(menu.node.dep) + menu_vis = kconfiglib.expr_value(menu.node.dep) verify(menu_vis == module_vis, "menu \"{}\" should have visibility '{}' with modules, " "has visibility '{}'". @@ -939,14 +963,14 @@ g def verify_comment_visibility(comment, no_module_vis, module_vis): c.modules.set_value("n") # TODO: uninternalize - comment_vis = kconfiglib.eval_expr(comment.node.dep) + comment_vis = kconfiglib.expr_value(comment.node.dep) verify(comment_vis == no_module_vis, "comment \"{}\" should have visibility '{}' without " "modules, has visibility '{}'". format(comment.text, no_module_vis, comment_vis)) c.modules.set_value("y") - comment_vis = kconfiglib.eval_expr(comment.node.dep) + comment_vis = kconfiglib.expr_value(comment.node.dep) verify(comment_vis == module_vis, "comment \"{}\" should have visibility '{}' with " "modules, has visibility '{}'". @@ -1731,11 +1755,11 @@ def run_compatibility_tests(): # (generated by the C implementation) should be compared to ._config # (generated by us) after each invocation. all_arch_tests = [(test_load, False), - (test_config_absent, True), - (test_call_all, False), + (test_alldefconfig, True), + (test_sanity, False), (test_all_no, True), - (test_all_yes, True), (test_all_no_simpler, True), + (test_all_yes, True), # Needs to report success/failure for each arch/defconfig # combo, hence False. (test_defconfig, False)] @@ -1857,49 +1881,78 @@ def test_all_yes(conf, arch): else: shell("make allyesconfig") -def test_call_all(conf, arch): +def test_sanity(conf, arch): """ - Call all public methods on all symbols, choices, and TODO menu nodes for - all architectures to make sure we never crash or hang. (Nearly all public - methods: some are hard to test like this, but are exercised by other - tests.) + Do sanity checks on each configuration and call all public methods on all + symbols, choices, and menu nodes for all architectures to make sure we + never crash or hang. """ print("For {}...".format(arch)) + conf.modules + conf.defconfig_list conf.defconfig_filename - conf.mainmenu_text conf.enable_undef_warnings() conf.disable_undef_warnings() - conf.disable_warnings() conf.enable_warnings() + conf.disable_warnings() + conf.mainmenu_text conf.unset_values() # Python 2/3 compatible - for _, s in conf.syms.items(): - s.__str__() - s.__repr__() - s.assignable - s.type - s.str_value - s.tri_value - s.visibility - s.unset_value() + for _, sym in conf.syms.items(): + if sym.name != "UNAME_RELEASE": + verify(not sym.is_constant, sym.name + " in 'syms' and constant") + + verify(sym not in conf.const_syms, + sym.name + " in both 'syms' and 'const_syms'") + + for dep in sym._direct_dependents: + verify(not dep.is_constant, + "the constant symbol {} depends on {}" + .format(dep.name, sym.name)) + + sym.__repr__() + sym.__str__() + sym.assignable + conf.disable_warnings() + sym.set_value("y") + conf.enable_warnings() + sym.str_value + sym.tri_value + sym.type + sym.unset_value() + sym.user_str_value + sym.user_tri_value + sym.visibility + + for sym in conf.defined_syms: + verify(sym.nodes, sym.name + " is defined but lacks menu nodes") + + for _, sym in conf.const_syms.items(): + verify(sym.is_constant, + '"{}" is in const_syms but not marked constant' + .format(sym.name)) - # TODO: verify that constant symbols do not: - # 1) have a non-empty dep - # 2) have nodes + verify(not sym.nodes, + '"{}" is constant but has menu nodes' + .format(sym.name)) - # TODO: Look for weird stuff in the dictionaries + verify(not sym._direct_dependents, + '"{}" is constant but is a dependency of some symbol' + .format(sym.name)) - # TODO: infinite recursion action - #for _, s in conf.const_syms.items(): - # s.__str__() - # s.__repr__() - # s.assignable - # s.type - # s.value - # s.visibility - # s.unset_value() + sym.__repr__() + sym.__str__() + sym.assignable + conf.disable_warnings() + sym.set_value("y") + conf.enable_warnings() + sym.str_value + sym.tri_value + sym.type + sym.unset_value() + sym.visibility # Cheat with internals for c in conf._choices: @@ -1907,13 +1960,38 @@ def test_call_all(conf, arch): c.__repr__() c.str_value c.tri_value + c.user_str_value + c.user_tri_value c.assignable c.selection c.default_selection c.type c.visibility -def test_config_absent(conf, arch): + # Menu nodes + + node = conf.top_node + + while 1: + # Everything else should be well exercised elsewhere + node.__repr__() + + if node.list is not None: + node = node.list + + elif node.next is not None: + node = node.next + + else: + while node.parent is not None: + node = node.parent + if node.next is not None: + node = node.next + break + else: + break + +def test_alldefconfig(conf, arch): """ Verify that Kconfiglib generates the same .config as 'make alldefconfig', for each architecture -- cgit v1.2.3 From 7bbaf7e7cf131d83931bfda2d2e8e5d6ef1b235f Mon Sep 17 00:00:00 2001 From: Ulf Magnusson Date: Sat, 28 Oct 2017 05:46:25 +0200 Subject: Add uncommitted test files --- kconfiglib.py | 2 ++ tests/Khelp | 46 ++++++++++++++++++++++++++++++++++++++++++++++ tests/Klocation | 2 ++ tests/Krepr | 2 +- tests/Kstr | 1 + tests/Kwtf | 8 ++++++++ 6 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 tests/Khelp create mode 100644 tests/Kwtf diff --git a/kconfiglib.py b/kconfiglib.py index 3262b43..1bf61a6 100644 --- a/kconfiglib.py +++ b/kconfiglib.py @@ -893,6 +893,8 @@ class Config(object): self._file = self._open(filename) except IOError as e: # Extend the error message a bit in this case + # TODO: broken for top-level Kconfig (because + # self._filename/_linenr isn't set) raise IOError( "{}:{}: {} Also note that e.g. $FOO in a 'source' " "statement does not refer to the environment " diff --git a/tests/Khelp b/tests/Khelp new file mode 100644 index 0000000..fdc81ed --- /dev/null +++ b/tests/Khelp @@ -0,0 +1,46 @@ +config TWO_HELP_STRINGS + help + first help string + + + + +config TWO_HELP_STRINGS + help + second help string + +config NO_BLANK_AFTER_HELP + help + help for + NO_BLANK_AFTER_HELP +choice CHOICE_HELP + help + help for + CHOICE_HELP +endchoice + +config HELP_TERMINATED_BY_COMMENT + bool + help + a + b + c +# + +config TRICKY_HELP + bool + help + + + a + b + c + + d + e + f + + + g + h + i diff --git a/tests/Klocation b/tests/Klocation index 498a372..737a221 100644 --- a/tests/Klocation +++ b/tests/Klocation @@ -28,3 +28,5 @@ config _INCLUDED # Expands to "tests/Klocation_included" source "$EXPANDED_FROM_ENV/Klocation$_INCLUDED" + +config MULTI_DEF diff --git a/tests/Krepr b/tests/Krepr index d886fe3..5a5b8b8 100644 --- a/tests/Krepr +++ b/tests/Krepr @@ -41,7 +41,7 @@ endchoice config CHOICE_HOOK choice - tristate "choice" if n + tristate "optional choice" if n optional endchoice diff --git a/tests/Kstr b/tests/Kstr index 8d74017..e591792 100644 --- a/tests/Kstr +++ b/tests/Kstr @@ -51,6 +51,7 @@ config INT range BAZ QAZ if DEP config MODULES + bool "MODULES" option modules config OPTIONS diff --git a/tests/Kwtf b/tests/Kwtf new file mode 100644 index 0000000..c71e328 --- /dev/null +++ b/tests/Kwtf @@ -0,0 +1,8 @@ +config A + bool + select n + select m + select y + imply n + imply m + imply y -- cgit v1.2.3 From 989e9f77cfe8caabc7ac241572e9b52682901135 Mon Sep 17 00:00:00 2001 From: Ulf Magnusson Date: Mon, 30 Oct 2017 00:50:09 +0100 Subject: Consistently use 0/1/2 for tristate values Easier to work with, allowing e.g. direct comparisons with < and >. Make set_value() take 0, 1, 2 for bool and tristate symbols, and fix other APIs to match. Also: - Add introductions to various concepts in the module docstring. Document some more attributes. Still TODOs. - Rename the Config class to Kconfig. - Escape " and \ in the name of constant symbols when printing them. Also make the (un)escaping 100% consistent with how the C tools do it (\ before non-magic character should be unescaped too). - Clean up the escaping/unescaping code and provide two public escape()/unescape() functions. - Export the original MODULES-independent type in orig_type. It's needed for printing symbols in the reparsable __str__() Kconfig format with just public APIs. - Lots of other minor reorganizing and nits all over. --- examples/allnoconfig.py | 8 +- examples/allnoconfig_simpler.py | 8 +- examples/allyesconfig.py | 22 +- examples/defconfig.py | 2 +- examples/defconfig_oldconfig.py | 10 +- examples/eval_expr.py | 4 +- examples/help_grep.py | 4 +- examples/print_sym_info.py | 63 +- examples/print_tree.py | 4 +- kconfiglib.py | 1566 +++++++++++++++++++++++---------------- testsuite.py | 350 +++++---- 11 files changed, 1188 insertions(+), 853 deletions(-) diff --git a/examples/allnoconfig.py b/examples/allnoconfig.py index 2348da4..be36b47 100644 --- a/examples/allnoconfig.py +++ b/examples/allnoconfig.py @@ -8,7 +8,7 @@ # # $ make [ARCH=] scriptconfig SCRIPT=Kconfiglib/examples/allnoconfig.py -from kconfiglib import Config, Symbol, STR_TO_TRI +from kconfiglib import Kconfig, Symbol, STR_TO_TRI import sys def do_allnoconfig(node): @@ -27,7 +27,7 @@ def do_allnoconfig(node): if (sym.choice is None and not sym.is_allnoconfig_y and sym.assignable and - STR_TO_TRI[sym.assignable[0]] < sym.tri_value): + sym.assignable[0] < sym.tri_value): # Yup, lower it sym.set_value(sym.assignable[0]) @@ -39,12 +39,12 @@ def do_allnoconfig(node): node = node.next -conf = Config(sys.argv[1]) +conf = Kconfig(sys.argv[1]) # Do an initial pass to set 'option allnoconfig_y' symbols to 'y' for sym in conf.defined_syms: if sym.is_allnoconfig_y: - sym.set_value("y") + sym.set_value(2) while 1: # Changing later symbols in the configuration can sometimes allow earlier diff --git a/examples/allnoconfig_simpler.py b/examples/allnoconfig_simpler.py index e732669..59a1bd4 100644 --- a/examples/allnoconfig_simpler.py +++ b/examples/allnoconfig_simpler.py @@ -12,17 +12,17 @@ # Kconfiglib immediately invalidates (flags for recalculation) all (possibly) # dependent symbols when a value is assigned to a symbol, which slows this down # a bit (due to tons of redundant invalidation), but makes any assignment -# pattern safe ("just works"). Config.load_config() instead invalidates all +# pattern safe ("just works"). Kconfig.load_config() instead invalidates all # symbols up front, making it much faster. If you really need to eke out # performance, look at how load_config() does things (which involves internal # APIs that don't invalidate symbols). This has been fast enough for all cases # I've seen so far though (around 3 seconds for this particular script on my # Core i7 2600K, including the initial Kconfig parsing). -from kconfiglib import Config, BOOL, TRISTATE +from kconfiglib import Kconfig, BOOL, TRISTATE import sys -conf = Config(sys.argv[1]) +conf = Kconfig(sys.argv[1]) # Avoid warnings printed by Kconfiglib when assigning a value to a symbol that # has no prompt. Such assignments never have an effect. @@ -30,6 +30,6 @@ conf.disable_warnings() for sym in conf.defined_syms: if sym.type in (BOOL, TRISTATE): - sym.set_value("y" if sym.is_allnoconfig_y else "n") + sym.set_value(2 if sym.is_allnoconfig_y else 0) conf.write_config(".config") diff --git a/examples/allyesconfig.py b/examples/allyesconfig.py index f91b6d7..6e2e065 100644 --- a/examples/allyesconfig.py +++ b/examples/allyesconfig.py @@ -8,20 +8,20 @@ # allyesconfig is a bit more involved than allnoconfig as we need to handle # choices in two different modes: # -# y: One symbol is "y", the rest are "n" -# m: Any number of symbols are "m", the rest are "n" +# y: One symbol is y, the rest are n +# m: Any number of symbols are m, the rest are n # -# Only tristate choices can be in "m" mode. No "m" mode choices seem to appear -# for allyesconfig on the kernel Kconfigs as of 4.14, but we still handle it. +# Only tristate choices can be in m mode. No m mode choices seem to appear for +# allyesconfig on the kernel Kconfigs as of 4.14, but we still handle it. # # Usage: # # $ make [ARCH=] scriptconfig SCRIPT=Kconfiglib/examples/allyesconfig.py -from kconfiglib import Config, Choice, STR_TO_TRI +from kconfiglib import Kconfig, Choice, STR_TO_TRI import sys -conf = Config(sys.argv[1]) +conf = Kconfig(sys.argv[1]) # Collect all the choices in the configuration. Demonstrates how the menu node # tree can be walked iteratively by using the parent pointers. @@ -70,14 +70,14 @@ while 1: for sym in non_choice_syms: # See allnoconfig example. [-1] gives the last (highest) assignable # value. - if sym.assignable and sym.tri_value < STR_TO_TRI[sym.assignable[-1]]: + if sym.assignable and sym.tri_value < sym.assignable[-1]: sym.set_value(sym.assignable[-1]) no_changes = False # Handle choices for choice in choices: - # Handle a choice whose visibility allows it to be in "y" mode + # Handle a choice whose visibility allows it to be in y mode if choice.visibility == 2: selection = choice.default_selection @@ -88,7 +88,7 @@ while 1: selection is not choice.user_selection: # Yup, select it - selection.set_value("y") + selection.set_value(2) no_changes = False # Handle a choice whose visibility only allows it to be in "m" mode. @@ -100,10 +100,10 @@ while 1: # Does the choice have a symbol that can be "m" that we haven't # already set to "m"? - if sym.user_tri_value != 1 and "m" in sym.assignable: + if sym.user_tri_value != 1 and 1 in sym.assignable: # Yup, set it - sym.set_value("m") + sym.set_value(1) no_changes = False if no_changes: diff --git a/examples/defconfig.py b/examples/defconfig.py index ce2bf6e..236db8d 100644 --- a/examples/defconfig.py +++ b/examples/defconfig.py @@ -8,7 +8,7 @@ import kconfiglib import os import sys -conf = kconfiglib.Config(sys.argv[1]) +conf = kconfiglib.Kconfig(sys.argv[1]) if os.path.exists(".config"): print("using existing .config") diff --git a/examples/defconfig_oldconfig.py b/examples/defconfig_oldconfig.py index 98173e7..8e72c8a 100644 --- a/examples/defconfig_oldconfig.py +++ b/examples/defconfig_oldconfig.py @@ -15,7 +15,7 @@ import kconfiglib import sys -conf = kconfiglib.Config(sys.argv[1]) +conf = kconfiglib.Kconfig(sys.argv[1]) # Mirrors defconfig conf.load_config("arch/x86/configs/x86_64_defconfig") @@ -23,15 +23,15 @@ conf.write_config(".config") # Mirrors the first oldconfig conf.load_config(".config") -conf.syms["ETHERNET"].set_value('n') +conf.syms["ETHERNET"].set_value(0) conf.write_config(".config") # Mirrors the second oldconfig conf.load_config(".config") -conf.syms["ETHERNET"].set_value('y') +conf.syms["ETHERNET"].set_value(2) for s in conf: - if s.user_value is None and 'n' in s.assignable: - s.set_value('n') + if s.user_value is None and 0 in s.assignable: + s.set_value(0) # Write the final configuration conf.write_config(".config") diff --git a/examples/eval_expr.py b/examples/eval_expr.py index a907f35..be02c77 100644 --- a/examples/eval_expr.py +++ b/examples/eval_expr.py @@ -13,10 +13,10 @@ if len(sys.argv) < 3: print('Pass symbol name (without "CONFIG_" prefix) with SCRIPT_ARG=NAME') sys.exit(1) -conf = kconfiglib.Config(sys.argv[1]) +conf = kconfiglib.Kconfig(sys.argv[1]) # Enable modules so that 'm' doesn't get demoted to 'n' -conf.syms["MODULES"].set_value("y") +conf.syms["MODULES"].set_value(2) print("the expression '{}' evaluates to {}" .format(sys.argv[2], conf.eval_string(sys.argv[2]))) diff --git a/examples/help_grep.py b/examples/help_grep.py index 6fcb08f..6f54583 100644 --- a/examples/help_grep.py +++ b/examples/help_grep.py @@ -33,7 +33,7 @@ # ... -from kconfiglib import Config, Symbol, Choice, MENU, COMMENT +from kconfiglib import Kconfig, Symbol, Choice, MENU, COMMENT import re import sys @@ -68,5 +68,5 @@ def search_tree(node): node = node.next -conf = Config(sys.argv[1]) +conf = Kconfig(sys.argv[1]) search_tree(conf.top_node) diff --git a/examples/print_sym_info.py b/examples/print_sym_info.py index 2c1b0f0..cc9f50a 100644 --- a/examples/print_sym_info.py +++ b/examples/print_sym_info.py @@ -5,48 +5,49 @@ # $ make [ARCH=] scriptconfig SCRIPT=Kconfiglib/examples/print_sym_info.py SCRIPT_ARG= # # Example output for SCRIPT_ARG=modules: -# -# config MODULES -# bool -# prompt "Enable loadable module support" -# option modules -# help -# Kernel modules are small pieces of compiled code which can -# be inserted in the running kernel, rather than being -# permanently built into the kernel. You use the "modprobe" -# tool to add (and sometimes remove) them. If you say Y here, -# many parts of the kernel can be built as modules (by -# answering M instead of Y where indicated): this is most -# useful for infrequently used options which are not required -# for booting. For more information, see the man pages for -# modprobe, lsmod, modinfo, insmod and rmmod. -# -# If you say Y here, you will need to run "make -# modules_install" to put the modules under /lib/modules/ -# where modprobe can find them (you may need to be root to do -# this). -# -# If unsure, say Y. # -# value = n -# visibility = y -# currently assignable values: n, y -# defined at init/Kconfig:1678 +# menuconfig MODULES +# bool +# prompt "Enable loadable module support" +# option modules +# help +# Kernel modules are small pieces of compiled code which can +# be inserted in the running kernel, rather than being +# permanently built into the kernel. You use the "modprobe" +# tool to add (and sometimes remove) them. If you say Y here, +# many parts of the kernel can be built as modules (by +# answering M instead of Y where indicated): this is most +# useful for infrequently used options which are not required +# for booting. For more information, see the man pages for +# modprobe, lsmod, modinfo, insmod and rmmod. +# +# If you say Y here, you will need to run "make +# modules_install" to put the modules under /lib/modules/ +# where modprobe can find them (you may need to be root to do +# this). +# +# If unsure, say Y. +# +# value = n +# visibility = y +# currently assignable values: n, y +# defined at init/Kconfig:1674 -import kconfiglib +from kconfiglib import Kconfig, TRI_TO_STR import sys if len(sys.argv) < 3: print('Pass symbol name (without "CONFIG_" prefix) with SCRIPT_ARG=') sys.exit(1) -conf = kconfiglib.Config(sys.argv[1]) +conf = Kconfig(sys.argv[1]) sym = conf.syms[sys.argv[2]] print(sym) -print("value = " + sym.value) -print("visibility = " + sym.visibility) -print("currently assignable values: " + ", ".join(sym.assignable)) +print("value = " + sym.str_value) +print("visibility = " + TRI_TO_STR[sym.visibility]) +print("currently assignable values: " + + ", ".join([TRI_TO_STR[v] for v in sym.assignable])) for node in sym.nodes: print("defined at {}:{}".format(node.filename, node.linenr)) diff --git a/examples/print_tree.py b/examples/print_tree.py index 8d18ce9..9d9eac2 100644 --- a/examples/print_tree.py +++ b/examples/print_tree.py @@ -38,7 +38,7 @@ # config GENERIC_IRQ_PROBE # ... -from kconfiglib import Config, Symbol, Choice, MENU, COMMENT +from kconfiglib import Kconfig, Symbol, Choice, MENU, COMMENT import sys def indent_print(s, indent): @@ -63,5 +63,5 @@ def print_items(node, indent): node = node.next -conf = Config(sys.argv[1]) +conf = Kconfig(sys.argv[1]) print_items(conf.top_node, 0) diff --git a/kconfiglib.py b/kconfiglib.py index 1bf61a6..96a421c 100644 --- a/kconfiglib.py +++ b/kconfiglib.py @@ -9,127 +9,290 @@ from Kconfig-based configuration systems. Features include the following: - Reading/writing of .config files - - Inspection of symbol properties: print()ing a symbol (which calls __str__()) - produces output which could be fed back into a Kconfig parser to redefine - the symbol, and __str__() is implemented with only public APIs. + - Inspection of symbol properties and expressions: printing a symbol (calling + Symbol.__str__()) gives output which could be fed back into a Kconfig parser + to redefine the symbol, and __str__() is implemented with only public APIs. - A helpful __repr__() is implemented on all objects as well. + A helpful __repr__() is implemented on all objects as well, also implemented + with public APIs. - - Expression inspection and evaluation: All expressions are exposed and use a - simple tuple-based format that can be processed manually if needed. + - Expressions use a simple tuple-based format and can be processed manually if + needed. - Menu tree inspection: The underlying menu tree is exposed, including submenus created implicitly from symbols depending on preceding symbols. This can be used e.g. to implement menuconfig-like functionality. - Runs under both Python 2 and 3. The code mostly uses basic Python features - (the most advanced things used are probably @property and __slots__). + and has no third-party dependencies (the most advanced things used are + @property and __slots__). - Robust and highly compatible with the standard Kconfig C tools: The test - suite automatically compares the output from Kconfiglib with the output from - the C tools on the real kernel Kconfig and defconfig files for all ARCHes. - The comparison is done by diffing the generated .config files to make sure - they're identical. All tests are expected to pass. + suite automatically compares output from Kconfiglib and the C tools (by + diffing generated .config files) on the real kernel Kconfig and defconfig + files, for all ARCHes. All tests are expected to pass. - A suite of self tests is also included. + A set of selftests is also included. - - Internals that (mostly) mirror the C implementation. A lot can indirectly be - learned about how it works by reading the Kconfiglib documentation and code. + - Not horribly slow despite being a pure Python implementation: Parses the x86 + Kconfigs in about a second on a Core i7 2600K (with a warm file cache). For + long-running jobs, PyPy gives a big performance boost. - - Pretty speedy by pure Python standards: Parses the x86 Kconfigs in about a - second on a Core i7 2600K (with a warm file cache). For long-running jobs, - PyPy gives a large performance boost. + - Internals that (mostly) mirror the C implementation while being simpler to + understand. -Using Kconfiglib on the Linux kernel -==================================== +Using Kconfiglib on the Linux kernel with the Makefile targets +============================================================== For the Linux kernel, a handy interface is provided by the -scripts/kconfig/Makefile patch. +scripts/kconfig/Makefile patch, which adds the following targets: -Use the 'iscriptconfig' target for experimentation. It gives an interactive -Python prompt where the configuration for ARCH has been preloaded. - $ make [ARCH=] [PYTHONCMD=] iscriptconfig +make iscriptconfig +------------------ -To run a script, use the 'scriptconfig' target. +This target gives an interactive Python prompt where the configuration for ARCH +has been preloaded and is available in 'kconf'. - $ make [ARCH=] [PYTHONCMD=] scriptconfig SCRIPT= [SCRIPT_ARG=] +To get a feel for the API, try evaluating and printing the symbols in +kconf.defined_syms, and explore the menu tree starting at kconf.top_node by +following 'next' and 'list' pointers. -PYTHONCMD is the Python interpreter to use. It defaults to "python". +The item contained in the menu node is found in MenuNode.item, and all symbols +and choices have a 'nodes' attribute which gives their menu nodes (usually only +one). + +If you want to look up a symbol by name, use the kconf.syms dictionary. + + +As usual, ARCH= can be passed to 'make' to select the arch. +PYTHONCMD= selects the Python executable to use (default: +"python"). Tip: IronPython (PYTHONCMD=ipython) autocompletion is handy when figuring out -the API. +the API, as it provides autocompletion for attributes. + +make scriptconfig SCRIPT=