Author: pierre
Date: Sat Jun 15 10:25:10 2019
New Revision: 4108

Log:
Update to Kconfiglib version 12.4.0. This removes the need to use
".configuration.old"

Modified:
   jhalfs/trunk/Makefile
   jhalfs/trunk/jhalfs
   jhalfs/trunk/menu/kconfiglib.py
   jhalfs/trunk/menu/menuconfig.py

Modified: jhalfs/trunk/Makefile
==============================================================================
--- jhalfs/trunk/Makefile       Sat Jun 15 08:26:23 2019        (r4107)
+++ jhalfs/trunk/Makefile       Sat Jun 15 10:25:10 2019        (r4108)
@@ -12,12 +12,12 @@
        @$$(grep RUN_ME configuration 2>/dev/null | sed -e 's@RUN_ME=\"@@' -e 
's@\"@@')
 
 menuconfig:
-       @cp -a configuration .configuration.old 2>/dev/null || true
+       @cp -a configuration configuration.old 2>/dev/null || true
        @CONFIG_="" KCONFIG_CONFIG=configuration $(CONFIG)/menuconfig.py 
$(CONFIG_CONFIG_IN)
 
 # Clean up
 
 clean:
-       rm -f configuration .configuration.old error
+       rm -f configuration configuration.old error
 
 .PHONY: all menuconfig clean

Modified: jhalfs/trunk/jhalfs
==============================================================================
--- jhalfs/trunk/jhalfs Sat Jun 15 08:26:23 2019        (r4107)
+++ jhalfs/trunk/jhalfs Sat Jun 15 10:25:10 2019        (r4108)
@@ -131,7 +131,7 @@
 # If the user has not saved his configuration file, let's ask
 # if he or she really wants to run this stuff
 time_current=$(stat -c '%Y' configuration 2>/dev/null || date +%s)
-time_old=$(stat -c '%Y' .configuration.old 2>/dev/null || printf '%s' 
"$time_current")
+time_old=$(stat -c '%Y' configuration.old 2>/dev/null || printf '%s' 
"$time_current")
 if [ "$(printf '%d' "$time_old")" -ge "$(printf '%d' "$time_current")" ] ; then
   printf 'Do you want to run jhalfs? yes/no (yes): '
   read -r ANSWER

Modified: jhalfs/trunk/menu/kconfiglib.py
==============================================================================
--- jhalfs/trunk/menu/kconfiglib.py     Sat Jun 15 08:26:23 2019        (r4107)
+++ jhalfs/trunk/menu/kconfiglib.py     Sat Jun 15 10:25:10 2019        (r4108)
@@ -12,6 +12,11 @@
 See the homepage at https://github.com/ulfalizer/Kconfiglib for a longer
 overview.
 
+Since Kconfiglib 12.0.0, the library version is available in
+kconfiglib.VERSION, which is a (<major>, <minor>, <patch>) tuple, e.g.
+(12, 0, 0).
+
+
 Using Kconfiglib on the Linux kernel with the Makefile targets
 ==============================================================
 
@@ -47,8 +52,17 @@
 make kmenuconfig
 ----------------
 
-This target runs the curses menuconfig interface with Python 3 (Python 2 is
-currently not supported for the menuconfig).
+This target runs the curses menuconfig interface with Python 3. As of
+Kconfiglib 12.2.0, both Python 2 and Python 3 are supported (previously, only
+Python 3 was supported, so this was a backport).
+
+
+make guiconfig
+--------------
+
+This target runs the Tkinter menuconfig interface. Both Python 2 and Python 3
+are supported. To change the Python interpreter used, pass
+PYTHONCMD=<executable> to 'make'. The default is 'python'.
 
 
 make [ARCH=<arch>] iscriptconfig
@@ -56,7 +70,7 @@
 
 This target gives an interactive Python prompt where a Kconfig instance has
 been preloaded and is available in 'kconf'. To change the Python interpreter
-used, pass PYTHONCMD=<executable> to make. The default is "python".
+used, pass PYTHONCMD=<executable> to 'make'. The default is 'python'.
 
 To get a feel for the API, try evaluating and printing the symbols in
 kconf.defined_syms, and explore the MenuNode menu tree starting at
@@ -382,7 +396,7 @@
 -----------------
 
 'source' and 'rsource' accept glob patterns, sourcing all matching Kconfig
-files. They require at least one matching file, throwing a KconfigError
+files. They require at least one matching file, raising a KconfigError
 otherwise.
 
 For example, the following statement might source sub1/foofoofoo and
@@ -437,8 +451,8 @@
     all assignments to undefined symbols within .config files. By default, no
     such warnings are generated.
 
-    This warning can also be enabled/disabled via
-    Kconfig.enable/disable_undef_warnings().
+    This warning can also be enabled/disabled via the Kconfig.warn_assign_undef
+    variable.
 
 
 Preprocessor user functions defined in Python
@@ -529,8 +543,10 @@
 
 # Get rid of some attribute lookups. These are obvious in context.
 from glob import iglob
-from os.path import dirname, exists, expandvars, isabs, islink, join, \
-                    relpath, split
+from os.path import dirname, exists, expandvars, islink, join, realpath
+
+
+VERSION = (12, 4, 0)
 
 
 # File layout:
@@ -619,9 +635,8 @@
       top-level Kconfig file. If a file is source'd multiple times, it will
       appear multiple times. Use set() to get unique filenames.
 
-      Note: Using this for incremental builds is redundant. Kconfig.sync_deps()
-      already indirectly catches any file modifications that change the
-      configuration output.
+      Note that Kconfig.sync_deps() already indirectly catches any file
+      modifications that change configuration output.
 
     env_vars:
       A set() with the names of all environment variables referenced in the
@@ -691,18 +706,55 @@
       A dictionary with all preprocessor variables, indexed by name. See the
       Variable class.
 
+    warn:
+      Set this variable to True/False to enable/disable warnings. See
+      Kconfig.__init__().
+
+      When 'warn' is False, the values of the other warning-related variables
+      are ignored.
+
+      This variable as well as the other warn* variables can be read to check
+      the current warning settings.
+
+    warn_to_stderr:
+      Set this variable to True/False to enable/disable warnings on stderr. See
+      Kconfig.__init__().
+
+    warn_assign_undef:
+      Set this variable to True to generate warnings for assignments to
+      undefined symbols in configuration files.
+
+      This variable is False by default unless the KCONFIG_WARN_UNDEF_ASSIGN
+      environment variable was set to 'y' when the Kconfig instance was
+      created.
+
+    warn_assign_override:
+      Set this variable to True to generate warnings for multiple assignments
+      to the same symbol in configuration files, where the assignments set
+      different values (e.g. CONFIG_FOO=m followed by CONFIG_FOO=y, where the
+      last value would get used).
+
+      This variable is True by default. Disabling it might be useful when
+      merging configurations.
+
+    warn_assign_redun:
+      Like warn_assign_override, but for multiple assignments setting a symbol
+      to the same value.
+
+      This variable is True by default. Disabling it might be useful when
+      merging configurations.
+
     warnings:
-      A list of strings containing all warnings that have been generated. This
-      allows flexibility in how warnings are printed and processed.
+      A list of strings containing all warnings that have been generated, for
+      cases where more flexibility is needed.
 
       See the 'warn_to_stderr' parameter to Kconfig.__init__() and the
-      Kconfig.enable/disable_stderr_warnings() functions as well. Note that
-      warnings still get added to Kconfig.warnings when 'warn_to_stderr' is
-      True.
-
-      Just as for warnings printed to stderr, only optional warnings that are
-      enabled will get added to Kconfig.warnings. See the various
-      Kconfig.enable/disable_*_warnings() functions.
+      Kconfig.warn_to_stderr variable as well. Note that warnings still get
+      added to Kconfig.warnings when 'warn_to_stderr' is True.
+
+      Just as for warnings printed to stderr, only warnings that are enabled
+      will get added to Kconfig.warnings. See the various Kconfig.warn*
+      variables.
 
     missing_syms:
       A list with (name, value) tuples for all assignments to undefined symbols
@@ -741,13 +793,9 @@
         "_encoding",
         "_functions",
         "_set_match",
+        "_srctree_prefix",
         "_unset_match",
-        "_warn_for_no_prompt",
-        "_warn_for_override",
-        "_warn_for_redun_assign",
-        "_warn_for_undef_assign",
-        "_warn_to_stderr",
-        "_warnings_enabled",
+        "_warn_no_prompt",
         "choices",
         "comments",
         "config_prefix",
@@ -769,6 +817,11 @@
         "unique_choices",
         "unique_defined_syms",
         "variables",
+        "warn",
+        "warn_assign_override",
+        "warn_assign_redun",
+        "warn_assign_undef",
+        "warn_to_stderr",
         "warnings",
         "y",
 
@@ -800,8 +853,8 @@
         default warning settings (KCONFIG_WARN_UNDEF and
         KCONFIG_WARN_UNDEF_ASSIGN).
 
