Skip to content

Commit f56175b

Browse files
committed
Add internal routines to remove args and targets
Adds SCons.Script._Remove_Target and SCons.Script._Remove_Argument to allow taking away values placed the public attributes BUILD_TARGETS, COMMAND_LINE_TARGETS, ARGUMENTS and ARGLIST. Part two of three harvesting from old PR SCons#3799 (the short-option piece was merged as part of SCons#4598). Intended customer will be the Options logic, Unit tests created, also for existing SCons.Script._Add_Targets and SCons.Script._Add_Arguments. Signed-off-by: Mats Wichmann <[email protected]>
1 parent 92dfd05 commit f56175b

File tree

4 files changed

+247
-7
lines changed

4 files changed

+247
-7
lines changed

CHANGES.txt

+6
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER
4040
one from PEP 308 introduced in Python 2.5 (2006). The idiom being
4141
replaced (using and/or) is regarded as error prone.
4242
- Improve the description of PackageVariable.
43+
- Add internal routines _Remove_Targets and _Remove_Arguments to
44+
allow taking away values placed the public attributes BUILD_TARGETS,
45+
COMMAND_LINE_TARGETS, ARGUMENTS and ARGLIST. This is a step towards
46+
fixing the handling of option-arguments specified with a space
47+
separator (multiple issues, harvested from #3799). These interfaces
48+
are not part of the public API.
4349

4450

4551
RELEASE 4.9.1 - Thu, 27 Mar 2025 11:40:20 -0700

RELEASE.txt

+3
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ IMPROVEMENTS
4848
documentation: performance improvements (describe the circumstances
4949
under which they would be observed), or major code cleanups
5050

51+
- Add internal routines to maniplutate publicly visible argument and
52+
target lists. These interfaces are not part of the public API.
53+
5154
PACKAGING
5255
---------
5356

SCons/Script/ScriptTests.py

+181
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# MIT License
2+
#
3+
# Copyright The SCons Foundation
4+
#
5+
# Permission is hereby granted, free of charge, to any person obtaining
6+
# a copy of this software and associated documentation files (the
7+
# "Software"), to deal in the Software without restriction, including
8+
# without limitation the rights to use, copy, modify, merge, publish,
9+
# distribute, sublicense, and/or sell copies of the Software, and to
10+
# permit persons to whom the Software is furnished to do so, subject to
11+
# the following conditions:
12+
13+
# The above copyright notice and this permission notice shall be included
14+
# in all copies or substantial portions of the Software.
15+
#
16+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17+
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23+
24+
# Unit tests of functionality from SCons.Script._init__.py.
25+
#
26+
# Most of the tests of this functionality are actually end-to-end scripts
27+
# in the test/ hierarchy.
28+
#
29+
# This module is for specific bits of functionality that seem worth
30+
# testing here - particularly if there's private data involved.
31+
32+
import unittest
33+
34+
from SCons.Script import (
35+
_Add_Arguments,
36+
_Add_Targets,
37+
_Remove_Argument,
38+
_Remove_Target,
39+
ARGLIST,
40+
ARGUMENTS,
41+
BUILD_TARGETS,
42+
COMMAND_LINE_TARGETS,
43+
_build_plus_default,
44+
)
45+
46+
47+
class TestScriptFunctions(unittest.TestCase):
48+
def setUp(self):
49+
# Clear global state before each test
50+
ARGUMENTS.clear()
51+
ARGLIST.clear()
52+
del COMMAND_LINE_TARGETS[:]
53+
del BUILD_TARGETS[:]
54+
del _build_plus_default[:]
55+
56+
def test_Add_Arguments(self):
57+
test_args = ['foo=bar', 'spam=eggs']
58+
59+
_Add_Arguments(test_args)
60+
self.assertEqual(ARGUMENTS, {'foo': 'bar', 'spam': 'eggs'})
61+
self.assertEqual(ARGLIST, [('foo', 'bar'), ('spam', 'eggs')])
62+
63+
def test_Add_Arguments_empty(self):
64+
# Adding am empty argument is a no-op, with no error
65+
_Add_Arguments([])
66+
self.assertEqual(ARGUMENTS, {})
67+
self.assertEqual(ARGLIST, [])
68+
69+
def test_Add_Targets(self):
70+
test_targets = ['target1', 'target2']
71+
_Add_Targets(test_targets)
72+
73+
self.assertEqual(COMMAND_LINE_TARGETS, ['target1', 'target2'])
74+
self.assertEqual(BUILD_TARGETS, ['target1', 'target2'])
75+
self.assertEqual(_build_plus_default, ['target1', 'target2'])
76+
77+
# Test that methods were replaced
78+
self.assertEqual(BUILD_TARGETS._add_Default, BUILD_TARGETS._do_nothing)
79+
self.assertEqual(BUILD_TARGETS._clear, BUILD_TARGETS._do_nothing)
80+
self.assertEqual(
81+
_build_plus_default._add_Default, _build_plus_default._do_nothing
82+
)
83+
self.assertEqual(
84+
_build_plus_default._clear, _build_plus_default._do_nothing
85+
)
86+
87+
def test_Add_Targets_empty(self):
88+
# Adding am empty argument is a no-op, with no error
89+
_Add_Targets([])
90+
self.assertEqual(COMMAND_LINE_TARGETS, [])
91+
self.assertEqual(BUILD_TARGETS, [])
92+
self.assertEqual(_build_plus_default, [])
93+
94+
def test_Remove_Argument(self):
95+
ARGLIST.extend([
96+
('key1', 'value1'),
97+
('key2', 'value2')
98+
])
99+
ARGUMENTS.update({'key1': 'value1', 'key2': 'value2'})
100+
101+
_Remove_Argument('key1=value1')
102+
self.assertEqual(ARGUMENTS, {'key2': 'value2'})
103+
self.assertEqual(ARGLIST, [('key2', 'value2')])
104+
105+
def test_Remove_Argument_key_with_multiple_values(self):
106+
ARGLIST.extend([
107+
('key1', 'value1'),
108+
('key1', 'value2')
109+
])
110+
ARGUMENTS['key1'] = 'value2' # ARGUMENTS only keeps last, emulate
111+
112+
_Remove_Argument('key1=value1')
113+
self.assertEqual(ARGLIST, [('key1', 'value2')])
114+
# ARGUMENTS must be reconstructed
115+
self.assertEqual(ARGUMENTS, {'key1': 'value2'})
116+
117+
def test_Remove_Argument_nonexistent(self):
118+
# Removing a nonexistent argument is a no-op with no error
119+
ARGUMENTS['key1'] = 'value1'
120+
ARGLIST.append(('key1', 'value1'))
121+
122+
_Remove_Argument('nonexistent=value')
123+
self.assertEqual(ARGUMENTS, {'key1': 'value1'})
124+
self.assertEqual(ARGLIST, [('key1', 'value1')])
125+
126+
def test_Remove_Argument_empty(self):
127+
# Removing an empty argument is also a no-op with no error
128+
ARGUMENTS['key1'] = 'value1'
129+
ARGLIST.append(('key1', 'value1'))
130+
131+
_Remove_Argument('')
132+
self.assertEqual(ARGUMENTS, {'key1': 'value1'})
133+
self.assertEqual(ARGLIST, [('key1', 'value1')])
134+
135+
# XXX where does TARGETS come in?
136+
def test_Remove_Target(self):
137+
BUILD_TARGETS.extend(['target1', 'target2', 'target3'])
138+
COMMAND_LINE_TARGETS.extend(['target1', 'target2', 'target3'])
139+
140+
_Remove_Target('target2')
141+
self.assertEqual(BUILD_TARGETS, ['target1', 'target3'])
142+
self.assertEqual(COMMAND_LINE_TARGETS, ['target1', 'target3'])
143+
144+
def test_Remove_Target_duplicated(self):
145+
# Targets can be duplicated, only one should be removed
146+
# There is not a good way to determine which instance was added
147+
# "in error" so all we can do is check *something* was removed.
148+
BUILD_TARGETS.extend(['target1', 'target1'])
149+
COMMAND_LINE_TARGETS.extend(['target1', 'target1'])
150+
151+
_Remove_Target('target1')
152+
self.assertEqual(BUILD_TARGETS, ['target1'])
153+
self.assertEqual(COMMAND_LINE_TARGETS, ['target1'])
154+
155+
def test_Remove_Target_nonexistent(self):
156+
# Asking to remove a nonexistent argument is a no-op with no error
157+
BUILD_TARGETS.append('target1')
158+
COMMAND_LINE_TARGETS.append('target1')
159+
160+
_Remove_Target('nonexistent')
161+
self.assertEqual(BUILD_TARGETS, ['target1'])
162+
self.assertEqual(COMMAND_LINE_TARGETS, ['target1'])
163+
164+
def test_Remove_Target_empty(self):
165+
# Asking to remove an empty argument is also a no-op with no error
166+
BUILD_TARGETS.append('target1')
167+
COMMAND_LINE_TARGETS.append('target1')
168+
169+
_Remove_Target('')
170+
self.assertEqual(BUILD_TARGETS, ['target1'])
171+
self.assertEqual(COMMAND_LINE_TARGETS, ['target1'])
172+
173+
174+
if __name__ == '__main__':
175+
unittest.main()
176+
177+
# Local Variables:
178+
# tab-width:4
179+
# indent-tabs-mode:nil
180+
# End:
181+
# vim: set expandtab tabstop=4 shiftwidth=4:

SCons/Script/__init__.py

+57-7
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,16 @@
3030
some other module. If it's specific to the "scons" script invocation,
3131
it goes here.
3232
"""
33-
34-
import time
35-
start_time = time.time()
33+
from __future__ import annotations
3634

3735
import collections
3836
import itertools
3937
import os
38+
import sys
39+
import time
4040
from io import StringIO
4141

42-
import sys
42+
start_time = time.time()
4343

4444
# Special chicken-and-egg handling of the "--debug=memoizer" flag:
4545
#
@@ -135,7 +135,7 @@
135135
#profiling = Main.profiling
136136
#repositories = Main.repositories
137137

138-
from . import SConscript as _SConscript
138+
from . import SConscript as _SConscript # pylint: disable=import-outside-toplevel
139139

140140
call_stack = _SConscript.call_stack
141141

@@ -208,13 +208,15 @@ def _clear(self) -> None:
208208
# own targets to BUILD_TARGETS.
209209
_build_plus_default = TargetList()
210210

211-
def _Add_Arguments(alist) -> None:
211+
def _Add_Arguments(alist: list[str]) -> None:
212+
"""Add value(s) to ``ARGLIST`` and ``ARGUMENTS``."""
212213
for arg in alist:
213214
a, b = arg.split('=', 1)
214215
ARGUMENTS[a] = b
215216
ARGLIST.append((a, b))
216217

217-
def _Add_Targets(tlist) -> None:
218+
def _Add_Targets(tlist: list[str]) -> None:
219+
"""Add value(s) to ``COMMAND_LINE_TARGETS`` and ``BUILD_TARGETS``."""
218220
if tlist:
219221
COMMAND_LINE_TARGETS.extend(tlist)
220222
BUILD_TARGETS.extend(tlist)
@@ -224,6 +226,54 @@ def _Add_Targets(tlist) -> None:
224226
_build_plus_default._add_Default = _build_plus_default._do_nothing
225227
_build_plus_default._clear = _build_plus_default._do_nothing
226228

229+
def _Remove_Argument(aarg: str) -> None:
230+
"""Remove *aarg* from ``ARGLIST`` and ``ARGUMENTS``.
231+
232+
Used to remove a variables-style argument that is no longer valid.
233+
This can happpen because the command line is processed once early,
234+
before we see any :func:`SCons.Script.Main.AddOption` calls, so we
235+
could not recognize it belongs to an option and is not a standalone
236+
variable=value argument.
237+
238+
.. versionaddedd:: NEXT_RELEASE
239+
"""
240+
if aarg:
241+
a, b = aarg.split('=', 1)
242+
243+
# remove from ARGLIST first which would contain duplicates if
244+
# -x A=B A=B was specified on the CL
245+
if (a, b) in ARGLIST:
246+
ARGLIST.remove((a, b))
247+
248+
# Remove first in case no matching values left in ARGLIST
249+
ARGUMENTS.pop(a, None)
250+
# Set ARGUMENTS[A] back to latest value in ARGLIST
251+
# (assuming order matches CL order)
252+
for item in ARGLIST:
253+
if item[0] == a:
254+
ARGUMENTS[a] = item[1]
255+
256+
def _Remove_Target(targ: str) -> None:
257+
"""Remove *targ* from ``BUILD_TARGETS`` and ``COMMAND_LINE_TARGETS``.
258+
259+
Used to remove a target that is no longer valid. This can happpen
260+
because the command line is processed once early, before we see any
261+
:func:`SCons.Script.Main.AddOption` calls, so we could not recognize
262+
it belongs to an option standalone target argument.
263+
264+
Since we are "correcting an error", we also have to fix up the internal
265+
:data:`_build_plus_default` list.
266+
267+
.. versionaddedd:: NEXT_RELEASE
268+
"""
269+
if targ:
270+
if targ in COMMAND_LINE_TARGETS:
271+
COMMAND_LINE_TARGETS.remove(targ)
272+
if targ in BUILD_TARGETS:
273+
BUILD_TARGETS.remove(targ)
274+
if targ in _build_plus_default:
275+
_build_plus_default.remove(targ)
276+
227277
def _Set_Default_Targets_Has_Been_Called(d, fs):
228278
return DEFAULT_TARGETS
229279

0 commit comments

Comments
 (0)