diff --git a/CHANGES.txt b/CHANGES.txt index 021938a0d7..657fdf0709 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -14,6 +14,9 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER - Updated Value Node docs and tests. - Python 3.13 compat: re.sub deprecated count, flags as positional args, caused update-release-info test to fail. + - Dump() with json format selected now recognizes additional compound types + (UserDict and UserList), which improves the detail of the display. + json output is also sorted, to match the default display. RELEASE 4.7.0 - Sun, 17 Mar 2024 17:22:20 -0700 diff --git a/RELEASE.txt b/RELEASE.txt index b06b6294f0..656a2681af 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -26,8 +26,9 @@ DEPRECATED FUNCTIONALITY CHANGED/ENHANCED EXISTING FUNCTIONALITY --------------------------------------- -- List modifications to existing features, where the previous behavior - wouldn't actually be considered a bug +- Dump() with json format selected now recognizes additional compound types + (UserDict and UserList), which improves the detail of the display. + json output is also sorted, to match the default display. FIXES ----- diff --git a/SCons/Environment.py b/SCons/Environment.py index 3e57498ed9..a1db8b6f3b 100644 --- a/SCons/Environment.py +++ b/SCons/Environment.py @@ -35,9 +35,9 @@ import sys import re import shlex -from collections import UserDict, deque +from collections import UserDict, UserList, deque from subprocess import PIPE, DEVNULL -from typing import Optional +from typing import Optional, Sequence import SCons.Action import SCons.Builder @@ -1687,17 +1687,23 @@ def Dictionary(self, *args): return dlist - def Dump(self, key=None, format: str='pretty'): - """ Return construction variables serialized to a string. + def Dump(self, key: Optional[str] = None, format: str = 'pretty') -> str: + """ Returns a dump of serialized construction variables. + + The display formats are intended for humaan readers when + debugging - none of the supported formats produce a result that + SCons itself can directly make use of. Objects that cannot + directly be represented get a placeholder like + ```` or ``<>``. Args: - key (optional): if None, format the whole dict of variables. - Else format the value of `key` (Default value = None) - format (str, optional): specify the format to serialize to. - `"pretty"` generates a pretty-printed string, - `"json"` a JSON-formatted string. - (Default value = `"pretty"`) + key: if ``None``, format the whole dict of variables, + else format just the value of *key*. + format: specify the format to serialize to. ``"pretty"`` generates + a pretty-printed string, ``"json"`` a JSON-formatted string. + Raises: + ValueError: *format* is not a recognized serialization format. """ if key: cvars = self.Dictionary(key) @@ -1707,9 +1713,9 @@ def Dump(self, key=None, format: str='pretty'): fmt = format.lower() if fmt == 'pretty': - import pprint - pp = pprint.PrettyPrinter(indent=2) + import pprint # pylint: disable=import-outside-toplevel + pp = pprint.PrettyPrinter(indent=2) # TODO: pprint doesn't do a nice job on path-style values # if the paths contain spaces (i.e. Windows), because the # algorithm tries to break lines on spaces, while breaking @@ -1718,26 +1724,33 @@ def Dump(self, key=None, format: str='pretty'): return pp.pformat(cvars) elif fmt == 'json': - import json - def non_serializable(obj): - return '<>' % type(obj).__qualname__ - return json.dumps(cvars, indent=4, default=non_serializable) + import json # pylint: disable=import-outside-toplevel + + class DumpEncoder(json.JSONEncoder): + """SCons special json Dump formatter.""" + def default(self, obj): + if isinstance(obj, (UserList, UserDict)): + return obj.data + return f'<>' + + return json.dumps(cvars, indent=4, cls=DumpEncoder, sort_keys=True) else: raise ValueError("Unsupported serialization format: %s." % fmt) - def FindIxes(self, paths, prefix, suffix): - """Search a list of paths for something that matches the prefix and suffix. + def FindIxes(self, paths: Sequence[str], prefix: str, suffix: str) -> Optional[str]: + """Search *paths* for a path that has *prefix* and *suffix*. - Args: - paths: the list of paths or nodes. - prefix: construction variable for the prefix. - suffix: construction variable for the suffix. + Returns on first match. - Returns: the matched path or None + Arguments: + paths: the list of paths or nodes. + prefix: construction variable for the prefix. + suffix: construction variable for the suffix. + Returns: + The matched path or ``None`` """ - suffix = self.subst('$'+suffix) prefix = self.subst('$'+prefix) diff --git a/SCons/EnvironmentTests.py b/SCons/EnvironmentTests.py index 8a385c1bc0..8061e595b5 100644 --- a/SCons/EnvironmentTests.py +++ b/SCons/EnvironmentTests.py @@ -3182,7 +3182,8 @@ def test_Dump(self) -> None: assert len(env.Dump()) > 200, env.Dump() # no args version assert env.Dump('FOO', 'json') == '"foo"' # JSON key version - self.assertEqual(env.Dump('FOOFLAGS', 'json'), '"<>"') + expect = """[\n "--bar",\n "--baz"\n]""" + self.assertEqual(env.Dump('FOOFLAGS', 'json'), expect) import json env_dict = json.loads(env.Dump(format = 'json')) assert env_dict['FOO'] == 'foo' # full JSON version