-        Raises KconfigError on syntax errors, and (possibly a subclass of)
-        IOError on IO errors ('errno', 'strerror', and 'filename' are
+        Raises KconfigError on syntax/semantic errors, and (possibly a subclass
+        of) IOError on IO errors ('errno', 'strerror', and 'filename' are
         available). Note that IOError can be caught as OSError on Python 3.
 
         filename (default: "Kconfig"):
@@ -820,12 +873,12 @@
 
         warn (default: True):
           True if warnings related to this configuration should be generated.
-          This can be changed later with Kconfig.enable/disable_warnings(). It
+          This can be changed later by setting Kconfig.warn to True/False. It
           is provided as a constructor argument since warnings might be
           generated during parsing.
 
-          See the other Kconfig.enable_*_warnings() functions as well, which
-          enable or suppress certain warnings when warnings are enabled.
+          See the other Kconfig.warn_* variables as well, which enable or
+          suppress certain warnings when warnings are enabled.
 
           All generated warnings are added to the Kconfig.warnings list. See
           the class documentation.
@@ -834,8 +887,8 @@
           True if warnings should be printed to stderr in addition to being
           added to Kconfig.warnings.
 
-          This can be changed later with
-          Kconfig.enable/disable_stderr_warnings().
+          This can be changed later by setting Kconfig.warn_to_stderr to
+          True/False.
 
         encoding (default: "utf-8"):
           The encoding to use when reading and writing files. If None, the
@@ -852,6 +905,11 @@
           Related PEP: https://www.python.org/dev/peps/pep-0538/
         """
         self.srctree = os.environ.get("srctree", "")
+        # A prefix we can reliably strip from glob() results to get a filename
+        # relative to $srctree. relpath() can cause issues for symlinks,
+        # because it assumes symlink/../foo is the same as foo/.
+        self._srctree_prefix = realpath(self.srctree) + os.sep
+
         self.config_prefix = os.environ.get("CONFIG_", "CONFIG_")
 
         # Regular expressions for parsing .config files
@@ -862,11 +920,11 @@
 
         self.warnings = []
 
-        self._warnings_enabled = warn
-        self._warn_to_stderr = warn_to_stderr
-        self._warn_for_undef_assign = \
+        self.warn = warn
+        self.warn_to_stderr = warn_to_stderr
+        self.warn_assign_undef = \
             os.environ.get("KCONFIG_WARN_UNDEF_ASSIGN") == "y"
-        self._warn_for_redun_assign = self._warn_for_override = True
+        self.warn_assign_override = self.warn_assign_redun = True
 
 
         self._encoding = encoding
@@ -1018,7 +1076,7 @@
         self._add_choice_deps()
 
 
-        self._warn_for_no_prompt = True
+        self._warn_no_prompt = True
 
         self.mainmenu_text = self.top_node.prompt[0]
 
@@ -1038,7 +1096,7 @@
 
         return None
 
-    def load_config(self, filename=None, replace=True, verbose=True):
+    def load_config(self, filename=None, replace=True, verbose=None):
         """
         Loads symbol values from a file in the .config format. Equivalent to
         calling Symbol.set_value() to set each of the values.
@@ -1086,50 +1144,55 @@
           If True, all existing user values will be cleared before loading the
           .config. Pass False to merge configurations.
 
-        verbose (default: True):
-          If True and filename is None (automatically infer configuration
-          file), a message will be printed to stdout telling which file got
-          loaded (or that no file got loaded). This is meant to reduce
-          boilerplate in tools.
-
-        Returns True if an existing configuration was loaded (that didn't come
-        from the 'option defconfig_list' symbol), and False otherwise. This is
-        mostly useful in conjunction with filename=None, as True will always be
-        returned otherwise.
+        verbose (default: None):
+          Limited backwards compatibility to prevent crashes. A warning is
+          printed if anything but None is passed.
+
+          Prior to Kconfiglib 12.0.0, this option enabled printing of messages
+          to stdout when 'filename' was None. A message is (always) returned
+          now instead, which is more flexible.
+
+          Will probably be removed in some future version.
+
+        Returns a string with a message saying which file got loaded (or
+        possibly that no file got loaded, when 'filename' is None). This is
+        meant to reduce boilerplate in tools, which can do e.g.
+        print(kconf.load_config()). The returned message distinguishes between
+        loading (replace == True) and merging (replace == False).
         """
-        loaded_existing = True
+        if verbose is not None:
+            _warn_verbose_deprecated("load_config")
+
+        msg = None
         if filename is None:
             filename = standard_config_filename()
-            if exists(filename):
-                if verbose:
-                    print("Using existing configuration '{}' as base"
-                          .format(filename))
-            else:
-                filename = self.defconfig_filename
-                if filename is None:
-                    if verbose:
-                        print("Using default symbol values as base")
-                    return False
+            if not exists(filename) and \
+               not exists(join(self.srctree, filename)):
+                defconfig = self.defconfig_filename
+                if defconfig is None:
+                    return "Using default symbol values (no '{}')" \
+                           .format(filename)
+
+                msg = " default configuration '{}' (no '{}')" \
+                      .format(defconfig, filename)
+                filename = defconfig
 
-                if verbose:
-                    print("Using default configuration found in '{}' as "
-                          "base".format(filename))
-
-                loaded_existing = False
+        if not msg:
+            msg = " configuration '{}'".format(filename)
 
         # Disable the warning about assigning to symbols without prompts. This
         # is normal and expected within a .config file.
-        self._warn_for_no_prompt = False
+        self._warn_no_prompt = False
 
-        # This stub only exists to make sure _warn_for_no_prompt gets reenabled
+        # This stub only exists to make sure _warn_no_prompt gets reenabled
         try:
             self._load_config(filename, replace)
         except UnicodeDecodeError as e:
             _decoding_error(e, filename)
         finally:
-            self._warn_for_no_prompt = True
+            self._warn_no_prompt = True
 
-        return loaded_existing
+        return ("Loaded" if replace else "Merged") + msg
 
     def _load_config(self, filename, replace):
         with self._open_config(filename) as f:
@@ -1250,14 +1313,15 @@
                     else:
                         display_user_val = sym.user_value
 
-                    msg = '{} set more than once. Old value: "{}", new value: 
"{}".'.format(
+                    msg = '{} set more than once. Old value "{}", new value 
"{}".'.format(
                         _name_and_loc(sym), display_user_val, val
                     )
 
                     if display_user_val == val:
-                        self._warn_redun_assign(msg, filename, linenr)
-                    else:
-                        self._warn_override(msg, filename, linenr)
+                        if self.warn_assign_redun:
+                            self._warn(msg, filename, linenr)
+                    elif self.warn_assign_override:
+                        self._warn(msg, filename, linenr)
 
                 sym.set_value(val)
 
@@ -1277,8 +1341,7 @@
         # Called for assignments to undefined symbols during .config loading
 
         self.missing_syms.append((name, val))
-
-        if self._warn_for_undef_assign:
+        if self.warn_assign_undef:
             self._warn(
                 "attempt to assign the value '{}' to the undefined symbol {}"
                 .format(val, name), filename, linenr)
@@ -1293,6 +1356,11 @@
         write_config(). The order in the C implementation depends on the hash
         table implementation as of writing, and so won't match.
 
+        If 'filename' exists and its contents is identical to what would get
+        written out, it is left untouched. This avoids updating file metadata
+        like the modification time and possibly triggering redundant work in
+        build tools.
+
         filename:
           Self-explanatory.
 
@@ -1301,37 +1369,48 @@
           would usually want it enclosed in '/* */' to make it a C comment,
           and include a final terminating newline.
         """
-        with self._open(filename, "w") as f:
-            f.write(header)
+        self._write_if_changed(filename, self._autoconf_contents(header))
 
-            for sym in self.unique_defined_syms:
-                # Note: _write_to_conf is determined when the value is
-                # calculated. This is a hidden function call due to
-                # property magic.
-                val = sym.str_value
-                if sym._write_to_conf:
-                    if sym.orig_type in _BOOL_TRISTATE:
-                        if val != "n":
-                            f.write("#define {}{}{} 1\n"
-                                    .format(self.config_prefix, sym.name,
-                                            "_MODULE" if val == "m" else ""))
+    def _autoconf_contents(self, header):
+        # write_autoconf() helper. Returns the contents to write as a string,
+        # with 'header' at the beginning.
+
+        # "".join()ed later
+        chunks = [header]
+        add = chunks.append
 
-                    elif sym.orig_type is STRING:
-                        f.write('#define {}{} "{}"\n'
-                                .format(self.config_prefix, sym.name,
-                                        escape(val)))
-
-                    else:  # sym.orig_type in _INT_HEX:
-                        if sym.orig_type is HEX and \
-                           not val.startswith(("0x", "0X")):
-                            val = "0x" + val
+        for sym in self.unique_defined_syms:
+            # _write_to_conf is determined when the value is calculated. This
+            # is a hidden function call due to property magic.
+            val = sym.str_value
+            if not sym._write_to_conf:
+                continue
+
+            if sym.orig_type in _BOOL_TRISTATE:
+                if val == "y":
+                    add("#define {}{} 1\n"
+                        .format(self.config_prefix, sym.name))
+                elif val == "m":
+                    add("#define {}{}_MODULE 1\n"
+                        .format(self.config_prefix, sym.name))
+
+            elif sym.orig_type is STRING:
+                add('#define {}{} "{}"\n'
+                    .format(self.config_prefix, sym.name, escape(val)))
+
+            else:  # sym.orig_type in _INT_HEX:
+                if sym.orig_type is HEX and \
+                   not val.startswith(("0x", "0X")):
+                    val = "0x" + val
+
+                add("#define {}{} {}\n"
+                    .format(self.config_prefix, sym.name, val))
 
-                        f.write("#define {}{} {}\n"
-                                .format(self.config_prefix, sym.name, val))
+        return "".join(chunks)
 
     def write_config(self, filename=None,
                      header="# Generated by Kconfiglib 
(https://github.com/ulfalizer/Kconfiglib)\n",
-                     save_old=True, verbose=True):
+                     save_old=True, verbose=None):
         r"""
         Writes out symbol values in the .config format. The format matches the
         C implementation, including ordering.
@@ -1344,10 +1423,15 @@
         See the 'Intro to symbol values' section in the module docstring to
         understand which symbols get written out.
 
+        If 'filename' exists and its contents is identical to what would get
+        written out, it is left untouched. This avoids updating file metadata
+        like the modification time and possibly triggering redundant work in
+        build tools.
+
         filename (default: None):
           Filename to save configuration to (a string).
 
-          If None (the default), the filename in the the environment variable
+          If None (the default), the filename in the environment variable
           KCONFIG_CONFIG is used if set, and ".config" otherwise. See
           standard_config_filename().
 
@@ -1358,43 +1442,117 @@
 
         save_old (default: True):
           If True and <filename> already exists, a copy of it will be saved to
-          .<filename>.old in the same directory before the new configuration is
-          written. The leading dot is added only if the filename doesn't
-          already start with a dot.
-
-          Errors are silently ignored if .<filename>.old cannot be written
-          (e.g. due to being a directory).
-
-        verbose (default: True):
-          If True and filename is None (automatically infer configuration
-          file), a message will be printed to stdout telling which file got
-          written. This is meant to reduce boilerplate in tools.
+          <filename>.old in the same directory before the new configuration is
+          written.
+
+          Errors are silently ignored if <filename>.old cannot be written (e.g.
+          due to being a directory, or <filename> being something like
+          /dev/null).
+
+        verbose (default: None):
+          Limited backwards compatibility to prevent crashes. A warning is
+          printed if anything but None is passed.
+
+          Prior to Kconfiglib 12.0.0, this option enabled printing of messages
+          to stdout when 'filename' was None. A message is (always) returned
+          now instead, which is more flexible.
+
+          Will probably be removed in some future version.
+
+        Returns a string with a message saying which file got saved. This is
+        meant to reduce boilerplate in tools, which can do e.g.
+        print(kconf.write_config()).
         """
+        if verbose is not None:
+            _warn_verbose_deprecated("write_config")
+
         if filename is None:
             filename = standard_config_filename()
-        else:
-            verbose = False
+
+        contents = self._config_contents(header)
+        if self._contents_eq(filename, contents):
+            return "No change to '{}'".format(filename)
 
         if save_old:
             _save_old(filename)
 
         with self._open(filename, "w") as f:
-            f.write(header)
+            f.write(contents)
+
+        return "Configuration saved to '{}'".format(filename)
+
+    def _config_contents(self, header):
+        # write_config() helper. Returns the contents to write as a string,
+        # with 'header' at the beginning.
+        #
+        # More memory friendly would be to 'yield' the strings and
+        # "".join(_config_contents()), but it was a bit slower on my system.
+
+        # node_iter() was used here before commit 3aea9f7 ("Add '# end of
+        # <menu>' after menus in .config"). Those comments get tricky to
+        # implement with it.
+
+        for sym in self.unique_defined_syms:
+            sym._visited = False
+
+        # Did we just print an '# end of ...' comment?
+        after_end_comment = False
+
+        # "".join()ed later
+        chunks = [header]
+        add = chunks.append
+
+        node = self.top_node
+        while 1:
+            # Jump to the next node with an iterative tree walk
+            if node.list:
+                node = node.list
+            elif node.next:
+                node = node.next
+            else:
+                while node.parent:
+                    node = node.parent
+
+                    # Add a comment when leaving visible menus
+                    if node.item is MENU and expr_value(node.dep) and \
+                       expr_value(node.visibility) and \
+                       node is not self.top_node:
+                        add("# end of {}\n".format(node.prompt[0]))
+                        after_end_comment = True
+
+                    if node.next:
+                        node = node.next
+                        break
+                else:
+                    # No more nodes
+                    return "".join(chunks)
 
-            for node in self.node_iter(unique_syms=True):
-                item = node.item
+            # Generate configuration output for the node
 
-                if item.__class__ is Symbol:
-                    f.write(item.config_string)
+            item = node.item
+
+            if item.__class__ is Symbol:
+                if item._visited:
+                    continue
+                item._visited = True
 
-                elif expr_value(node.dep) and \
-                     ((item is MENU and expr_value(node.visibility)) or
-                       item is COMMENT):
+                conf_string = item.config_string
+                if not conf_string:
+                    continue
 
-                    f.write("\n#\n# {}\n#\n".format(node.prompt[0]))
+                if after_end_comment:
+                    # Add a blank line before the first symbol printed after an
+                    # '# end of ...' comment
+                    after_end_comment = False
+                    add("\n")
+                add(conf_string)
+
+            elif expr_value(node.dep) and \
+                 ((item is MENU and expr_value(node.visibility)) or
+                   item is COMMENT):
 
-        if verbose:
-            print("Configuration written to '{}'".format(filename))
+                add("\n#\n# {}\n#\n".format(node.prompt[0]))
+                after_end_comment = False
 
     def write_min_config(self, filename,
                          header="# Generated by Kconfiglib 
(https://github.com/ulfalizer/Kconfiglib)\n"):
@@ -1416,34 +1574,53 @@
           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.
-        """
+
+        Returns a string with a message saying which file got saved. This is
+        meant to reduce boilerplate in tools, which can do e.g.
+        print(kconf.write_min_config()).
+        """
+        contents = self._min_config_contents(header)
+        if self._contents_eq(filename, contents):
+            return "No change to '{}'".format(filename)
+
         with self._open(filename, "w") as f:
-            f.write(header)
+            f.write(contents)
 
-            for sym in self.unique_defined_syms:
-                # Skip symbols that cannot be changed. Only check
-                # non-choice symbols, as selects don't affect choice
-                # symbols.
-                if not sym.choice and \
-                   sym.visibility <= expr_value(sym.rev_dep):
-                    continue
+        return "Minimal configuration saved to '{}'".format(filename)
 
-                # Skip symbols whose value matches their default
-                if sym.str_value == sym._str_default():
-                    continue
+    def _min_config_contents(self, header):
+        # write_min_config() helper. Returns the contents to write as a string,
+        # with 'header' at the beginning.
 
-                # Skip symbols that would be selected by default in a
-                # choice, unless the choice is optional or the symbol type
-                # isn't bool (it might be possible to set the choice mode
-                # to n or the symbol to m in those cases).
-                if sym.choice and \
-                   not sym.choice.is_optional and \
-                   sym.choice._get_selection_from_defaults() is sym and \
-                   sym.orig_type is BOOL and \
-                   sym.tri_value == 2:
-                    continue
+        chunks = [header]
+        add = chunks.append
 
-                f.write(sym.config_string)
+        for sym in self.unique_defined_syms:
+            # Skip symbols that cannot be changed. Only check
+            # non-choice symbols, as selects don't affect choice
+            # symbols.
+            if not sym.choice and \
+               sym.visibility <= expr_value(sym.rev_dep):
+                continue
+
+            # Skip symbols whose value matches their default
+            if sym.str_value == sym._str_default():
+                continue
+
+            # Skip symbols that would be selected by default in a
+            # choice, unless the choice is optional or the symbol type
+            # isn't bool (it might be possible to set the choice mode
+            # to n or the symbol to m in those cases).
+            if sym.choice and \
+               not sym.choice.is_optional and \
+               sym.choice._get_selection_from_defaults() is sym and \
+               sym.orig_type is BOOL and \
+               sym.tri_value == 2:
+                continue
+
+            add(sym.config_string)
+
+        return "".join(chunks)
 
     def sync_deps(self, path):
         """
@@ -1484,6 +1661,11 @@
           3. A new auto.conf with the current symbol values is written, to keep
              track of them for the next build.
 
+             If auto.conf exists and its contents is identical to what would
+             get written out, it is left untouched. This avoids updating file
+             metadata like the modification time and possibly triggering
+             redundant work in build tools.
+
 
         The last piece of the puzzle is knowing what symbols each source file
         depends on. Knowing that, dependencies can be added from source files
@@ -1502,29 +1684,16 @@
         if not exists(path):
             os.mkdir(path, 0o755)
 
-        # This setup makes sure that at least the current working directory
-        # gets reset if things fail
-        prev_dir = os.getcwd()
-        try:
-            # cd'ing into the symbol file directory simplifies
-            # _sync_deps() and saves some work
-            os.chdir(path)
-            self._sync_deps()
-        finally:
-            os.chdir(prev_dir)
-
-    def _sync_deps(self):
         # Load old values from auto.conf, if any
-        self._load_old_vals()
+        self._load_old_vals(path)
 
         for sym in self.unique_defined_syms:
-            # Note: _write_to_conf is determined when the value is
-            # calculated. This is a hidden function call due to
-            # property magic.
+            # _write_to_conf is determined when the value is calculated. This
+            # is a hidden function call due to property magic.
             val = sym.str_value
 
-            # Note: n tristate values do not get written to auto.conf and
-            # autoconf.h, making a missing symbol logically equivalent to n
+            # n tristate values do not get written to auto.conf and autoconf.h,
+            # making a missing symbol logically equivalent to n
 
             if sym._write_to_conf:
                 if sym._old_val is None and \
@@ -1546,31 +1715,16 @@
                 continue
 
             # 'sym' has a new value. Flag it.
-            _touch_dep_file(sym.name)
+            _touch_dep_file(path, sym.name)
 
         # Remember the current values as the "new old" values.
         #
         # This call could go anywhere after the call to _load_old_vals(), but
         # putting it last means _sync_deps() can be safely rerun if it fails
         # before this point.
-        self._write_old_vals()
+        self._write_old_vals(path)
 
-    def _write_old_vals(self):
-        # Helper for writing auto.conf. Basically just a simplified
-        # write_config() that doesn't write any comments (including
-        # '# CONFIG_FOO is not set' comments). The format matches the C
-        # implementation, though the ordering is arbitrary there (depends on
-        # the hash table implementation).
-        #
-        # A separate helper function is neater than complicating write_config()
-        # by passing a flag to it, plus we only need to look at symbols here.
-
-        with self._open("auto.conf", "w") as f:
-            for sym in self.unique_defined_syms:
-                if not (sym.orig_type in _BOOL_TRISTATE and not sym.tri_value):
-                    f.write(sym.config_string)
-
-    def _load_old_vals(self):
+    def _load_old_vals(self, path):
         # Loads old symbol values from auto.conf into a dedicated
         # Symbol._old_val field. Mirrors load_config().
         #
@@ -1581,11 +1735,15 @@
         for sym in self.unique_defined_syms:
             sym._old_val = None
 
-        if not exists("auto.conf"):
-            # No old values
-            return
+        try:
+            auto_conf = self._open(join(path, "auto.conf"), "r")
+        except IOError as e:
+            if e.errno == errno.ENOENT:
+                # No old values
+                return
+            raise
 
-        with self._open("auto.conf", "r") as f:
+        with auto_conf as f:
             for line in f:
                 match = self._set_match(line)
                 if not match:
@@ -1607,7 +1765,30 @@
                 else:
                     # Flag that the symbol no longer exists, in
                     # case something still depends on it
-                    _touch_dep_file(name)
+                    _touch_dep_file(path, name)
+
+    def _write_old_vals(self, path):
+        # Helper for writing auto.conf. Basically just a simplified
+        # write_config() that doesn't write any comments (including
+        # '# CONFIG_FOO is not set' comments). The format matches the C
+        # implementation, though the ordering is arbitrary there (depends on
+        # the hash table implementation).
+        #
+        # A separate helper function is neater than complicating write_config()
+        # by passing a flag to it, plus we only need to look at symbols here.
+
+        self._write_if_changed(
+            os.path.join(path, "auto.conf"),
+            self._old_vals_contents())
+
+    def _old_vals_contents(self):
+        # _write_old_vals() helper. Returns the contents to write as a string.
+
+        # Temporary list instead of generator makes this a bit faster
+        return "".join([
+            sym.config_string for sym in self.unique_defined_syms
+                if not (sym.orig_type in _BOOL_TRISTATE and not sym.tri_value)
+        ])
 
     def node_iter(self, unique_syms=False):
         """
@@ -1685,20 +1866,19 @@
 
         self._filename = None
 
-        # Don't include the "if " from below to avoid giving confusing error
-        # messages
-        self._line = s
         self._tokens = self._tokenize("if " + s)
+        # Strip "if " to avoid giving confusing error messages
+        self._line = s
         self._tokens_i = 1  # Skip the 'if' token
 
         return expr_value(self._expect_expr_and_eol())
 
     def unset_values(self):
         """
-        Resets the user values of all symbols, as if Kconfig.load_config() or
-        Symbol.set_value() had never been called.
+        Removes any user values from all symbols, as if Kconfig.load_config()
+        or Symbol.set_value() had never been called.
         """
-        self._warn_for_no_prompt = False
+        self._warn_no_prompt = False
         try:
             # set_value() already rejects undefined symbols, and they don't
             # need to be invalidated (because their value never changes), so we
@@ -1709,98 +1889,100 @@
             for choice in self.unique_choices:
                 choice.unset_value()
         finally:
-            self._warn_for_no_prompt = True
+            self._warn_no_prompt = True
 
     def enable_warnings(self):
         """
-        See Kconfig.__init__().
+        Do 'Kconfig.warn = True' instead. Maintained for backwards
+        compatibility.
         """
-        self._warnings_enabled = True
+        self.warn = True
 
     def disable_warnings(self):
         """
-        See Kconfig.__init__().
+        Do 'Kconfig.warn = False' instead. Maintained for backwards
+        compatibility.
         """
-        self._warnings_enabled = False
+        self.warn = False
 
     def enable_stderr_warnings(self):
         """
-        See Kconfig.__init__().
+        Do 'Kconfig.warn_to_stderr = True' instead. Maintained for backwards
+        compatibility.
         """
-        self._warn_to_stderr = True
+        self.warn_to_stderr = True
 
     def disable_stderr_warnings(self):
         """
-        See Kconfig.__init__().
+        Do 'Kconfig.warn_to_stderr = False' instead. Maintained for backwards
+        compatibility.
         """
-        self._warn_to_stderr = False
+        self.warn_to_stderr = False
 
     def enable_undef_warnings(self):
         """
-        Enables warnings for assignments to undefined symbols. Disabled by
-        default unless the KCONFIG_WARN_UNDEF_ASSIGN environment variable was
-        set to 'y' when the Kconfig instance was created.
+        Do 'Kconfig.warn_assign_undef = True' instead. Maintained for backwards
+        compatibility.
         """
-        self._warn_for_undef_assign = True
+        self.warn_assign_undef = True
 
     def disable_undef_warnings(self):
         """
-        See enable_undef_assign().
+        Do 'Kconfig.warn_assign_undef = False' instead. Maintained for
+        backwards compatibility.
         """
-        self._warn_for_undef_assign = False
+        self.warn_assign_undef = False
 
     def enable_override_warnings(self):
         """
-        Enables warnings for duplicated assignments in .config files that set
-        different values (e.g. CONFIG_FOO=m followed by CONFIG_FOO=y, where
-        the last value set is used).
-
-        These warnings are enabled by default. Disabling them might be helpful
-        in certain cases when merging configurations.
+        Do 'Kconfig.warn_assign_override = True' instead. Maintained for
+        backwards compatibility.
         """
-        self._warn_for_override = True
+        self.warn_assign_override = True
 
     def disable_override_warnings(self):
         """
-        See enable_override_warnings().
+        Do 'Kconfig.warn_assign_override = False' instead. Maintained for
+        backwards compatibility.
         """
-        self._warn_for_override = False
+        self.warn_assign_override = False
 
     def enable_redun_warnings(self):
         """
-        Enables warnings for duplicated assignments in .config files that all
-        set the same value.
-
-        These warnings are enabled by default. Disabling them might be helpful
-        in certain cases when merging configurations.
+        Do 'Kconfig.warn_assign_redun = True' instead. Maintained for backwards
+        compatibility.
         """
-        self._warn_for_redun_assign = True
+        self.warn_assign_redun = True
 
     def disable_redun_warnings(self):
         """
-        See enable_redun_warnings().
+        Do 'Kconfig.warn_assign_redun = False' instead. Maintained for
+        backwards compatibility.
         """
-        self._warn_for_redun_assign = False
+        self.warn_assign_redun = False
 
     def __repr__(self):
         """
         Returns a string with information about the Kconfig object when it is
         evaluated on e.g. the interactive Python prompt.
         """
+        def status(flag):
+            return "enabled" if flag else "disabled"
+
         return "<{}>".format(", ".join((
             "configuration with {} symbols".format(len(self.syms)),
             'main menu prompt "{}"'.format(self.mainmenu_text),
             "srctree is current directory" if not self.srctree else
                 'srctree "{}"'.format(self.srctree),
             'config symbol prefix "{}"'.format(self.config_prefix),
-            "warnings " +
-                ("enabled" if self._warnings_enabled else "disabled"),
-            "printing of warnings to stderr " +
-                ("enabled" if self._warn_to_stderr else "disabled"),
+            "warnings " + status(self.warn),
+            "printing of warnings to stderr " + status(self.warn_to_stderr),
             "undef. symbol assignment warnings " +
-                ("enabled" if self._warn_for_undef_assign else "disabled"),
+                status(self.warn_assign_undef),
+            "overriding symbol assignment warnings " +
+                status(self.warn_assign_override),
             "redundant symbol assignment warnings " +
-                ("enabled" if self._warn_for_redun_assign else "disabled")
+                status(self.warn_assign_redun)
         )))
 
     #
@@ -1838,17 +2020,23 @@
                            "set to '{}'".format(self.srctree) if self.srctree
                                else "unset or blank"))
 
-    def _enter_file(self, full_filename, rel_filename):
+    def _enter_file(self, filename):
         # Jumps to the beginning of a sourced Kconfig file, saving the previous
         # position and file object.
         #
-        # full_filename:
-        #   Actual path to the file.
-        #
-        # rel_filename:
-        #   File path with $srctree prefix stripped, stored in e.g.
-        #   self._filename (which makes it indirectly show up in
-        #   MenuNode.filename). Equals full_filename for absolute paths.
+        # filename:
+        #   Absolute path to file
+
+        # Path relative to $srctree, stored in e.g. self._filename
+        # (which makes it indirectly show up in MenuNode.filename). Equals
+        # 'filename' for absolute paths passed to 'source'.
+        if filename.startswith(self._srctree_prefix):
+            # Relative path (or a redundant absolute path to within $srctree,
+            # but it's probably fine to reduce those too)
+            rel_filename = filename[len(self._srctree_prefix):]
+        else:
+            # Absolute path
+            rel_filename = filename
 
         self.kconfig_filenames.append(rel_filename)
 
@@ -1883,14 +2071,14 @@
                             "\n".join("{}:{}".format(name, linenr)
                                       for name, linenr in self._include_path)))
 
