Skip to content

Commit 960f328

Browse files
authored
Merge pull request #4838 from mwichmann/tool-init
Do not duplicate Tool class creation
2 parents 4c3b8a7 + fef3edf commit 960f328

5 files changed

Lines changed: 116 additions & 19 deletions

File tree

CHANGES.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER
121121
- Adjust race protection for CacheDir - now uses a unique temporary
122122
directory for each writer before doing the move, instead of depending
123123
on a one-time uuid to make a path to the file.
124+
- Clarify VariantDir behavior when switching to not duplicate sources
125+
and tweak wording a bit.
126+
- Don't duplicate the creation of Tool objects if "default" is part of
127+
the tool list.
124128
- Test suite: add support for Python 3.15 in the Action unit tests.
125129
- Add a test for FindFile to be sure it locates non-existing derived
126130
files as advertised.

RELEASE.txt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,12 +87,14 @@ IMPROVEMENTS
8787
- Switch remaining "original style" docstring parameter listings to Google style.
8888

8989
- Additional small tweaks to Environment.py type hints, fold some overly
90-
long function signature lines, and some linting-insipired cleanups.
90+
long function signature lines, and some linting-inspired cleanups.
9191

9292
- Test framework: tweak module docstrings
9393

9494
- Test suite: end to end tests don't use assert in result checks
9595

96+
- Don't duplicate the creation of Tool objects if "default" is part of
97+
the tool list.
9698
- Test suite: add support for Python 3.15 in the Action unit tests.
9799
- zip tool now uses zipfile module's "from_file" method to populate ZipInfo
98100
records, rather than doing manually.
@@ -174,7 +176,7 @@ DEVELOPMENT
174176
- Update pyproject.toml to support Python 3.14 and remove restrictions on lxml version install
175177
- Unify internal "_null" sentinel usage.
176178
- Docbook tests: improve skip message, more clearly indicate which test
177-
need actual installed system programs (add -live suffix).
179+
needs actual installed system programs (add -live suffix).
178180
- Implement type hints for Environment and environment utilities.
179181

180182
- MSVC: Added a host/target batch file configuration table for Visual

SCons/Tool/ToolTests.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,28 @@ def test_Tool(self) -> None:
100100
assert exc_caught, "did not catch expected UserError"
101101

102102

103+
def test_Tool_instance(self) -> None:
104+
"""Test Tool instance passthrough and equality/hash behavior."""
105+
106+
# Passing an existing Tool instance back to Tool() returns the
107+
# same instance rather than creating a duplicate.
108+
t = SCons.Tool.Tool('g++')
109+
assert SCons.Tool.Tool(t) is t, "Tool() did not return the passed-in instance"
110+
111+
# A Tool compares equal to its name string and to another Tool
112+
# with the same name, but not to unrelated types.
113+
assert t == 'g++', t
114+
assert t == SCons.Tool.Tool('g++'), t
115+
assert t != 'gcc', t
116+
assert t != 1, t
117+
assert t != None, t # noqa: E711 (explicitly exercising __eq__)
118+
119+
# __hash__ is consistent with the name, so a Tool and its name
120+
# collapse together in a set.
121+
assert hash(t) == hash('g++'), hash(t)
122+
assert len({t, 'g++', SCons.Tool.Tool('g++')}) == 1
123+
124+
103125
def test_pathfind(self) -> None:
104126
"""Test that find_program_path() alters PATH only if add_path is true"""
105127

SCons/Tool/__init__.py

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,31 @@
2222
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
2323
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
2424

