diff --git a/CHANGES.txt b/CHANGES.txt index 6c6502e6a..d472eed25 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -57,6 +57,8 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER which tests the runner was adjusted to match. - Switch remaining "original style" docstring parameter listings to Google style. + - Additional small tweaks to Environment.py type hints, fold some overly + long function signature lines, and some linting-insipired cleanups. RELEASE 4.10.1 - Sun, 16 Nov 2025 10:51:57 -0700 diff --git a/RELEASE.txt b/RELEASE.txt index 67d325e8c..ef9fd7607 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -57,6 +57,9 @@ IMPROVEMENTS - Switch remaining "original style" docstring parameter listings to Google style. +- Additional small tweaks to Environment.py type hints, fold some overly +long function signature lines, and some linting-insipired cleanups. + PACKAGING --------- diff --git a/SCons/Environment.py b/SCons/Environment.py index 426cdd6b8..4c05ec0bc 100644 --- a/SCons/Environment.py +++ b/SCons/Environment.py @@ -39,10 +39,12 @@ from collections import UserDict, UserList, deque from collections.abc import Callable, Collection from subprocess import DEVNULL, PIPE +from types import ModuleType from typing import TYPE_CHECKING, Any, NoReturn, cast, overload import SCons.Action import SCons.Builder +import SCons.CacheDir import SCons.Debug import SCons.Defaults import SCons.Memoize @@ -51,6 +53,7 @@ import SCons.Node.FS import SCons.Node.Python import SCons.Platform +import SCons.Scanner import SCons.SConf import SCons.SConsign import SCons.Subst @@ -111,7 +114,11 @@ def alias_builder(env, target, source) -> None: name='AliasBuilder', ) -def apply_tools(env: EnvironmentBase, tools: list[str | tuple[str, dict[str, Any]]], toolpath: list[str] | None) -> None: +def apply_tools( + env: EnvironmentBase, + tools: list[str | tuple[str, dict[str, Any]]], + toolpath: list[str] | None, +) -> None: """Apply the specified tools to the Environment.""" # Store the toolpath in the Environment. # This is expected to work even if no tools are given, so do this first. @@ -122,7 +129,7 @@ def apply_tools(env: EnvironmentBase, tools: list[str | tuple[str, dict[str, Any # Filter out null tools from the list. for tool in [_f for _f in tools if _f]: - if is_List(tool) or is_Tuple(tool): + if is_Sequence(tool): # toolargs should be a dict of kw args toolname, toolargs, *_ = cast(tuple, tool) _ = env.Tool(toolname, **toolargs) @@ -149,9 +156,9 @@ def apply_tools(env: EnvironmentBase, tools: list[str | tuple[str, dict[str, Any #'HOST_CPU', ] -def copy_non_reserved_keywords(dict: dict[str, Any]) -> dict[str, Any]: +def copy_non_reserved_keywords(dict_: dict[str, Any]) -> dict[str, Any]: """Copy a dict excluding reserved construction variable names.""" - result = semi_deepcopy(dict) + result = semi_deepcopy(dict_) for k in result.copy().keys(): if k in reserved_construction_var_names: msg = "Ignoring attempt to set reserved variable `$%s'" @@ -177,7 +184,7 @@ def _set_BUILDERS(env: EnvironmentBase, key: str, value): for k in bd.copy().keys(): del bd[k] except KeyError: - bd = BuilderDict(bd, env) + bd = BuilderDict(bd, env) # XXX use-before-set? env._dict[key] = bd for k, v in value.items(): if not SCons.Builder.is_a_Builder(v): @@ -259,9 +266,9 @@ def _add_define(item: Any, defines: deque[Any], prepend: bool = False) -> None: def _is_in(item: Any, defines: deque[Any]): """Return match for *item* if found in *defines*. - Accounts for type differences: tuple ("FOO", "BAR"), list - ["FOO", "BAR"], string "FOO=BAR" and dict {"FOO": "BAR"} all - differ as far as Python equality comparison is concerned, but + Accounts for type differences: tuple ``("FOO", "BAR")``, list + ``["FOO", "BAR"]``, string ``"FOO=BAR"`` and dict ``{"FOO": "BAR"}`` + all differ as far as Python equality comparison is concerned, but are the same for purposes of creating the preprocessor macro. Also an unvalued string should match something like ``("FOO", None)``. Since the caller may wish to remove a matched entry, we need to @@ -280,7 +287,7 @@ def _macro_conv(v) -> list: """Normalize a macro to a list for comparisons.""" if is_Tuple(v): return list(v) - elif is_String(v): + if is_String(v): rv = v.split("=") if len(rv) == 1: return [v, None] @@ -472,10 +479,9 @@ def __str__(self) -> str: def __getattr__(self, name: str) -> Any: if name == 'env': return self.object - elif name == 'builder': + if name == 'builder': return self.method - else: - raise AttributeError(name) + raise AttributeError(name) def __setattr__(self, name: str, value: Any | None) -> None: if name == 'env': @@ -530,9 +536,10 @@ def __delitem__(self, item: str) -> None: super().__delitem__(item) delattr(self.env, item) - def update(self, mapping: dict[str, Any]) -> None: # type: ignore[override] - for i, v in mapping.items(): - self.__setitem__(i, v) + # MutableMapping.update() should be fine here with our own __setitem__. + # def update(self, mapping: dict[str, Any]) -> None: # type: ignore[override] + # for i, v in other.items(): + # self.__setitem__(i, v) class SubstitutionEnvironment: @@ -563,16 +570,16 @@ class actually becomes useful.) Special note: methods here and in actual child classes might be called via proxy from an :class:`OverrideEnvironment`, which isn't in the class inheritance chain. Take care that methods called with a *self* - that's really an ``OverrideEnvironment`` don't make bad assumptions. + that's actually an ``OverrideEnvironment`` don't make bad assumptions. Note: as currently constituted, this class is not useful standalone, - except for unit tests (the only things that ever instantiate one). - There is at least one method (:meth:`MergeFlags`) that calls methods that - don't exist here (:meth:`~Base.Append`, :meth:`~Base.AppendUnique`, - and :meth:`~Split`) that are left to derived classes - :class:`Base`). - A brief experiment in moving ``MergeFlags`` - broke things as it's part of how an environment is initialized. - It doesn't seen worthwhile to refactor this separation. + except for unit tests, the only things that ever instantiate one. + There is at least one method (:meth:`MergeFlags`) that calls methods + (:meth:`~Base.Append`, :meth:`~Base.AppendUnique`, and :meth:`~Split`) + that only exist in instances of child class :class:`Base` and classes + derived from it. A brief experiment in moving ``MergeFlags`` down + to ``Base`` broke things as it's part of how an environment is initialized. + We could maybe refactor this separation, but it seems a lot of work. """ def __init__(self, **kw) -> None: @@ -587,21 +594,30 @@ def __init__(self, **kw) -> None: def _init_special(self) -> None: """Initialize the dispatch tables for special construction variables.""" - self._special_del: dict[str, Callable[..., Any | None]] = {} - self._special_del['SCANNERS'] = _del_SCANNERS - - self._special_set = {} + self._special_del: dict[str, Callable[..., Any | None]] = { + 'SCANNERS': _del_SCANNERS + } + self._special_set: dict[str, Callable[..., Any | None]] = { + 'BUILDERS': _set_BUILDERS, + 'SCANNERS': _set_SCANNERS, + } for key in reserved_construction_var_names: self._special_set[key] = _set_reserved for key in future_reserved_construction_var_names: self._special_set[key] = _set_future_reserved - self._special_set['BUILDERS'] = _set_BUILDERS - self._special_set['SCANNERS'] = _set_SCANNERS - # Freeze the keys of self._special_set in a list for use by - # methods that need to check. + # Freeze the special_set keys in case someone needs to check self._special_set_keys = list(self._special_set.keys()) + ####################################################################### + # Methods that make an Environment act like a dictionary. These have + # the expected standard names for Python mapping objects. For performance, + # we don't actually make an Environment a subclass of UserDict but just + # use the same trick of a private data member that is the actual + # storage for the construction variables. Generally these methods have + # been added on demand so not everything may be emulated. + ####################################################################### + def __eq__(self, other) -> bool: """Compare two environments. @@ -612,7 +628,7 @@ def __eq__(self, other) -> bool: is sure to work even if one or both are are instances of :class:`OverrideEnvironment`. However an actual ``SubstitutionEnvironment`` doesn't have a ``Dictionary`` method - That causes problems for unit tests written to excercise + That causes problems for unit tests written to exercise ``SubsitutionEnvironment`` directly, although nobody else seems to ever instantiate one. We count on :class:`OverrideEnvironment` to fake the :attr:`_dict` to make things work. @@ -669,6 +685,11 @@ def setdefault(self, key: str, default: Any | None = None) -> Any | None: """Emulate the ``setdefault`` method of dictionaries.""" return self._dict.setdefault(key, default) + ####################################################################### + # Utility methods that are primarily for internal use by SCons. + # These begin with lower-case letters. + ####################################################################### + def arg2nodes( self, args: str | Node | list[str | Node], @@ -690,22 +711,19 @@ def arg2nodes( Returns: a list of nodes. """ + if not args: + return [] if node_factory is _null: node_factory = self.fs.File if lookup_list is _null: lookup_list = self.lookup_list - - if not args: - return [] - args = flatten(args) - nodes: list[Node] = [] for v in args: if is_String(v): n = None - for l in lookup_list: - n = l(v) + for func in lookup_list: + n = func(v) if n is not None: break if n is not None: @@ -1185,7 +1203,9 @@ def append_define(name: str, mapping: dict[str, Any] = mapping) -> None: do_parse(arg) return mapping - def MergeFlags(self, args: str | list[str] | dict[str, Any], unique: bool = True) -> None: + def MergeFlags( + self, args: str | list[str] | dict[str, Any], unique: bool = True + ) -> None: """Merge flags into construction variables. Merges the flags from *args* into this construction environent. @@ -1264,13 +1284,21 @@ def MergeFlags(self, args: str | list[str] | dict[str, Any], unique: bool = True self[key] = t - -def default_decide_source(dependency: FileNode, target: FileNode, prev_ni: NodeInfoBase, repo_node: Node | None = None) -> bool: +def default_decide_source( + dependency: FileNode, + target: FileNode, + prev_ni: NodeInfoBase, + repo_node: Node | None = None, +) -> bool: f = SCons.Defaults.DefaultEnvironment().decide_source return f(dependency, target, prev_ni, repo_node) - -def default_decide_target(dependency: FileNode, target: FileNode, prev_ni: NodeInfoBase, repo_node: Node | None = None) -> bool: +def default_decide_target( + dependency: FileNode, + target: FileNode, + prev_ni: NodeInfoBase, + repo_node: Node | None = None, +) -> bool: f = SCons.Defaults.DefaultEnvironment().decide_target return f(dependency, target, prev_ni, repo_node) @@ -1303,7 +1331,7 @@ class Base(SubstitutionEnvironment): Arguments: platform: the SCons platform name to use for initialization. - tools: list of tools to initialize. + tools: name or list of tool names to initialize. toolpath: list of paths to search for tools. variables: a :class:`~SCons.Variables.Variables` object to use to populate construction variables from command-line @@ -1317,14 +1345,6 @@ class Base(SubstitutionEnvironment): # a little organized by grouping the methods. ####################################################################### - ####################################################################### - # Methods that make an Environment act like a dictionary. These have - # the expected standard names for Python mapping objects. Note that - # we don't actually make an Environment a subclass of UserDict for - # performance reasons. Note also that we only supply methods for - # dictionary functionality that we actually need and use. - ####################################################################### - def __init__( self, platform: str | PlatformSpec | None = None, @@ -1357,9 +1377,7 @@ def __init__( self._dict['BUILDERS'] = BuilderDict(self._dict['BUILDERS'], self) if platform is None: - platform = self._dict.get('PLATFORM', None) - if platform is None: - platform = SCons.Platform.Platform() + platform = self._dict.get('PLATFORM', SCons.Platform.Platform()) if is_String(platform): platform = SCons.Platform.Platform(platform) self._dict['PLATFORM'] = str(platform) @@ -1378,12 +1396,11 @@ def __init__( # some of them during initialization. if 'options' in kw: # Backwards compatibility: handle the old "options" keyword. - variables = kw['options'] - del kw['options'] + variables = kw.pop('options') self.Replace(**kw) keys = list(kw.keys()) if variables: - keys = keys + list(variables.keys()) + keys.extend(variables.keys()) variables.Update(self) save = {} @@ -1486,7 +1503,7 @@ def get_CacheDir(self) -> CacheDir: self._last_CacheDir: CacheDir = cd return cd - def get_factory(self, factory, default: str='File'): + def get_factory(self, factory, default: str = 'File'): """Return a factory function for creating Nodes.""" name = default try: @@ -1768,7 +1785,7 @@ def Clone( assignments. Arguments: - tools: list of tools to initialize. + tools: name or list of tool names to initialize. toolpath: list of paths to search for tools. variables: a :class:`~SCons.Variables.Variables` object to use to populate construction variables from command-line @@ -1824,23 +1841,50 @@ def _changed_build(self, dependency: FileNode, target: FileNode, prev_ni: NodeIn return True return self.decide_source(dependency, target, prev_ni, repo_node) - def _changed_content(self, dependency: FileNode, target: FileNode, prev_ni: NodeInfoBase, repo_node: Node | None = None) -> bool: + @staticmethod + def _changed_content( + dependency: FileNode, + target: FileNode, + prev_ni: NodeInfoBase, + repo_node: Node | None = None, + ) -> bool: """Decide whether a target needs to be rebuilt based on content change.""" return dependency.changed_content(target, prev_ni, repo_node) - def _changed_timestamp_then_content(self, dependency: FileNode, target: FileNode, prev_ni: NodeInfoBase, repo_node: Node | None = None) -> bool: + @staticmethod + def _changed_timestamp_then_content( + dependency: FileNode, + target: FileNode, + prev_ni: NodeInfoBase, + repo_node: Node | None = None, + ) -> bool: """Decide whether a target needs to be rebuilt based on timestamp then content.""" return dependency.changed_timestamp_then_content(target, prev_ni, repo_node) - def _changed_timestamp_newer(self, dependency: FileNode, target: FileNode, prev_ni: NodeInfoBase, repo_node: Node | None = None) -> bool: + @staticmethod + def _changed_timestamp_newer( + dependency: FileNode, + target: FileNode, + prev_ni: NodeInfoBase, + repo_node: Node | None = None, + ) -> bool: """Decide whether a target needs to be rebuilt based on newer timestamp.""" return dependency.changed_timestamp_newer(target, prev_ni, repo_node) - def _changed_timestamp_match(self, dependency: FileNode, target: FileNode, prev_ni: NodeInfoBase, repo_node: Node | None = None) -> bool: + @staticmethod + def _changed_timestamp_match( + dependency: FileNode, + target: FileNode, + prev_ni: NodeInfoBase, + repo_node: Node | None = None, + ) -> bool: """Decide whether a target needs to be rebuilt based on timestamp matching.""" return dependency.changed_timestamp_match(target, prev_ni, repo_node) - def Decider(self, function: str | Callable[[FileNode, FileNode, NodeInfoBase, Node | None], bool]) -> None: + def Decider( + self, + function: str | Callable[[FileNode, FileNode, NodeInfoBase, Node | None], bool], + ) -> None: """Set the decision function for whether targets need rebuilding.""" self.cache_timestamp_newer = False if function in ('MD5', 'content'): @@ -2005,11 +2049,11 @@ def ParseConfig(self, command, function=None, unique: bool = True) -> Any | None :meth:`MergeFlags` is used. Takes 3 args ``(env, args, unique)`` unique: if true (the default) no duplicate values are allowed """ - if function is None: - def parse_conf(env, cmd, unique=unique): - return env.MergeFlags(cmd, unique) + def parse_conf(env, cmd, unique=unique): + return env.MergeFlags(cmd, unique) + if function is None: function = parse_conf if is_List(command): command = ' '.join(command) @@ -2017,7 +2061,12 @@ def parse_conf(env, cmd, unique=unique): return function(self, self.backtick(command), unique) - def ParseDepends(self, filename: str, must_exist: bool = False, only_one: bool = False) -> None: + def ParseDepends( + self, + filename: str, + must_exist: bool = False, + only_one: bool = False, + ) -> None: """Parse a depends-style file *filename* for explicit dependencies. This is completely abusable, and should be unnecessary in the @@ -2029,13 +2078,14 @@ def ParseDepends(self, filename: str, must_exist: bool = False, only_one: bool = """ filename = self.subst(filename) try: + # TODO: use Node layer to account for repository / variantdir? with open(filename) as fp: lines = LogicalLines(fp).readlines() except OSError: if must_exist: raise return - lines = [line for line in lines if line[0] != '#'] + lines = [line for line in lines if not line.startswith('#')] tdlist = [] for line in lines: try: @@ -2224,7 +2274,7 @@ def Replace(self, **kw) -> None: except KeyError: pass else: - kwbd = BuilderDict(kwbd,self) + kwbd = BuilderDict(kwbd, self) del kw['BUILDERS'] self.__setitem__('BUILDERS', kwbd) kw = copy_non_reserved_keywords(kw) @@ -2248,18 +2298,18 @@ def ReplaceIxes( new_prefix: construction variable for the new prefix. new_suffix: construction variable for the new suffix. """ - old_prefix = self.subst('$'+old_prefix) - old_suffix = self.subst('$'+old_suffix) + old_prefix = self.subst('$' + old_prefix) + old_suffix = self.subst('$' + old_suffix) - new_prefix = self.subst('$'+new_prefix) - new_suffix = self.subst('$'+new_suffix) + new_prefix = self.subst('$' + new_prefix) + new_suffix = self.subst('$' + new_suffix) - dir,name = os.path.split(str(path)) - if name[:len(old_prefix)] == old_prefix: - name = name[len(old_prefix):] - if name[-len(old_suffix):] == old_suffix: - name = name[:-len(old_suffix)] - return os.path.join(dir, new_prefix+name+new_suffix) + dir_, name = os.path.split(str(path)) + if name[: len(old_prefix)] == old_prefix: + name = name[len(old_prefix) :] + if name[-len(old_suffix) :] == old_suffix: + name = name[: -len(old_suffix)] + return os.path.join(dir_, new_prefix + name + new_suffix) def SetDefault(self, **kw) -> None: """Set construction variables if they do not already have values. @@ -2277,16 +2327,18 @@ def _find_toolpath_dir(self, tp): return self.fs.Dir(self.subst(tp)).srcnode().get_abspath() def Tool( - self, tool: str | Callable, toolpath: Collection[str] | None = None, **kwargs + self, + tool: str | Callable, + toolpath: Collection[str] | None = None, + **kwargs, ) -> SCons.Tool.Tool: """Find and run tool module *tool*. *tool* is generally a string, but can also be a callable object, in which case it is just called, without any of the setup. - The skipped setup includes storing *kwargs* into the created - :class:`~SCons.Tool.Tool` instance, which is extracted and used - when the instance is called, so in the skip case, the called - object will not get the *kwargs*. + The setup stores *kwargs* into the created :class:`~SCons.Tool.Tool` + instance, which is extracted and used when the instance is called, + so in the skip case, the called object will not see the *kwargs*. .. versionchanged:: 4.2 returns the tool object rather than ``None``. @@ -2369,6 +2421,7 @@ def subst_string(a, self=self): if is_String(a): a = self.subst(a) return a + nargs = list(map(subst_string, args)) nkw = self.subst_kw(kw) return SCons.Action.Action(*nargs, **nkw) @@ -2403,12 +2456,21 @@ def AddPostAction(self, files: str | Node | list[str | Node], action) -> list[No executor.add_post_action(action) return nodes - def Alias(self, target: str | Node | list[str | Node], source: str | Node | list[str | Node] = [], action=None, **kw) -> list[Node]: + def Alias( + self, + target: str | Node | list[str | Node], + source: str | Node | list[str | Node] | None = None, + action=None, + **kw, + ) -> list[Node]: """Create an alias *target* that depends on *source*.""" tlist = self.arg2nodes(target, self.ans.Alias) - if not is_List(source): + if is_List(source): + source = [_f for _f in cast(list, source) if _f] + elif source is not None: source = [source] # type: ignore[list-item] - source = [_f for _f in cast(list, source) if _f] + else: + source = [] if not action: if not source: @@ -2561,7 +2623,11 @@ def Command(self, target, source, action, **kw) -> list[Node]: bld = SCons.Builder.Builder(**bkw) return bld(self, target, source, **kw) - def Depends(self, target: str | Node | list[str | Node], dependency: str | Node | list[str | Node]) -> list[Node]: + def Depends( + self, + target: str | Node | list[str | Node], + dependency: str | Node | list[str | Node], + ) -> list[Node]: """Explicity specify that *target* depends on *dependency*.""" tlist = self.arg2nodes(target, self.fs.Entry) dlist = self.arg2nodes(dependency, self.fs.Entry) @@ -2649,8 +2715,7 @@ def Execute(self, action, *args, **kw): errstr = result.filename + ': ' + errstr sys.stderr.write("scons: *** %s\n" % errstr) return result.status - else: - return result + return result @overload def File(self, name: str | Node, *args, **kw) -> FileNode: ... @@ -2668,14 +2733,21 @@ def File(self, name: str | Node | list[str | Node], *args, **kw) -> FileNode | l return result return self.fs.File(cast(str, s), *args, **kw) - def FindFile(self, file: str, dirs: str | Node | list[str | Node]) -> FileNode | None: + def FindFile( + self, file: str, dirs: str | Node | list[str | Node] + ) -> FileNode | None: """Find *file* in *dirs* and return the corresponding Node.""" sfile = self.subst(file) nodes = self.arg2nodes(dirs, self.fs.Dir) return SCons.Node.FS.find_file(sfile, tuple(nodes)) - def Flatten(self, sequence: Any) -> list: - """Flatten a nested sequence into a single sequence.""" + @staticmethod + def Flatten(sequence: Any) -> list: + """Flatten a nested sequence into a single list. + + *sequence* can also be a scalar, which still returns a list. + See :meth:`SCons.Util.flatten`. + """ return flatten(sequence) @overload @@ -2689,16 +2761,15 @@ def GetBuildPath(self, files: str | Node | list[str | Node]) -> str | list[str]: result = list(map(str, self.arg2nodes(files, self.fs.Entry))) if is_List(files): return result - else: - return result[0] + return result[0] def Glob( self, pattern: str, ondisk: bool = True, - source: bool = True, + source: bool = False, strings: bool = False, - exclude: list[str] | None = None, + exclude: str | list[str] | None = None, ) -> list[Node] | list[str]: """Return a list of nodes matching *pattern*.""" return self.fs.Glob(self.subst(pattern), ondisk, source, strings, exclude) @@ -2715,7 +2786,8 @@ def Ignore( t.add_ignore(dlist) return tlist - def Literal(self, string: str): + @staticmethod + def Literal(string: str) -> SCons.Subst.Literal: """Return a Literal substitution wrapper for *string*.""" return SCons.Subst.Literal(string) @@ -2728,15 +2800,15 @@ def Local(self, *targets: str | Node | list[str | Node]) -> list[Node]: ret.append(targ) else: for t in self.arg2nodes(targ, self.fs.Entry): - t.set_local() # type: ignore[attr-defined] - ret.append(t) + t.set_local() # type: ignore[attr-defined] + ret.append(t) return ret def Precious(self, *targets: str | Node | list[str | Node]) -> list[Node]: """Mark *targets* as precious: do not delete before building.""" tlist = [] for t in targets: - tlist.extend(self.arg2nodes(t, self.fs.Entry)) + tlist.extend(self.arg2nodes(t, node_factory=self.fs.Entry)) for t in tlist: t.set_precious() return tlist @@ -2745,7 +2817,7 @@ def Pseudo(self, *targets: str | Node | list[str | Node]) -> list[Node]: """Mark *targets* as pseudo: must not exist.""" tlist = [] for t in targets: - tlist.extend(self.arg2nodes(t, self.fs.Entry)) + tlist.extend(self.arg2nodes(t, node_factory=self.fs.Entry)) for t in tlist: t.set_pseudo() return tlist @@ -2755,7 +2827,11 @@ def Repository(self, *dirs: str | DirNode | list[str | DirNode], **kw) -> None: dirs = self.arg2nodes(list(dirs), self.fs.Dir) self.fs.Repository(*dirs, **kw) - def Requires(self, target: str | Node | list[str | Node], prerequisite: str | Node | list[str | Node]) -> list[Node]: + def Requires( + self, + target: str | Node | list[str | Node], + prerequisite: str | Node | list[str | Node], + ) -> list[Node]: """Specify that *prerequisite* must be built before *target*. Creates an order-only relationship, not a full dependency. @@ -2782,14 +2858,23 @@ def Scanner(self, *args, **kw) -> SCons.Scanner.ScannerBase: nkw = self.subst_kw(kw) return SCons.Scanner.ScannerBase(*nargs, **nkw) - def SConsignFile(self, name: str = SCons.SConsign.current_sconsign_filename(), dbm_module: str | None = None) -> None: - """Specify the name of the signature database use for this environment. + def SConsignFile( + self, name: str | None = "", dbm_module: ModuleType | None = None, + ) -> None: + """Specify the name of the signature database to use for this environment. + + For historical reasons, if *name* is ``None``, the database is + stored as one file per directory, rather than a single database + for the project. - If *dbm_module* is specified, it is the name of the database module - to use. It must follow the Python Database API specification + If *dbm_module* is specified, it is the database module to use + (you need to pass the module object, not a string name). + The module must follow the Python Database API specification described in PEP 249. """ if name is not None: + if not name: + name = SCons.SConsign.current_sconsign_filename() name = self.subst(name) if not os.path.isabs(name): name = os.path.join(str(self.fs.SConstruct_dir), name) @@ -2800,7 +2885,11 @@ def SConsignFile(self, name: str = SCons.SConsign.current_sconsign_filename(), d self.Execute(SCons.Defaults.Mkdir(sconsign_dir)) SCons.SConsign.File(name, dbm_module) - def SideEffect(self, side_effect: str | Node | list[str | Node], target: str | Node | list[str | Node]) -> list[Node]: + def SideEffect( + self, + side_effect: str | Node | list[str | Node], + target: str | Node | list[str | Node], + ) -> list[Node]: """Record that *side_effects* are also built when building *target*.""" side_effects = self.arg2nodes(side_effect, self.fs.Entry) targets = self.arg2nodes(target, self.fs.Entry) @@ -2830,7 +2919,9 @@ def Split(self, arg: Node | list[Node]) -> list[Node]: ... @overload def Split(self, arg: list[str | Node]) -> list[str | Node]: ... - def Split(self, arg: str | Node | list[str] | list[Node] | list[str | Node]) -> list[str] | list[Node] | list[str | Node]: + def Split( + self, arg: str | Node | list[str] | list[Node] | list[str | Node] + ) -> list[str] | list[Node] | list[str | Node]: """Convert *arg* into a list of strings or Nodes. If *arg* is a string, it is split on whitespace. This makes @@ -2848,12 +2939,13 @@ def Split(self, arg: str | Node | list[str] | list[Node] | list[str | Node]) -> """ if is_List(arg): return list(map(self.subst, arg)) # type: ignore[arg-type] - elif is_String(arg): + if is_String(arg): return self.subst(arg).split() # type: ignore[arg-type] - else: - return [self.subst(arg)] # type: ignore[arg-type] + return [self.subst(arg)] # type: ignore[arg-type] - def Value(self, value: Any | None, built_value: Any | None = None, name: str | None = None): + def Value( + self, value: Any | None, built_value: Any | None = None, name: str | None = None + ) -> SCons.Node.Python.Value: """Return a value Node ((Python expression). .. versionchanged:: 4.0 @@ -2861,7 +2953,9 @@ def Value(self, value: Any | None, built_value: Any | None = None, name: str | N """ return SCons.Node.Python.ValueWithMemo(value, built_value, name) - def VariantDir(self, variant_dir: str | Node, src_dir: str | Node, duplicate: bool = True) -> None: + def VariantDir( + self, variant_dir: str | Node, src_dir: str | Node, duplicate: bool = True + ) -> None: """Create a VariantDir mapping. This function creates a mapping from the source directory *src_dir* to the @@ -2875,9 +2969,9 @@ def VariantDir(self, variant_dir: str | Node, src_dir: str | Node, duplicate: bo def FindSourceFiles(self, node: str | Node = ".") -> list[EntryNode]: """Return the list of all source files under *node*.""" - node = self.arg2nodes(node, self.fs.Entry)[0] - + mynode = self.arg2nodes(node, self.fs.Entry)[0] sources = [] + def build_source(ss) -> None: for s in ss: if isinstance(s, SCons.Node.FS.Dir): @@ -2886,17 +2980,20 @@ def build_source(ss) -> None: build_source(s.sources) elif isinstance(s.disambiguate(), SCons.Node.FS.File): sources.append(s) - build_source(node.all_children()) + + build_source(mynode.all_children()) def final_source(node): while node != node.srcnode(): - node = node.srcnode() + node = node.srcnode() return node - sources = list(map(final_source, sources)) + + sources = map(final_source, sources) # remove duplicates return list(set(sources)) - def FindInstalledFiles(self) -> list[FileNode]: + @staticmethod + def FindInstalledFiles() -> list[FileNode]: """Return the list of all targets of the Install and InstallAs Builders.""" from SCons.Tool import install if install._UNIQUE_INSTALLED_FILES is None: @@ -2976,8 +3073,7 @@ def __getattr__(self, name: str) -> Any: # so they have access to overridden values. if isinstance(attr, MethodWrapper): return attr.clone(self) - else: - return attr + return attr def __setattr__(self, name: str, value: Any | None) -> None: setattr(self.__dict__['__subject'], name, value) @@ -3167,7 +3263,8 @@ def __getattr__(self, name: str) -> Any: def __setattr__(self, name: str, value: Any | None) -> None: return setattr(self.__dict__['__subject'], name, value) - def executor_to_lvars(self, kwdict: dict[str, Any]) -> None: + @staticmethod + def executor_to_lvars(kwdict: dict[str, Any]) -> None: """Transfer executor's local vars to kwdict as 'lvars'.""" if 'executor' in kwdict: kwdict['lvars'] = kwdict['executor'].get_lvars() @@ -3175,7 +3272,8 @@ def executor_to_lvars(self, kwdict: dict[str, Any]) -> None: else: kwdict['lvars'] = {} - def raw_to_mode(self, mapping: dict[str, Any]) -> None: + @staticmethod + def raw_to_mode(mapping: dict[str, Any]) -> None: """Transfer 'raw' entry to 'mode' entry in mapping.""" try: raw = mapping['raw'] diff --git a/SCons/Environment.xml b/SCons/Environment.xml index fda4c464a..d421e1683 100644 --- a/SCons/Environment.xml +++ b/SCons/Environment.xml @@ -3184,7 +3184,7 @@ SConscript('bar/SConscript') # will chdir to bar -Specify where to store the &SCons; file signature database, +Specify where to store the &SCons; signature database, and which database format to use. This may be useful to specify alternate database files and/or file locations for different types of builds. @@ -3192,15 +3192,16 @@ database files and/or file locations for different types of builds. The optional name argument is the base name of the database file(s). -If not an absolute path name, -these are placed relative to the directory containing the -top-level &SConstruct; file. +The database is placed relative to the directory containing the +top-level &SConstruct; file, unless name +is an absolute path name. The default is .sconsign. The actual database file(s) stored on disk -may have an appropriate suffix appended -by the chosen -dbm_module +may have an appropriate suffix appended by the chosen +dbm_module, +and may also include the name of the hash format +(see ). The optional dbm_module