-        # Note: We already know that the file exists
-
         try:
-            self._readline = self._open(full_filename, "r").readline
+            self._readline = self._open(filename, "r").readline
         except IOError as e:
+            # We already know that the file exists
             raise _KconfigIOError(
-                e, "{}:{}: Could not open '{}' ({}: {})"
-                   .format(self._filename, self._linenr, full_filename,
+                e, "{}:{}: Could not open '{}' (in '{}') ({}: {})"
+                   .format(self._filename, self._linenr, filename,
+                           self._line.strip(),
                            errno.errorcode[e.errno], e.strerror))
 
         self._filename = rel_filename
@@ -1920,8 +2108,8 @@
             # a help text)
             return True
 
-        # Note: readline() returns '' over and over at EOF, which we rely on
-        # for help texts at the end of files (see _line_after_help())
+        # readline() returns '' over and over at EOF, which we rely on for help
+        # texts at the end of files (see _line_after_help())
         line = self._readline()
         if not line:
             return False
@@ -1932,7 +2120,6 @@
             line = line[:-2] + self._readline()
             self._linenr += 1
 
-        self._line = line  # Used for error reporting
         self._tokens = self._tokenize(line)
         # Initialize to 1 instead of 0 to factor out code from _parse_block()
         # and _parse_properties(). They immediately fetch self._tokens[0].
@@ -1954,10 +2141,36 @@
             line = line[:-2] + self._readline()
             self._linenr += 1
 
-        self._line = line
         self._tokens = self._tokenize(line)
         self._reuse_tokens = True
 
+    def _write_if_changed(self, filename, contents):
+        # Writes 'contents' into 'filename', but only if it differs from the
+        # current contents of the file.
+        #
+        # Another variant would be write a temporary file on the same
+        # filesystem, compare the files, and rename() the temporary file if it
+        # differs, but it breaks stuff like write_config("/dev/null"), which is
+        # used out there to force evaluation-related warnings to be generated.
+        # This simple version is pretty failsafe and portable.
+
+        if not self._contents_eq(filename, contents):
+            with self._open(filename, "w") as f:
+                f.write(contents)
+
+    def _contents_eq(self, filename, contents):
+        # Returns True if the contents of 'filename' is 'contents' (a string),
+        # and False otherwise (including if 'filename' can't be opened/read)
+
+        try:
+            with self._open(filename, "r") as f:
+                # Robust re. things like encoding and line endings (mmap()
+                # trickery isn't)
+                return f.read(len(contents) + 1) == contents
+        except IOError:
+            # If the error here would prevent writing the file as well, we'll
+            # notice it later
+            return False
 
     #
     # Tokenization
@@ -2009,9 +2222,11 @@
         # regexes and string operations where possible. This is the biggest
         # hotspot during parsing.
         #
-        # Note: It might be possible to rewrite this to 'yield' tokens instead,
-        # working across multiple lines. The 'option env' lookback thing below
-        # complicates things though.
+        # It might be possible to rewrite this to 'yield' tokens instead,
+        # working across multiple lines. Lookback and compatibility with old
+        # janky versions of the C tools complicate things though.
+
+        self._line = s  # Used for error reporting
 
         # Initial token on the line
         match = _command_match(s)
@@ -2260,7 +2475,6 @@
             return True
         return False
 
-
     #
     # Preprocessor logic
     #
@@ -2518,7 +2732,6 @@
 
         return ""
 
-
     #
     # Parsing
     #
@@ -2622,19 +2835,19 @@
             elif t0 in _SOURCE_TOKENS:
                 pattern = self._expect_str_and_eol()
 
-                # Check if the pattern is absolute and avoid stripping srctree
-                # from it below in that case. We must do the check before
-                # join()'ing, as srctree might be an absolute path.
-                pattern_is_abs = isabs(pattern)
-
                 if t0 in _REL_SOURCE_TOKENS:
                     # Relative source
                     pattern = join(dirname(self._filename), pattern)
 
-                # Sort the glob results to ensure a consistent ordering of
-                # Kconfig symbols, which indirectly ensures a consistent
-                # ordering in e.g. .config files
-                filenames = sorted(iglob(join(self.srctree, pattern)))
+                # - glob() doesn't support globbing relative to a directory, so
+                #   we need to prepend $srctree to 'pattern'. Use join()
+                #   instead of '+' so that an absolute path in 'pattern' is
+                #   preserved.
+                #
+                # - Sort the glob results to ensure a consistent ordering of
+                #   Kconfig symbols, which indirectly ensures a consistent
+                #   ordering in e.g. .config files
+                filenames = sorted(iglob(join(self._srctree_prefix, pattern)))
 
                 if not filenames and t0 in _OBL_SOURCE_TOKENS:
                     raise KconfigError(
@@ -2648,23 +2861,13 @@
                                     if self.srctree else "unset or blank"))
 
                 for filename in filenames:
-                    self._enter_file(
-                        filename,
-                        # Unless an absolute path is passed to *source, strip
-                        # the $srctree prefix from the filename. That way it
-                        # appears without a $srctree prefix in
-                        # MenuNode.filename, which is nice e.g. when generating
-                        # documentation.
-                        filename if pattern_is_abs else
-                            relpath(filename, self.srctree))
-
+                    self._enter_file(filename)
                     prev = self._parse_block(None, parent, prev)
-
                     self._leave_file()
 
             elif t0 is end_token:
-                # We have reached the end of the block. Terminate the final
-                # node and return it.
+                # Reached the end of the block. Terminate the final node and
+                # return it.
 
                 if self._tokens[1] is not None:
                     self._trailing_tokens_error()
@@ -2759,8 +2962,6 @@
 
             elif t0 is _T_MAINMENU:
                 self.top_node.prompt = (self._expect_str_and_eol(), self.y)
-                self.top_node.filename = self._filename
-                self.top_node.linenr = self._linenr
 
             else:
                 # A valid endchoice/endif/endmenu is caught by the 'end_token'
@@ -2953,7 +3154,7 @@
                 return
 
     def _set_type(self, node, new_type):
-        # Note: UNKNOWN == 0, which is falsy
+        # UNKNOWN is falsy
         if node.item.orig_type and node.item.orig_type is not new_type:
             self._warn("{} defined with multiple types, {} will be used"
                        .format(_name_and_loc(node.item),
@@ -3227,7 +3428,6 @@
         for choice in self.unique_choices:
             choice._invalidate()
 
-
     #
     # Post-parsing menu tree processing, including dependency propagation and
     # implicit submenu creation
@@ -3325,18 +3525,15 @@
 
         cur = node.list
         while cur:
-            cur.dep = dep = self._make_and(cur.dep, basedep)
-
-            # Propagate dependencies to prompt
-            if cur.prompt:
-                cur.prompt = (cur.prompt[0],
-                              self._make_and(cur.prompt[1], dep))
+            dep = cur.dep = self._make_and(cur.dep, basedep)
 
             if cur.item.__class__ in _SYMBOL_CHOICE:
-                # Propagate 'visible if' dependencies to the prompt
+                # Propagate 'visible if' and dependencies to the prompt
                 if cur.prompt:
                     cur.prompt = (cur.prompt[0],
-                                  self._make_and(cur.prompt[1], visible_if))
+                                  self._make_and(
+                                      cur.prompt[1],
+                                      self._make_and(visible_if, dep)))
 
                 # Propagate dependencies to defaults
                 if cur.defaults:
@@ -3358,6 +3555,11 @@
                     cur.implies = [(target, self._make_and(cond, dep))
                                    for target, cond in cur.implies]
 
+            elif cur.prompt:  # Not a symbol/choice
+                # Propagate dependencies to the prompt. 'visible if' is only
+                # propagated to symbols/choices.
+                cur.prompt = (cur.prompt[0],
+                              self._make_and(cur.prompt[1], dep))
 
             cur = cur.next
 
@@ -3394,7 +3596,6 @@
                 target.weak_rev_dep,
                 self._make_and(sym, cond))
 
-
     #
     # Misc.
     #
@@ -3570,9 +3771,9 @@
         #   The "U" flag would currently work for both Python 2 and 3, but it's
         #   deprecated on Python 3, so play it future-safe.
         #
-        #   A simpler solution would be to use io.open(), which defaults to
-        #   universal newlines on both Python 2 and 3 (and is an alias for
-        #   open() on Python 3), but it's appreciably slower on Python 2:
+        #   io.open() defaults to universal newlines on Python 2 (and is an
+        #   alias for open() on Python 3), but it returns 'unicode' strings and
+        #   slows things down:
         #
         #     Parsing x86 Kconfigs on Python 2
         #
@@ -3636,37 +3837,25 @@
                sym.name != "MODULES":
 
                 msg = "undefined symbol {}:".format(sym.name)
-
                 for node in self.node_iter():
                     if sym in node.referenced:
                         msg += "\n\n- Referenced at {}:{}:\n\n{}" \
                                .format(node.filename, node.linenr, node)
-
                 self._warn(msg)
 
     def _warn(self, msg, filename=None, linenr=None):
         # For printing general warnings
 
-        if self._warnings_enabled:
-            msg = "warning: " + msg
-            if filename is not None:
-                msg = "{}:{}: {}".format(filename, linenr, msg)
-
-            self.warnings.append(msg)
-            if self._warn_to_stderr:
-                sys.stderr.write(msg + "\n")
-
-    def _warn_override(self, msg, filename, linenr):
-        # See the class documentation
-
-        if self._warn_for_override:
-            self._warn(msg, filename, linenr)
-
-    def _warn_redun_assign(self, msg, filename, linenr):
-        # See the class documentation
+        if not self.warn:
+            return
 
-        if self._warn_for_redun_assign:
-            self._warn(msg, filename, linenr)
+        msg = "warning: " + msg
+        if filename is not None:
+            msg = "{}:{}: {}".format(filename, linenr, msg)
+
+        self.warnings.append(msg)
+        if self.warn_to_stderr:
+            sys.stderr.write(msg + "\n")
 
 
 class Symbol(object):
@@ -3828,7 +4017,7 @@
     ranges:
       List of (low, high, cond) tuples for the symbol's 'range' properties. For
       example, 'range 1 2 if A' is represented as (1, 2, A). If there is no
-      condition, 'cond' is self.config.y.
+      condition, 'cond' is self.kconfig.y.
 
       Note that 'depends on' and parent dependencies are propagated to 'range'
       conditions.
@@ -3849,21 +4038,45 @@
       Like rev_dep, for imply.
 
     direct_dep:
-      The 'depends on' dependencies. If a symbol is defined in multiple
-      locations, the dependencies at each location are ORed together.
+      The direct ('depends on') dependencies for the symbol, or self.kconfig.y
+      if there are no direct dependencies.
+
+      This attribute includes any dependencies from surrounding menus and if's.
+      Those get propagated to the direct dependencies, and the resulting direct
+      dependencies in turn get propagated to the conditions of all properties.
 
-      Internally, this is used to implement 'imply', which only applies if the
-      implied symbol has expr_value(self.direct_dep) != 0. 'depends on' and
-      parent dependencies are automatically propagated to the conditions of
-      properties, so normally it's redundant to check the direct dependencies.
+      If the symbol is defined in multiple locations, the dependencies from the
+      different locations get ORed together.
 
     referenced:
       A set() with all symbols and choices referenced in the properties and
       property conditions of the symbol.
 