25-
"""SCons tool selection.
26-
27-
Looks for modules that define a callable object that can modify a
28-
construction environment as appropriate for a given tool (or tool chain).
29-
30-
Note that because this subsystem just *selects* a callable that can
31-
modify a construction environment, it's possible for people to define
32-
their own "tool specification" in an arbitrary callable function. No
33-
one needs to use or tie in to this subsystem in order to roll their own
34-
tool specifications.
25+
"""SCons tool subsystem.
26+
27+
Tool specification modules are callable objects that modify construction
28+
environments to dynamically enable specific types of builds. This module
29+
provides the support for handling tool modules:
30+
31+
- a Tool class to locate and load a tool module.
32+
- lists of default tools for supported platform types and a mechanism to
33+
select the available ones for the current platform.
34+
- various rules for name remapping, special-cased toolchains, etc.
35+
- utility functions for creating common Builders (eliminating duplication
36+
when multiple tool modules could potentially create a Builder).
37+
- create Scanners for common types used by the Builder utilities.
38+
39+
This is not set up like a traditional Python package: this file implements
40+
the part that other subsystems can import and call. The remaining files
41+
are either dynamically loaded tool modules that present entry points
42+
(``exists()`` and ``generate()``) called through the Tool instance,
43+
or support files/common logic that can be imported by those tool
44+
modules. Neither are expected to be directly imported by any other SCons
45+
subsystem (test code may reach in and do so).
46+
47+
Tool modules are simply callable objects that modify a construction
48+
environment. You can define custom tool specifications in any callable
49+
without needing to integrate with this subsystem.
3550
"""
3651

3752
from __future__ import annotations
@@ -40,6 +55,7 @@
4055
import os
4156
import importlib.util
4257

58+
import SCons.Action
4359
import SCons.Builder
4460
import SCons.Errors
4561
import SCons.Node.FS
@@ -50,6 +66,7 @@
5066
import SCons.Scanner.LaTeX
5167
import SCons.Scanner.Prog
5268
import SCons.Scanner.SWIG
69+
import SCons.Util
5370
from SCons.Tool.linkCommon import LibSymlinksActionFunction, LibSymlinksStrFun
5471

5572
DefaultToolpath = []
@@ -108,7 +125,25 @@
108125

109126

110127
class Tool:
128+
"""A class for loading and applying tool modules.
129+
130+
*name* is looked up using standard paths plus any specified *toolpath*.
131+
To avoid duplicate creation of instances, recognize if *name*
132+
is actually an existing instance, if so, just return ourselves
133+
without further setup.
134+
135+
.. versionchanged:: NEXT_RELEASE
136+
Accept an existing instance at creation time and don't duplicate it.
137+
"""
138+
139+
def __new__(cls, name, toolpath=None, **kwargs) -> "Tool":
140+
if isinstance(name, Tool):
141+
return name
142+
return super().__new__(cls)
143+
111144
def __init__(self, name, toolpath=None, **kwargs) -> None:
145+
if isinstance(name, Tool):
146+
return
112147
if toolpath is None:
113148
toolpath = []
114149

@@ -270,6 +305,23 @@ def __call__(self, env, *args, **kw) -> None:
270305
def __str__(self) -> str:
271306
return self.name
272307

308+
def __eq__(self, other) -> bool:
309+
"""Compare a Tool by name.
310+
311+
A Tool is equal to another Tool with the same name, and to a
312+
string matching its name (so a Tool can be used interchangeably
313+
with its name in membership tests and sets). Comparison against
314+
any other type is deferred (returns ``NotImplemented``).
315+
"""
316+
if isinstance(other, Tool):
317+
return self.name == other.name
318+
if isinstance(other, str):
319+
return self.name == other
320+
return NotImplemented
321+
322+
def __hash__(self) -> int:
323+
return hash(self.name)
324+
273325

274326
LibSymlinksAction = SCons.Action.Action(LibSymlinksActionFunction, LibSymlinksStrFun)
275327

@@ -671,18 +723,34 @@ def InstallVersionedLib(self, *args, **kw):
671723

672724
def FindTool(tools, env):
673725
for tool in tools:
726+
if not SCons.Util.is_String(tool):
727+
# Already a Tool instance
728+
if tool.exists(env):
729+
return tool
730+
continue
674731
t = Tool(tool)
675732
if t.exists(env):
676-
return tool
733+
return t
677734
return None
678735

679736

680737
def FindAllTools(tools, env):
681738
def ToolExists(tool, env=env):
682-
return Tool(tool).exists(env)
683-
684-
return list(filter(ToolExists, tools))
739+
if not SCons.Util.is_String(tool):
740+
return tool.exists(env)
741+
t = Tool(tool)
742+
return t.exists(env)
685743

744+
results = []
745+
for tool in tools:
746+
if not SCons.Util.is_String(tool):
747+
if tool.exists(env):
748+
results.append(tool)
749+
continue
750+
t = Tool(tool)
751+
if t.exists(env):
752+
results.append(t)
753+
return results
686754

687755
def tool_list(platform, env):
688756
other_plat_tools = []
@@ -773,7 +841,7 @@ def tool_list(platform, env):
773841

774842
# XXX this logic about what tool provides what should somehow be
775843
# moved into the tool files themselves.
776-
if c_compiler and c_compiler == 'mingw':
844+
if c_compiler and str(c_compiler) == 'mingw':
777845
# MinGW contains a linker, C compiler, C++ compiler,
778846
# Fortran compiler, archiver and assembler:
779847
cxx_compiler = None
@@ -783,7 +851,7 @@ def tool_list(platform, env):
783851
ar = None
784852
else:
785853
# Don't use g++ if the C compiler has built-in C++ support:
786-
if c_compiler in ('msvc', 'intelc', 'icc'):
854+
if str(c_compiler) in ('msvc', 'intelc', 'icc'):
787855
cxx_compiler = None
788856
else:
789857
cxx_compiler = FindTool(cxx_compilers, env) or cxx_compilers[0]

SCons/Tool/default.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,12 @@
2929
selection method.
3030
"""
3131

32+
import SCons.Platform
3233
import SCons.Tool
3334

3435
def generate(env) -> None:
3536
"""Add default tools."""
36-
for t in SCons.Tool.tool_list(env['PLATFORM'], env):
37+
for t in SCons.Platform.DefaultToolList(env['PLATFORM'], env):
3738
SCons.Tool.Tool(t)(env)
3839

3940
def exists(env) -> bool:

0 commit comments

Comments
 (0)