-      Also includes dependencies inherited from surrounding menus and if's.
+      Also includes dependencies from surrounding menus and if's, because those
+      get propagated to the symbol (see the 'Intro to symbol values' section in
+      the module docstring).
+
       Choices appear in the dependencies of choice symbols.
 
+      For the following definitions, only B and not C appears in A's
+      'referenced'. To get transitive references, you'll have to recursively
+      expand 'references' until no new items appear.
+
+        config A
+                bool
+                depends on B
+
+        config B
+                bool
+                depends on C
+
+        config C
+                bool
+
+      See the Symbol.direct_dep attribute if you're only interested in the
+      direct dependencies of the symbol (its 'depends on'). You can extract the
+      symbols in it with the global expr_items() function.
+
     env_var:
       If the Symbol has an 'option env="FOO"' option, this contains the name
       ("FOO") of the environment variable. None for symbols without no
@@ -4178,8 +4391,8 @@
         """
         See the class documentation.
         """
-        # Note: _write_to_conf is determined when the value is calculated. This
-        # is a hidden function call due to property magic.
+        # _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 ""
@@ -4286,8 +4499,8 @@
 
     def unset_value(self):
         """
-        Resets the user value of the symbol, as if the symbol had never gotten
-        a user value via Kconfig.load_config() or Symbol.set_value().
+        Removes any user value from the symbol, as if the symbol had never
+        gotten a user value via Kconfig.load_config() or Symbol.set_value().
         """
         if self.user_value is not None:
             self.user_value = None
@@ -4358,8 +4571,9 @@
 
     def __str__(self):
         """
-        Returns a string representation of the symbol when it is printed,
-        matching the Kconfig format, with parent dependencies propagated.
+        Returns a string representation of the symbol when it is printed.
+        Matches the Kconfig format, with any parent dependencies propagated to
+        the 'depends on' condition.
 
         The string is constructed by joining the strings returned by
         MenuNode.__str__() for each of the symbol's menu nodes, so symbols
@@ -4526,7 +4740,7 @@
                 self._rec_invalidate()
                 return
 
-        if self.kconfig._warn_for_no_prompt:
+        if self.kconfig._warn_no_prompt:
             self.kconfig._warn(_name_and_loc(self) + " has no prompt, meaning "
                                "user values have no effect on it")
 
@@ -4721,7 +4935,7 @@
     defaults:
       List of (symbol, cond) tuples for the choice's 'defaults' properties. For
       example, 'default A if B && C' is represented as (A, (AND, B, C)). If
-      there is no condition, 'cond' is self.config.y.
+      there is no condition, 'cond' is self.kconfig.y.
 
       Note that 'depends on' and parent dependencies are propagated to
       'default' conditions.
@@ -4733,7 +4947,9 @@
       A set() with all symbols referenced in the properties and property
       conditions of the choice.
 
-      Also includes dependencies inherited from surrounding menus and if's.
+      Also includes dependencies from surrounding menus and if's, because those
+      get propagated to the choice (see the 'Intro to symbol values' section in
+      the module docstring).
 
     is_optional:
       True if the choice has the 'optional' flag set on it and can be in
@@ -4932,9 +5148,10 @@
 
     def __str__(self):
         """
-        Returns a string representation of the choice when it is printed,
-        matching the Kconfig format (though without the contained choice
-        symbols).
+        Returns a string representation of the choice when it is printed.
+        Matches the Kconfig format (though without the contained choice
+        symbols), with any parent dependencies propagated to the 'depends on'
+        condition.
 
         The returned string does not end in a newline.
 
@@ -5113,6 +5330,18 @@
     ranges:
       Like MenuNode.defaults, for ranges.
 
+    orig_prompt:
+    orig_defaults:
+    orig_selects:
+    orig_implies:
+    orig_ranges:
+      These work the like the corresponding attributes without orig_*, but omit
+      any dependencies propagated from 'depends on' and surrounding 'if's (the
+      direct dependencies, stored in MenuNode.dep).
+
+      One use for this is generating less cluttered documentation, by only
+      showing the direct dependencies in one place.
+
     help:
       The help text for the menu node for Symbols and Choices. None if there is
       no help text. Always stored in the node rather than the Symbol or Choice.
@@ -5124,10 +5353,12 @@
       was undocumented.
 
     dep:
-      The 'depends on' dependencies for the menu node, or self.kconfig.y 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.
+      The direct ('depends on') dependencies for the menu node, or
+      self.kconfig.y if there are no direct dependencies.
+
+      This attribute includes any dependencies from surrounding menus and if's.
+      Those get propagated to the direct dependencies, and the resulting direct
+      dependencies in turn get propagated to the conditions of all properties.
 
       If a symbol or choice is defined in multiple locations, only the
       properties defined at a particular location get the corresponding
@@ -5210,6 +5441,47 @@
         self.ranges = []
 
     @property
+    def orig_prompt(self):
+        """
+        See the class documentation.
+        """
+        if not self.prompt:
+            return None
+        return (self.prompt[0], self._strip_dep(self.prompt[1]))
+
+    @property
+    def orig_defaults(self):
+        """
+        See the class documentation.
+        """
+        return [(default, self._strip_dep(cond))
+                for default, cond in self.defaults]
+
+    @property
+    def orig_selects(self):
+        """
+        See the class documentation.
+        """
+        return [(select, self._strip_dep(cond))
+                for select, cond in self.selects]
+
+    @property
+    def orig_implies(self):
+        """
+        See the class documentation.
+        """
+        return [(imply, self._strip_dep(cond))
+                for imply, cond in self.implies]
+
+    @property
+    def orig_ranges(self):
+        """
+        See the class documentation.
+        """
+        return [(low, high, self._strip_dep(cond))
+                for low, high, cond in self.ranges]
+
+    @property
     def referenced(self):
         """
         See the class documentation.
@@ -5294,8 +5566,9 @@
 
     def __str__(self):
         """
-        Returns a string representation of the menu node, matching the Kconfig
-        format.
+        Returns a string representation of the menu node. Matches the Kconfig
+        format, with any parent dependencies propagated to the 'depends on'
+        condition.
 
         The output could (almost) be fed back into a Kconfig parser to redefine
         the object associated with the menu node. See the module documentation
@@ -5349,13 +5622,20 @@
         else:
             lines = ["choice " + sc.name if sc.name else "choice"]
 
-        if sc.orig_type:  # != UNKNOWN
+        if sc.orig_type and not self.prompt:  # sc.orig_type != UNKNOWN
+            # If there's a prompt, we'll use the '<type> "prompt"' shorthand
+            # instead
             indent_add(TYPE_TO_STR[sc.orig_type])
 
         if self.prompt:
-            indent_add_cond(
-                'prompt "{}"'.format(escape(self.prompt[0])),
-                self.prompt[1])
+            if sc.orig_type:
+                prefix = TYPE_TO_STR[sc.orig_type]
+            else:
+                # Symbol defined without a type (which generates a warning)
+                prefix = "prompt"
+
+            indent_add_cond(prefix + ' "{}"'.format(escape(self.prompt[0])),
+                            self.orig_prompt[1])
 
         if sc.__class__ is Symbol:
             if sc.is_allnoconfig_y:
@@ -5370,13 +5650,13 @@
             if sc is sc.kconfig.modules:
                 indent_add("option modules")
 
-            for low, high, cond in self.ranges:
+            for low, high, cond in self.orig_ranges:
                 indent_add_cond(
                     "range {} {}".format(sc_expr_str_fn(low),
                                          sc_expr_str_fn(high)),
                     cond)
 
-        for default, cond in self.defaults:
+        for default, cond in self.orig_defaults:
             indent_add_cond("default " + expr_str(default, sc_expr_str_fn),
                             cond)
 
@@ -5384,10 +5664,10 @@
             indent_add("optional")
 
         if sc.__class__ is Symbol:
-            for select, cond in self.selects:
+            for select, cond in self.orig_selects:
                 indent_add_cond("select " + sc_expr_str_fn(select), cond)
 
-            for imply, cond in self.implies:
+            for imply, cond in self.orig_implies:
                 indent_add_cond("imply " + sc_expr_str_fn(imply), cond)
 
         if self.dep is not sc.kconfig.y:
@@ -5400,6 +5680,21 @@
 
         return "\n".join(lines)
 
+    def _strip_dep(self, expr):
+        # Helper function for removing MenuNode.dep from 'expr'. Uses two
+        # pieces of internal knowledge: (1) Expressions are reused rather than
+        # copied, and (2) the direct dependencies always appear at the end.
+
+        # ... if dep -> ... if y
+        if self.dep is expr:
+            return self.kconfig.y
+
+        # (AND, X, dep) -> X
+        if expr.__class__ is tuple and expr[0] is AND and expr[2] is self.dep:
+            return expr[1]
+
+        return expr
+
 
 class Variable(object):
     """
@@ -5418,9 +5713,9 @@
       with :=), this will equal 'value'. Accessing this property will raise a
       KconfigError if the expansion seems to be stuck in a loop.
 
-      Note: Accessing this field is the same as calling expanded_value_w_args()
-      with no arguments. I hadn't considered function arguments when adding it.
-      It is retained for backwards compatibility though.
+      Accessing this field is the same as calling expanded_value_w_args() with
+      no arguments. I hadn't considered function arguments when adding it. It
+      is retained for backwards compatibility though.
 
     is_recursive:
       True if the variable is recursive (defined with =).
@@ -5457,7 +5752,12 @@
 
 
 class KconfigError(Exception):
-    "Exception raised for Kconfig-related errors"
+    """
+    Exception raised for Kconfig-related errors.
+
+    KconfigError and KconfigSyntaxError are the same class. The
+    KconfigSyntaxError alias is only maintained for backwards compatibility.
+    """
 
 KconfigSyntaxError = KconfigError  # Backwards compatibility
 
@@ -5552,7 +5852,9 @@
     See expr_str().
     """
     if sc.__class__ is Symbol:
-        return '"{}"'.format(escape(sc.name)) if sc.is_constant else sc.name
+        if sc.is_constant and sc.name not in ("n", "m", "y"):
+            return '"{}"'.format(escape(sc.name))
+        return sc.name
 
     # Choice
     return "<choice {}>".format(sc.name) if sc.name else "<choice>"
@@ -5720,8 +6022,8 @@
     Helper for tools. Returns the value of KCONFIG_CONFIG (which specifies the
     .config file to load/save) if it is set, and ".config" otherwise.
 
-    Note: Calling load_config() with filename=None might give the behavior you
-    want, without having to use this function.
+    Calling load_config() with filename=None might give the behavior you want,
+    without having to use this function.
     """
     return os.environ.get("KCONFIG_CONFIG", ".config")
 
@@ -5733,8 +6035,8 @@
     Linux kernel.
 
     Disables warnings for duplicated assignments within configuration files for
-    the duration of the call (disable_override_warnings() +
-    disable_redun_warnings()), and enables them at the end. The
+    the duration of the call (kconf.warn_assign_override/warn_assign_redun = 
False),
+    and restores the previous warning settings at the end. The
     KCONFIG_ALLCONFIG configuration file is expected to override symbols.
 
     Exits with sys.exit() (which raises a SystemExit exception) and prints an
@@ -5748,38 +6050,39 @@
       Command-specific configuration filename - "allyes.config",
       "allno.config", etc.
     """
+    allconfig = os.environ.get("KCONFIG_ALLCONFIG")
+    if allconfig is None:
+        return
+
     def std_msg(e):
         # "Upcasts" a _KconfigIOError to an IOError, removing the custom
         # __str__() message. The standard message is better here.
         return IOError(e.errno, e.strerror, e.filename)
 
-    kconf.disable_override_warnings()
-    kconf.disable_redun_warnings()
+    old_warn_assign_override = kconf.warn_assign_override
+    old_warn_assign_redun = kconf.warn_assign_redun
+    kconf.warn_assign_override = kconf.warn_assign_redun = False
 
-    allconfig = os.environ.get("KCONFIG_ALLCONFIG")
-    if allconfig is not None:
-        if allconfig in ("", "1"):
-            try:
-                kconf.load_config(filename, False)
-            except IOError as e1:
-                try:
-                    kconf.load_config("all.config", False)
-                except IOError as e2:
-                    sys.exit("error: KCONFIG_ALLCONFIG is set, but neither {} "
-                             "nor all.config could be opened: {}, {}"
-                             .format(filename, std_msg(e1), std_msg(e2)))
-        else:
+    if allconfig in ("", "1"):
+        try:
+            print(kconf.load_config(filename, False))
+        except IOError as e1:
             try:
-                kconf.load_config(allconfig, False)
-            except IOError as e:
-                sys.exit("error: KCONFIG_ALLCONFIG is set to '{}', which "
-                         "could not be opened: {}"
-                         .format(allconfig, std_msg(e)))
-
-    # API wart: It would be nice if there was a way to query and/or push/pop
-    # warning settings
-    kconf.enable_override_warnings()
-    kconf.enable_redun_warnings()
+                print(kconf.load_config("all.config", False))
+            except IOError as e2:
+                sys.exit("error: KCONFIG_ALLCONFIG is set, but neither {} "
+                         "nor all.config could be opened: {}, {}"
+                         .format(filename, std_msg(e1), std_msg(e2)))
+    else:
+        try:
+            print(kconf.load_config(allconfig, False))
+        except IOError as e:
+            sys.exit("error: KCONFIG_ALLCONFIG is set to '{}', which "
+                     "could not be opened: {}"
+                     .format(allconfig, std_msg(e)))
+
+    kconf.warn_assign_override = old_warn_assign_override
+    kconf.warn_assign_redun = old_warn_assign_redun
 
 
 #
@@ -5879,13 +6182,13 @@
            int(sym.str_value, _TYPE_TO_BASE[sym.orig_type])
 
 
-def _touch_dep_file(sym_name):
+def _touch_dep_file(path, sym_name):
     # If sym_name is MY_SYM_NAME, touches my/sym/name.h. See the sync_deps()
     # docstring.
 
-    sym_path = sym_name.lower().replace("_", os.sep) + ".h"
+    sym_path = path + os.sep + sym_name.lower().replace("_", os.sep) + ".h"
     sym_path_dir = dirname(sym_path)
-    if sym_path_dir and not exists(sym_path_dir):
+    if not exists(sym_path_dir):
         os.makedirs(sym_path_dir, 0o755)
 
     # A kind of truncating touch, mirroring the C tools
@@ -5896,52 +6199,35 @@
 def _save_old(path):
     # See write_config()
 
-    dirname, basename = split(path)
-    backup = join(dirname,
-                  basename + ".old" if basename.startswith(".")
-                      else "." + basename + ".old")
+    def copy(src, dst):
+        # Import as needed, to save some startup time
+        import shutil
+        shutil.copyfile(src, dst)
+
+    if islink(path):
+        # Preserve symlinks
+        copy_fn = copy
+    elif hasattr(os, "replace"):
+        # Python 3 (3.3+) only. Best choice when available, because it
+        # removes <filename>.old on both *nix and Windows.
+        copy_fn = os.replace
+    elif os.name == "posix":
+        # Removes <filename>.old on POSIX systems
+        copy_fn = os.rename
+    else:
+        # Fall back on copying
+        copy_fn = copy
 
-    # os.replace() would be nice here, but it's Python 3 (3.3+) only
     try:
-        # Use copyfile() if 'path' is a symlink. The intention is probably to
-        # overwrite the target in that case.
-        if os.name == "posix" and not islink(path):
-            # Will remove .<filename>.old if it already exists on POSIX
-            # systems
-            os.rename(path, backup)
-        else:
-            # Only import as needed, to save some startup time
-            import shutil
-            shutil.copyfile(path, backup)
-    except:
-        # Ignore errors from 'filename' missing as well as other errors. The
-        # backup file is more of a nice-to-have, and not worth erroring out
-        # over e.g. if .<filename>.old happens to be a directory.
+        copy_fn(path, path + ".old")
+    except Exception:
+        # Ignore errors from 'path' missing as well as other errors.
+        # <filename>.old file is usually more of a nice-to-have, and not worth
+        # erroring out over e.g. if <filename>.old happens to be a directory or
+        # <filename> is something like /dev/null.
         pass
 
 
-def _decoding_error(e, filename, macro_linenr=None):
-    # Gives the filename and context for UnicodeDecodeError's, which are a pain
-    # to debug otherwise. 'e' is the UnicodeDecodeError object.
-    #
-    # If the decoding error is for the output of a $(shell,...) command,
-    # macro_linenr holds the line number where it was run (the exact line
-    # number isn't available for decoding errors in files).
-
-    raise KconfigError(
-        "\n"
-        "Malformed {} in {}\n"
-        "Context: {}\n"
-        "Problematic data: {}\n"
-        "Reason: {}".format(
-            e.encoding,
-            "'{}'".format(filename) if macro_linenr is None else
-                "output from macro at {}:{}".format(filename, macro_linenr),
-            e.object[max(e.start - 40, 0):e.end + 40],
-            e.object[e.start:e.end],
-            e.reason))
-
-
 def _name_and_loc(sc):
     # Helper for giving the symbol/choice name and location(s) in e.g. warnings
 
@@ -5994,7 +6280,6 @@
     # node2 has a prompt, we check its condition. Otherwise, we look directly
     # at node2.dep.
 
-    # If node2 has no prompt, use its menu node dependencies instead
     return _expr_depends_on(node2.prompt[1] if node2.prompt else node2.dep,
                             node1.item)
 
@@ -6240,6 +6525,38 @@
     raise KconfigError(msg)
 
 
+def _decoding_error(e, filename, macro_linenr=None):
+    # Gives the filename and context for UnicodeDecodeError's, which are a pain
+    # to debug otherwise. 'e' is the UnicodeDecodeError object.
+    #
+    # If the decoding error is for the output of a $(shell,...) command,
+    # macro_linenr holds the line number where it was run (the exact line
+    # number isn't available for decoding errors in files).
+
+    raise KconfigError(
+        "\n"
+        "Malformed {} in {}\n"
+        "Context: {}\n"
+        "Problematic data: {}\n"
+        "Reason: {}".format(
+            e.encoding,
+            "'{}'".format(filename) if macro_linenr is None else
+                "output from macro at {}:{}".format(filename, macro_linenr),
+            e.object[max(e.start - 40, 0):e.end + 40],
+            e.object[e.start:e.end],
+            e.reason))
+
+
+def _warn_verbose_deprecated(fn_name):
+    sys.stderr.write(
+        "Deprecation warning: {0}()'s 'verbose' argument has no effect. Since "
+        "Kconfiglib 12.0.0, the message is returned from {0}() instead, "
+        "and is always generated. Do e.g. print(kconf.{0}()) if you want to "
+        "want to show a message like \"Loaded configuration '.config'\" on "
+        "stdout. The old API required ugly hacks to reuse messages in "
+        "configuration interfaces.\n".format(fn_name))
+
+
 # Predefined preprocessor functions
 
 
@@ -6332,8 +6649,8 @@
     import platform
     _UNAME_RELEASE = platform.uname()[2]
 
-# Note: The token and type constants below are safe to test with 'is', which is
-# a bit faster (~30% faster on my machine, and a few % faster for total parsing
+# The token and type constants below are safe to test with 'is', which is a bit
+# faster (~30% faster on my machine, and a few % faster for total parsing
 # time), even without assuming Python's small integer optimization (which
 # caches small integer objects). The constants end up pointing to unique
 # integer objects, and since we consistently refer to them via the names below,
@@ -6631,11 +6948,11 @@
 #
 # '$' is included to detect preprocessor variable assignments with macro
 # expansions in the left-hand side.
-_command_match = _re_match(r"\s*([$A-Za-z0-9_-]+)\s*")
+_command_match = _re_match(r"\s*([A-Za-z0-9_$-]+)\s*")
 
 # An identifier/keyword after the first token. Also eats trailing whitespace.
 # '$' is included to detect identifiers containing macro expansions.
-_id_keyword_match = _re_match(r"([$A-Za-z0-9_/.-]+)\s*")
+_id_keyword_match = _re_match(r"([A-Za-z0-9_$/.-]+)\s*")
 
 # A fragment in the left-hand side of a preprocessor variable assignment. These
 # are the portions between macro expansions ($(foo)). Macros are supported in

Modified: jhalfs/trunk/menu/menuconfig.py
==============================================================================
--- jhalfs/trunk/menu/menuconfig.py     Sat Jun 15 08:26:23 2019        (r4107)
+++ jhalfs/trunk/menu/menuconfig.py     Sat Jun 15 10:25:10 2019        (r4108)
@@ -1,4 +1,4 @@
-#!/usr/bin/env python3
+#!/usr/bin/env python
 
 # Copyright (c) 2018-2019, Nordic Semiconductor ASA and Ulf Magnusson
 # SPDX-License-Identifier: ISC
@@ -7,8 +7,8 @@
 Overview
 ========
 
-A curses-based menuconfig implementation. The interface should feel familiar to
-people used to mconf ('make menuconfig').
+A curses-based Python 2/3 menuconfig implementation. The interface should feel
+familiar to people used to mconf ('make menuconfig').
 
 Supports the same keys as mconf, and also supports a set of keybindings
 inspired by Vi:
@@ -20,14 +20,14 @@
   G/End   : Jump to end of list
   g/Home  : Jump to beginning of list
 
+[Space] toggles values if possible, and enters menus otherwise. [Enter] works
+the other way around.
+
 The mconf feature where pressing a key jumps to a menu entry with that
 character in it in the current menu isn't supported. A jump-to feature for
 jumping directly to any symbol (including invisible symbols), choice, menu or
 comment (as in a Kconfig 'comment "Foo"') is available instead.
 
-Space and Enter are "smart" and try to do what you'd expect for the given menu
-entry.
-
 A few different modes are available:
 
   F: Toggle show-help mode, which shows the help text of the currently selected
@@ -55,6 +55,9 @@
 The KCONFIG_CONFIG environment variable specifies the .config file to load (if
 it exists) and save. If KCONFIG_CONFIG is unset, ".config" is used.
 
+When overwriting a configuration file, the old version is saved to
+<filename>.old (e.g. .config.old).
+
 $srctree is supported through Kconfiglib.
 
 
@@ -96,7 +99,7 @@
 
     - fg:COLOR      Set the foreground/background colors. COLOR can be one of
       * or *        the basic 16 colors (black, red, green, yellow, blue,
-    - bg:COLOR      magenta,cyan, white and brighter versions, for example,
+    - bg:COLOR      magenta, cyan, white and brighter versions, for example,
                     brightred). On terminals that support more than 8 colors,
                     you can also directly put in a color number, e.g. fg:123
                     (hexadecimal and octal constants are accepted as well).
@@ -171,19 +174,15 @@
 Limitations
 ===========
 
-  - Python 3 only
-
-    This is mostly due to Python 2 not having curses.get_wch(), which is needed
-    for Unicode support.
-
-  - Doesn't work out of the box on Windows
-
-    Can be made to work with 'pip install windows-curses' though. See the
-    https://github.com/zephyrproject-rtos/windows-curses repository.
+Doesn't work out of the box on Windows, but can be made to work with 'pip
+install windows-curses'. See the
+https://github.com/zephyrproject-rtos/windows-curses repository.
 
-    'pip install kconfiglib' on Windows automatically installs windows-curses
-    to make the menuconfig usable.
+'pip install kconfiglib' on Windows automatically installs windows-curses
+to make the menuconfig usable.
 """
+from __future__ import print_function
+
 import curses
 import errno
 import locale
@@ -193,7 +192,7 @@
 import textwrap
 
 from kconfiglib import Symbol, Choice, MENU, COMMENT, MenuNode, \
-                       BOOL, TRISTATE, STRING, INT, HEX, UNKNOWN, \
+                       BOOL, TRISTATE, STRING, INT, HEX, \
                        AND, OR, \
                        expr_str, expr_value, split_expr, \
                        standard_sc_expr_str, \
@@ -244,7 +243,7 @@
 
 # Lines of help text shown at the bottom of the information dialog
 _INFO_HELP_LINES = """
-[ESC/q] Return to menu       [/] Jump to symbol
+[ESC/q] Return to menu      [/] Jump to symbol
 """[1:-1].split("\n")
 
 # Lines of help text shown at the bottom of the search dialog
@@ -628,7 +627,6 @@
 #
 
 
-# Used as the entry point in setup.py
 def _main():
     menuconfig(standard_kconfig())
 
@@ -648,12 +646,12 @@
 
     _kconf = kconf
 
-    # Load existing configuration and set _conf_changed True if it is outdated
-    _conf_changed = _load_config()
-
     # Filename to save configuration to
     _conf_filename = standard_config_filename()
 
+    # Load existing configuration and set _conf_changed True if it is outdated
+    _conf_changed = _load_config()
+
     # Filename to save minimal configuration to
     _minconf_filename = "defconfig"
 
@@ -671,7 +669,7 @@
 
     # Disable warnings. They get mangled in curses mode, and we deal with
     # errors ourselves.
-    kconf.disable_warnings()
+    kconf.warn = False
 
     # Make curses use the locale settings specified in the environment
     locale.setlocale(locale.LC_ALL, "")
@@ -708,7 +706,8 @@
     # Returns True if .config is missing or outdated. We always prompt for
     # saving the configuration in that case.
 
-    if not _kconf.load_config():
+    print(_kconf.load_config())
+    if not os.path.exists(_conf_filename):
         # No .config
         return True
 
@@ -728,7 +727,7 @@
             if sym.config_string:
                 # Unwritten symbol
                 return True
-        elif sym.type in (BOOL, TRISTATE):
+        elif sym.orig_type in (BOOL, TRISTATE):
             if sym.tri_value != sym.user_value:
                 # Written bool/tristate symbol, new value
                 return True
@@ -769,7 +768,7 @@
 #     If True, the corresponding mode is on. See the module docstring.
 #
 #   _conf_filename:
-#     .config file to save the configuration to
+#     File to save the configuration to
 #
 #   _minconf_filename:
 #     File to save minimal configurations to
@@ -801,7 +800,7 @@
         curses.doupdate()
 
 
-        c = _get_wch_compat(_menu_win)
+        c = _getch_compat(_menu_win)
 
         if c == curses.KEY_RESIZE:
             _resize_main()
@@ -828,26 +827,17 @@
         elif c in (curses.KEY_HOME, "g"):
             _select_first_menu_entry()
 
-        elif c in (curses.KEY_RIGHT, " ", "\n", "l", "L"):
-            # Do appropriate node action. Only Space is treated specially,
-            # preferring to toggle nodes rather than enter menus.
-
+        elif c == " ":
+            # Toggle the node if possible
             sel_node = _shown[_sel_node_i]
-
-            if sel_node.is_menuconfig and not \
-               (c == " " and _prefer_toggle(sel_node.item)):
-
+            if not _change_node(sel_node):
                 _enter_menu(sel_node)
 
-            else:
+        elif c in (curses.KEY_RIGHT, "\n", "l", "L"):
+            # Enter the node if possible
+            sel_node = _shown[_sel_node_i]
+            if not _enter_menu(sel_node):
                 _change_node(sel_node)
-                if _is_y_mode_choice_sym(sel_node.item) and not sel_node.list:
-                    # Immediately jump to the parent menu after making a choice
-                    # selection, like 'make menuconfig' does, except if the
-                    # menu node has children (which can happen if a symbol
-                    # 'depends on' a choice symbol that immediately precedes
-                    # it).
-                    _leave_menu()
 
         elif c in ("n", "N"):
             _set_sel_node_tri_val(0)
@@ -929,8 +919,10 @@
             return None
 
         if c == "y":
-            if _try_save(_kconf.write_config, _conf_filename, "configuration"):
-                return "Configuration saved to '{}'".format(_conf_filename)
+            # Returns a message to print
+            msg = _try_save(_kconf.write_config, _conf_filename, 
"configuration")
+            if msg:
+                return msg
 
         elif c == "n":
             return "Configuration ({}) was not saved".format(_conf_filename)
@@ -961,10 +953,12 @@
     # Looking for this in addition to KEY_BACKSPACE (which is unreliable) makes
     # backspace work with TERM=vt100. That makes it likely to work in sane
     # environments.
-    #
-    # erasechar() returns a 'bytes' object. Since we use get_wch(), we need to
-    # decode it. Just give up and avoid crashing if it can't be decoded.
-    _ERASE_CHAR = curses.erasechar().decode("utf-8", "ignore")
+    _ERASE_CHAR = curses.erasechar()
+    if sys.version_info[0] >= 3:
+        # erasechar() returns a one-byte bytes object on Python 3. This sets
+        # _ERASE_CHAR to a blank string if it can't be decoded, which should be
+        # harmless.
+        _ERASE_CHAR = _ERASE_CHAR.decode("utf-8", "ignore")
 
     _init_styles()
 
@@ -1059,39 +1053,40 @@
     return win.getmaxyx()[1]
 
 
-def _prefer_toggle(item):
-    # For nodes with menus, determines whether Space should change the value of
-    # the node's item or enter its menu. We toggle symbols (which have menus
-    # when they're defined with 'menuconfig') and choices that can be in more
-    # than one mode (e.g. optional choices). In other cases, we enter the menu.
-
-    return isinstance(item, Symbol) or \
-           (isinstance(item, Choice) and len(item.assignable) > 1)
-
-
 def _enter_menu(menu):
-    # Makes 'menu' the currently displayed menu. "Menu" here includes choices
-    # and symbols defined with the 'menuconfig' keyword.
+    # Makes 'menu' the currently displayed menu. In addition to actual 'menu's,
+    # "menu" here includes choices and symbols defined with the 'menuconfig'
+    # keyword.
+    #
+    # Returns False if 'menu' can't be entered.
 
     global _cur_menu
     global _shown
     global _sel_node_i
     global _menu_scroll
 
+    if not menu.is_menuconfig:
+        # Not a menu
+        return False
+
     shown_sub = _shown_nodes(menu)
     # Never enter empty menus. We depend on having a current node.
-    if shown_sub:
-        # Remember where the current node appears on the screen, so we can try
-        # to get it to appear in the same place when we leave the menu
-        _parent_screen_rows.append(_sel_node_i - _menu_scroll)
-
-        # Jump into menu
-        _cur_menu = menu
-        _shown = shown_sub
-        _sel_node_i = _menu_scroll = 0
+    if not shown_sub:
+        return False
+
+    # Remember where the current node appears on the screen, so we can try
+    # to get it to appear in the same place when we leave the menu
+    _parent_screen_rows.append(_sel_node_i - _menu_scroll)
+
+    # Jump into menu
+    _cur_menu = menu
+    _shown = shown_sub
+    _sel_node_i = _menu_scroll = 0
+
+    if isinstance(menu.item, Choice):
+        _select_selected_choice_sym()
 
-        if isinstance(menu.item, Choice):
-            _select_selected_choice_sym()
+    return True
 
 
 def _select_selected_choice_sym():
@@ -1225,7 +1220,7 @@
         _sel_node_i -= 1
 
         # See _select_next_menu_entry()
-        if _sel_node_i <= _menu_scroll + _SCROLL_OFFSET:
+        if _sel_node_i < _menu_scroll + _SCROLL_OFFSET:
             _menu_scroll = max(_menu_scroll - 1, 0)
 
 
@@ -1418,7 +1413,7 @@
 
     _path_win.erase()
 
-    # Draw the menu path ("(top menu) -> menu -> submenu -> ...")
+    # Draw the menu path ("(Top) -> Menu -> Submenu -> ...")
 
     menu_prompts = []
 
@@ -1430,7 +1425,7 @@
         menu_prompts.append(menu.prompt[0] if menu.prompt else
                             standard_sc_expr_str(menu.item))
         menu = menu.parent
-    menu_prompts.append("(top menu)")
+    menu_prompts.append("(Top)")
     menu_prompts.reverse()
 
     # Hack: We can't put ACS_RARROW directly in the string. Temporarily
@@ -1471,10 +1466,6 @@
         res = []
 
         while node:
-            # This code is minorly performance-sensitive. Make it too slow
-            # (e.g., by always recursing the entire tree), and going in and out
-            # of menus no longer feels instant.
-
             if _visible(node) or _show_all:
                 res.append(node)
                 if node.list and not node.is_menuconfig:
@@ -1483,14 +1474,11 @@
                     # menus and choices as well as 'menuconfig' symbols.
                     res += rec(node.list)
 
-            elif node.list and isinstance(node.item, Symbol) and \
-                 expr_value(node.dep):
+            elif node.list and isinstance(node.item, Symbol):
                 # Show invisible symbols if they have visible children. This
                 # can happen for an m/y-valued symbol with an optional prompt
-                # ('prompt "foo" is COND') that is currently disabled. The
-                # expr_value(node.dep) check safely prunes the search: A node
-                # with unsatisfied direct dependencies can never have visible
-                # children.
+                # ('prompt "foo" is COND') that is currently disabled. Note
+                # that it applies to both 'config' and 'menuconfig' symbols.
                 shown_children = rec(node.list)
                 if shown_children:
                     res.append(node)
@@ -1553,36 +1541,32 @@
     # Changes the value of the menu node 'node' if it is a symbol. Bools and
     # tristates are toggled, while other symbol types pop up a text entry
     # dialog.
+    #
+    # Returns False if the value of 'node' can't be changed.
 
-    if not isinstance(node.item, (Symbol, Choice)):
-        return
-
-    # This will hit for invisible symbols, which appear in show-all mode and
-    # when an invisible symbol has visible children (which can happen e.g. for
-    # symbols with optional prompts)
-    if not (node.prompt and expr_value(node.prompt[1])):
-        return
+    if not _changeable(node):
+        return False
 
     # sc = symbol/choice
     sc = node.item
 
-    if sc.type in (INT, HEX, STRING):
+    if sc.orig_type in (INT, HEX, STRING):
         s = sc.str_value
 
         while True:
             s = _input_dialog(
-                "{} ({})".format(node.prompt[0], TYPE_TO_STR[sc.type]),
+                "{} ({})".format(node.prompt[0], TYPE_TO_STR[sc.orig_type]),
                 s, _range_info(sc))
 
             if s is None:
                 break
 
-            if sc.type in (INT, HEX):
+            if sc.orig_type in (INT, HEX):
                 s = s.strip()
 
                 # 'make menuconfig' does this too. Hex values not starting with
                 # '0x' are accepted when loading .config files though.
-                if sc.type == HEX and not s.startswith(("0x", "0X")):
+                if sc.orig_type == HEX and not s.startswith(("0x", "0X")):
                     s = "0x" + s
 
             if _check_valid(sc, s):
@@ -1594,13 +1578,42 @@
         # case: .assignable can be (2,) while .tri_value is 0.
         _set_val(sc, sc.assignable[0])
 
-    elif sc.assignable:
+    else:
         # Set the symbol to the value after the current value in
         # sc.assignable, with wrapping
         val_index = sc.assignable.index(sc.tri_value)
         _set_val(sc, sc.assignable[(val_index + 1) % len(sc.assignable)])
 
 
+    if _is_y_mode_choice_sym(sc) and not node.list:
+        # Immediately jump to the parent menu after making a choice selection,
+        # like 'make menuconfig' does, except if the menu node has children
+        # (which can happen if a symbol 'depends on' a choice symbol that
+        # immediately precedes it).
+        _leave_menu()
+
+
+    return True
+
+
+def _changeable(node):
+    # Returns True if the value if 'node' can be changed
+
+    sc = node.item
+
+    if not isinstance(sc, (Symbol, Choice)):
+        return False
+
+    # This will hit for invisible symbols, which appear in show-all mode and
+    # when an invisible symbol has visible children (which can happen e.g. for
+    # symbols with optional prompts)
+    if not (node.prompt and expr_value(node.prompt[1])):
+        return False
+
+    return sc.orig_type in (STRING, INT, HEX) or len(sc.assignable) > 1 \
+        or _is_y_mode_choice_sym(sc)
+
+
 def _set_sel_node_tri_val(tri_val):
     # Sets the value of the currently selected menu entry to 'tri_val', if that
     # value can be assigned
@@ -1702,7 +1715,7 @@
         curses.doupdate()
 
 
-        c = _get_wch_compat(win)
+        c = _getch_compat(win)
 
         if c == curses.KEY_RESIZE:
             # Resize the main display too. The dialog floats above it.
@@ -1846,29 +1859,33 @@
 
         filename = os.path.expanduser(filename)
 
-        if _try_save(save_fn, filename, description):
-            _msg("Success", "{} saved to {}".format(description, filename))
+        msg = _try_save(save_fn, filename, description)
+        if msg:
+            _msg("Success", msg)
             return filename
 
 
 def _try_save(save_fn, filename, description):
-    # Tries to save a configuration file. Pops up an error and returns False on
-    # failure.
+    # Tries to save a configuration file. Returns a message to print on
+    # success.
     #
     # save_fn:
     #   Function to call with 'filename' to save the file
     #
     # description:
     #   String describing the thing being saved
+    #
+    # Return value:
+    #   A message to print on success, and None on failure
 
     try:
-        save_fn(filename)
-        return True
+        # save_fn() returns a message to print
+        return save_fn(filename)
     except OSError as e:
         _error("Error saving {} to '{}'\n\n{} (errno: {})"
                .format(description, e.filename, e.strerror,
                        errno.errorcode[e.errno]))
-        return False
+        return None
 
 
 def _key_dialog(title, text, keys):
@@ -1902,7 +1919,7 @@
         curses.doupdate()
 
 
-        c = _get_wch_compat(win)
+        c = _getch_compat(win)
 
         if c == curses.KEY_RESIZE:
             # Resize the main display too. The dialog floats above it.
@@ -2000,30 +2017,29 @@
 
     _safe_curs_set(2)
 
-    # TODO: Code duplication with _select_{next,prev}_menu_entry(). Can this be
-    # factored out in some nice way?
+    # Logic duplication with _select_{next,prev}_menu_entry(), except we do a
+    # functional variant that returns the new (sel_node_i, scroll) values to
+    # avoid 'nonlocal'. TODO: Can this be factored out in some nice way?
 
     def select_next_match():
-        nonlocal sel_node_i
-        nonlocal scroll
+        if sel_node_i == len(matches) - 1:
+            return sel_node_i, scroll
 
-        if sel_node_i < len(matches) - 1:
-            sel_node_i += 1
+        if sel_node_i + 1 >= scroll + _height(matches_win) - _SCROLL_OFFSET \
+           and scroll < _max_scroll(matches, matches_win):
 
-            if sel_node_i >= scroll + _height(matches_win) - _SCROLL_OFFSET \
-               and scroll < _max_scroll(matches, matches_win):
+            return sel_node_i + 1, scroll + 1
 
-                scroll += 1
+        return sel_node_i + 1, scroll
 
     def select_prev_match():
-        nonlocal sel_node_i
-        nonlocal scroll
+        if sel_node_i == 0:
+            return sel_node_i, scroll
 
-        if sel_node_i > 0:
-            sel_node_i -= 1
+        if sel_node_i - 1 < scroll + _SCROLL_OFFSET:
+            return sel_node_i - 1, max(scroll - 1, 0)
 
-            if sel_node_i <= scroll + _SCROLL_OFFSET:
-                scroll = max(scroll - 1, 0)
+        return sel_node_i - 1, scroll
 
     while True:
         if s != prev_s:
@@ -2099,7 +2115,7 @@
         curses.doupdate()
 
 
-        c = _get_wch_compat(edit_box)
+        c = _getch_compat(edit_box)
 
         if c == "\n":
             if matches:
@@ -2130,21 +2146,21 @@
                     sel_node_i, scroll)
 
         elif c == curses.KEY_DOWN:
-            select_next_match()
+            sel_node_i, scroll = select_next_match()
 
         elif c == curses.KEY_UP:
-            select_prev_match()
+            sel_node_i, scroll = select_prev_match()
 
         elif c in (curses.KEY_NPAGE, "\x04"):  # Page Down/Ctrl-D
             # Keep it simple. This way we get sane behavior for small windows,
             # etc., for free.
             for _ in range(_PG_JUMP):
-                select_next_match()
+                sel_node_i, scroll = select_next_match()
 
         # Page Up (no Ctrl-U, as it's already used by the edit box)
         elif c == curses.KEY_PPAGE:
             for _ in range(_PG_JUMP):
-                select_prev_match()
+                sel_node_i, scroll = select_prev_match()
 
         elif c == curses.KEY_END:
             sel_node_i = len(matches) - 1
@@ -2364,7 +2380,7 @@
         curses.doupdate()
 
 
-        c = _get_wch_compat(text_win)
+        c = _getch_compat(text_win)
 
         if c == curses.KEY_RESIZE:
             _resize_info_dialog(top_line_win, text_win, bot_sep_win, help_win)
@@ -2600,8 +2616,7 @@
 
     for node in sc.nodes:
         if node.help is not None:
-            s += "Help:\n\n{}\n\n" \
-                 .format(textwrap.indent(node.help, "  "))
+            s += "Help:\n\n{}\n\n".format(_indent(node.help, 2))
 
     return s
 
@@ -2670,7 +2685,7 @@
 
     s = ""
     for i, term in enumerate(split_expr(expr, split_op)):
-        s += "{}{} {}".format(" "*indent,
+        s += "{}{} {}".format(indent*" ",
                               "  " if i == 0 else op_str,
                               _expr_str(term))
 
@@ -2689,34 +2704,34 @@
     # 'sym'. The selecting/implying symbols are grouped according to which
     # value they select/imply 'sym' to (n/m/y).
 
-    s = ""
-
-    def add_sis(expr, val, title):
-        nonlocal s
-
+    def sis(expr, val, title):
         # sis = selects/implies
         sis = [si for si in split_expr(expr, OR) if expr_value(si) == val]
-        if sis:
-            s += title
-            for si in sis:
-                s += "  - {}\n".format(split_expr(si, AND)[0].name)
-            s += "\n"
+        if not sis:
+            return ""
+
+        res = title
+        for si in sis:
+            res += "  - {}\n".format(split_expr(si, AND)[0].name)
+        return res + "\n"
+
+    s = ""
 
     if sym.rev_dep is not _kconf.n:
-        add_sis(sym.rev_dep, 2,
-                "Symbols currently y-selecting this symbol:\n")
-        add_sis(sym.rev_dep, 1,
-                "Symbols currently m-selecting this symbol:\n")
-        add_sis(sym.rev_dep, 0,
-                "Symbols currently n-selecting this symbol (no effect):\n")
+        s += sis(sym.rev_dep, 2,
+                 "Symbols currently y-selecting this symbol:\n")
+        s += sis(sym.rev_dep, 1,
+                 "Symbols currently m-selecting this symbol:\n")
+        s += sis(sym.rev_dep, 0,
+                 "Symbols currently n-selecting this symbol (no effect):\n")
 
     if sym.weak_rev_dep is not _kconf.n:
-        add_sis(sym.weak_rev_dep, 2,
-                "Symbols currently y-implying this symbol:\n")
-        add_sis(sym.weak_rev_dep, 1,
-                "Symbols currently m-implying this symbol:\n")
-        add_sis(sym.weak_rev_dep, 0,
-                "Symbols currently n-implying this symbol (no effect):\n")
+        s += sis(sym.weak_rev_dep, 2,
+                 "Symbols currently y-implying this symbol:\n")
+        s += sis(sym.weak_rev_dep, 1,
+                 "Symbols currently m-implying this symbol:\n")
+        s += sis(sym.weak_rev_dep, 0,
+                 "Symbols currently n-implying this symbol (no effect):\n")
 
     return s
 
@@ -2727,7 +2742,7 @@
 
     nodes = [item] if isinstance(item, MenuNode) else item.nodes
 
-    s = "Kconfig definition{}, with propagated dependencies\n" \
+    s = "Kconfig definition{}, with parent deps. propagated to 'depends on'\n" 
\
         .format("s" if len(nodes) > 1 else "")
     s += (len(s) - 1)*"="
 
@@ -2740,7 +2755,7 @@
              .format(node.filename, node.linenr,
                      _include_path_info(node),
                      _menu_path_info(node),
-                     textwrap.indent(node.custom_str(_name_and_val_str), "  "))
+                     _indent(node.custom_str(_name_and_val_str), 2))
 
     return s
 
@@ -2769,7 +2784,14 @@
         path = " -> " + (node.prompt[0] if node.prompt else
                          standard_sc_expr_str(node.item)) + path
 
-    return "(top menu)" + path
+    return "(Top)" + path
+
+
+def _indent(s, n):
+    # Returns 's' with each line indented 'n' spaces. textwrap.indent() is not
+    # available in Python 2 (it's 3.3+).
+
+    return "\n".join(n*" " + line for line in s.split("\n"))
 
 
 def _name_and_val_str(sc):
@@ -2952,8 +2974,7 @@
             # Print "(NEW)" next to symbols without a user value (from e.g. a
             # .config), but skip it for choice symbols in choices in y mode,
             # and for symbols of UNKNOWN type (which generate a warning though)
-            if sym.user_value is None and \
-               sym.type != UNKNOWN and \
+            if sym.user_value is None and sym.orig_type and \
                not (sym.choice and sym.choice.tri_value == 2):
 
                 s += " (NEW)"
@@ -3005,10 +3026,10 @@
         return ""
 
     # Wouldn't normally happen, and generates a warning
-    if item.type == UNKNOWN:
+    if not item.orig_type:
         return ""
 
-    if item.type in (STRING, INT, HEX):
+    if item.orig_type in (STRING, INT, HEX):
         return "({})".format(item.str_value)
 
     # BOOL or TRISTATE
@@ -3042,28 +3063,26 @@
     # Returns True if the string 's' is a well-formed value for 'sym'.
     # Otherwise, displays an error and returns False.
 
-    if sym.type not in (INT, HEX):
+    if sym.orig_type not in (INT, HEX):
         # Anything goes for non-int/hex symbols
         return True
 
-    base = 10 if sym.type == INT else 16
+    base = 10 if sym.orig_type == INT else 16
     try:
         int(s, base)
     except ValueError:
         _error("'{}' is a malformed {} value"
-               .format(s, TYPE_TO_STR[sym.type]))
+               .format(s, TYPE_TO_STR[sym.orig_type]))
         return False
 
     for low_sym, high_sym, cond in sym.ranges:
         if expr_value(cond):
-            low = int(low_sym.str_value, base)
-            val = int(s, base)
-            high = int(high_sym.str_value, base)
+            low_s = low_sym.str_value
+            high_s = high_sym.str_value
 
-            if not low <= val <= high:
+            if not int(low_s, base) <= int(s, base) <= int(high_s, base):
                 _error("{} is outside the range {}-{}"
-                       .format(s, low_sym.str_value, high_sym.str_value))
-
+                       .format(s, low_s, high_s))
                 return False
 
             break
@@ -3075,7 +3094,7 @@
     # Returns a string with information about the valid range for the symbol
     # 'sym', or None if 'sym' doesn't have a range
 
-    if sym.type in (INT, HEX):
+    if sym.orig_type in (INT, HEX):
         for low, high, cond in sym.ranges:
             if expr_value(cond):
                 return "Range: {}-{}".format(low.str_value, high.str_value)
@@ -3102,7 +3121,17 @@
     return True
 
 
-def _get_wch_compat(win):
+def _getch_compat(win):
+    # Uses get_wch() if available (Python 3.3+) and getch() otherwise. Also
+    # handles a PDCurses resizing quirk.
+
+    if hasattr(win, "get_wch"):
+        c = win.get_wch()
+    else:
+        c = win.getch()
+        if 0 <= c <= 255:
+            c = chr(c)
+
     # Decent resizing behavior on PDCurses requires calling resize_term(0, 0)
     # after receiving KEY_RESIZE, while ncurses (usually) handles terminal
     # resizing automatically in get(_w)ch() (see the end of the
@@ -3111,8 +3140,6 @@
     # resize_term(0, 0) reliably fails and does nothing on ncurses, so this
     # hack gives ncurses/PDCurses compatibility for resizing. I don't know
     # whether it would cause trouble for other implementations.
-
-    c = win.get_wch()
     if c == curses.KEY_RESIZE:
         try:
             curses.resize_term(0, 0)
-- 
http://lists.linuxfromscratch.org/listinfo/alfs-log
Unsubscribe: See the above information page

Reply via email to