Skip to content

Commit 84f4364

Browse files
committed
Fix bug with unique adds and delete_existing
AppendUnique and PrependUnique, when called with delete_existing true, had a small logic flaw: it might remove an existing value if the value to be added is a scalar (string, most likely) and there was a substring match. The code needs to do an equality test, not a membership test. Unit tests updated and got some reformatting, plus dropped a duplicate definition of reserved_variables. Signed-off-by: Mats Wichmann <mats@linux.com>
1 parent 876bb06 commit 84f4364

File tree

4 files changed

+87
-52
lines changed

4 files changed

+87
-52
lines changed

CHANGES.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER
4040
- Improve wording of manpage "Functions and Environment Methods" section.
4141
Make doc function signature style more consistent - tweaks to AddOption,
4242
DefaultEnvironment and Tool,.
43+
- Fix a problem with AppendUnique and PrependUnique where a value could
44+
be erroneously removed due to a substring match.
4345

4446

4547
RELEASE 4.8.0 - Sun, 07 Jul 2024 17:22:20 -0700

RELEASE.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ FIXES
4646
to be avaiable. `BoolVariable`, `EnumVariable`, `ListVariable`,
4747
`PackageVariable` and `PathVariable` are added to `__all__`,
4848
so this form of import should now work again.
49+
- Fix a problem with AppendUnique and PrependUnique where a value could
50+
be erroneously removed due to a substring match.
4951

5052
IMPROVEMENTS
5153
------------

SCons/Environment.py

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1512,11 +1512,17 @@ def AppendENVPath(self, name, newpath, envname: str='ENV',
15121512

15131513
self._dict[envname][name] = nv
15141514

1515-
def AppendUnique(self, delete_existing: bool=False, **kw) -> None:
1516-
"""Append values to existing construction variables
1517-
in an Environment, if they're not already there.
1518-
If delete_existing is True, removes existing values first, so
1519-
values move to end.
1515+
def AppendUnique(self, delete_existing: bool = False, **kw) -> None:
1516+
"""Append values uniquely to existing construction variables.
1517+
1518+
Similar to :meth:`Append`, but the result may not contain duplicates
1519+
of any values passed for each given key (construction variable),
1520+
so an existing list may need to be pruned first, however it may still
1521+
contain other duplicates.
1522+
1523+
If *delete_existing* is true, removes existing values first, so values
1524+
move to the end; otherwise (the default) values are skipped if
1525+
already present.
15201526
"""
15211527
kw = copy_non_reserved_keywords(kw)
15221528
for key, val in kw.items():
@@ -1539,12 +1545,11 @@ def AppendUnique(self, delete_existing: bool=False, **kw) -> None:
15391545
val = [x for x in val if x not in dk]
15401546
self._dict[key] = dk + val
15411547
else:
1548+
# val is not a list, so presumably a scalar (likely str).
15421549
dk = self._dict[key]
15431550
if is_List(dk):
1544-
# By elimination, val is not a list. Since dk is a
1545-
# list, wrap val in a list first.
15461551
if delete_existing:
1547-
dk = list(filter(lambda x, val=val: x not in val, dk))
1552+
dk = [x for x in dk if x != val]
15481553
self._dict[key] = dk + [val]
15491554
else:
15501555
if val not in dk:
@@ -1939,11 +1944,17 @@ def PrependENVPath(self, name, newpath, envname: str='ENV',
19391944

19401945
self._dict[envname][name] = nv
19411946

1942-
def PrependUnique(self, delete_existing: bool=False, **kw) -> None:
1943-
"""Prepend values to existing construction variables
1944-
in an Environment, if they're not already there.
1945-
If delete_existing is True, removes existing values first, so
1946-
values move to front.
1947+
def PrependUnique(self, delete_existing: bool = False, **kw) -> None:
1948+
"""Prepend values uniquely to existing construction variables.
1949+
1950+
Similar to :meth:`Prepend`, but the result may not contain duplicates
1951+
of any values passed for each given key (construction variable),
1952+
so an existing list may need to be pruned first, however it may still
1953+
contain other duplicates.
1954+
1955+
If *delete_existing* is true, removes existing values first, so values
1956+
move to the front; otherwise (the default) values are skipped if
1957+
already present.
19471958
"""
19481959
kw = copy_non_reserved_keywords(kw)
19491960
for key, val in kw.items():
@@ -1966,12 +1977,11 @@ def PrependUnique(self, delete_existing: bool=False, **kw) -> None:
19661977
val = [x for x in val if x not in dk]
19671978
self._dict[key] = val + dk
19681979
else:
1980+
# val is not a list, so presumably a scalar (likely str).
19691981
dk = self._dict[key]
19701982
if is_List(dk):
1971-
# By elimination, val is not a list. Since dk is a
1972-
# list, wrap val in a list first.
19731983
if delete_existing:
1974-
dk = [x for x in dk if x not in val]
1984+
dk = [x for x in dk if x != val]
19751985
self._dict[key] = [val] + dk
19761986
else:
19771987
if val not in dk:

SCons/EnvironmentTests.py

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,18 +1193,6 @@ def test_ENV(self) -> None:
11931193

11941194
def test_ReservedVariables(self) -> None:
11951195
"""Test warning generation when reserved variable names are set"""
1196-
1197-
reserved_variables = [
1198-
'CHANGED_SOURCES',
1199-
'CHANGED_TARGETS',
1200-
'SOURCE',
1201-
'SOURCES',
1202-
'TARGET',
1203-
'TARGETS',
1204-
'UNCHANGED_SOURCES',
1205-
'UNCHANGED_TARGETS',
1206-
]
1207-
12081196
warning = SCons.Warnings.ReservedVariableWarning
12091197
SCons.Warnings.enableWarningClass(warning)
12101198
old = SCons.Warnings.warningAsException(1)
@@ -1759,19 +1747,26 @@ def test_AppendENVPath(self) -> None:
17591747
ENV={'PATH': r'C:\dir\num\one;C:\dir\num\two'},
17601748
MYENV={'MYPATH': r'C:\mydir\num\one;C:\mydir\num\two'},
17611749
)
1750+
17621751
# have to include the pathsep here so that the test will work on UNIX too.
17631752
env1.AppendENVPath('PATH', r'C:\dir\num\two', sep=';')
17641753
env1.AppendENVPath('PATH', r'C:\dir\num\three', sep=';')
1765-
env1.AppendENVPath('MYPATH', r'C:\mydir\num\three', 'MYENV', sep=';')
17661754
assert (
17671755
env1['ENV']['PATH'] == r'C:\dir\num\one;C:\dir\num\two;C:\dir\num\three'
17681756
), env1['ENV']['PATH']
17691757

1758+
# add nonexisting - at end
17701759
env1.AppendENVPath('MYPATH', r'C:\mydir\num\three', 'MYENV', sep=';')
1760+
assert (
1761+
env1['MYENV']['MYPATH'] == r'C:\mydir\num\one;C:\mydir\num\two;C:\mydir\num\three'
1762+
), env1['MYENV']['MYPATH']
1763+
1764+
# add existing with delete_existing true - moves to the end
17711765
env1.AppendENVPath(
1772-
'MYPATH', r'C:\mydir\num\one', 'MYENV', sep=';', delete_existing=1
1766+
'MYPATH', r'C:\mydir\num\one', 'MYENV', sep=';', delete_existing=True
17731767
)
1774-
# this should do nothing since delete_existing is 0
1768+
# this should do nothing since delete_existing is false (the default)
1769+
env1.AppendENVPath('MYPATH', r'C:\mydir\num\three', 'MYENV', sep=';')
17751770
assert (
17761771
env1['MYENV']['MYPATH'] == r'C:\mydir\num\two;C:\mydir\num\three;C:\mydir\num\one'
17771772
), env1['MYENV']['MYPATH']
@@ -1783,6 +1778,7 @@ def test_AppendENVPath(self) -> None:
17831778
env1.AppendENVPath('PATH', env1.fs.Dir('sub2'), sep=';')
17841779
assert env1['ENV']['PATH'] == p + ';sub1;sub2', env1['ENV']['PATH']
17851780

1781+
17861782
def test_AppendUnique(self) -> None:
17871783
"""Test appending to unique values to construction variables
17881784
@@ -1832,34 +1828,46 @@ def test_AppendUnique(self) -> None:
18321828
assert env['CCC1'] == 'c1', env['CCC1']
18331829
assert env['CCC2'] == ['c2'], env['CCC2']
18341830
assert env['DDD1'] == ['a', 'b', 'c'], env['DDD1']
1835-
assert env['LL1'] == [env.Literal('a literal'), env.Literal('b literal')], env['LL1']
1836-
assert env['LL2'] == [env.Literal('c literal'), env.Literal('b literal'), env.Literal('a literal')], [str(x) for x in env['LL2']]
1831+
assert env['LL1'] == [env.Literal('a literal'), \
1832+
env.Literal('b literal')], env['LL1']
1833+
assert env['LL2'] == [
1834+
env.Literal('c literal'),
1835+
env.Literal('b literal'),
1836+
env.Literal('a literal'),
1837+
], [str(x) for x in env['LL2']]
1838+
1839+
env.AppendUnique(DDD1='b', delete_existing=True)
1840+
assert env['DDD1'] == ['a', 'c', 'b'], env['DDD1'] # b moves to end
18371841

1838-
env.AppendUnique(DDD1 = 'b', delete_existing=1)
1839-
assert env['DDD1'] == ['a', 'c', 'b'], env['DDD1'] # b moves to end
1840-
env.AppendUnique(DDD1 = ['a','b'], delete_existing=1)
1841-
assert env['DDD1'] == ['c', 'a', 'b'], env['DDD1'] # a & b move to end
1842-
env.AppendUnique(DDD1 = ['e','f', 'e'], delete_existing=1)
1843-
assert env['DDD1'] == ['c', 'a', 'b', 'f', 'e'], env['DDD1'] # add last
1842+
env.AppendUnique(DDD1=['a', 'b'], delete_existing=True)
1843+
assert env['DDD1'] == ['c', 'a', 'b'], env['DDD1'] # a & b move to end
1844+
1845+
env.AppendUnique(DDD1=['e', 'f', 'e'], delete_existing=True)
1846+
assert env['DDD1'] == ['c', 'a', 'b', 'f', 'e'], env['DDD1'] # add last
1847+
1848+
# issue regression: substrings should not be deleted
1849+
env.AppendUnique(BBB4='b4.newer', delete_existing=True)
1850+
assert env['BBB4'] == ['b4', 'b4.new', 'b4.newer'], env['BBB4']
18441851

18451852
env['CLVar'] = CLVar([])
1846-
env.AppendUnique(CLVar = 'bar')
1853+
env.AppendUnique(CLVar='bar')
18471854
result = env['CLVar']
18481855
assert isinstance(result, CLVar), repr(result)
18491856
assert result == ['bar'], result
18501857

18511858
env['CLVar'] = CLVar(['abc'])
1852-
env.AppendUnique(CLVar = 'bar')
1859+
env.AppendUnique(CLVar='bar')
18531860
result = env['CLVar']
18541861
assert isinstance(result, CLVar), repr(result)
18551862
assert result == ['abc', 'bar'], result
18561863

18571864
env['CLVar'] = CLVar(['bar'])
1858-
env.AppendUnique(CLVar = 'bar')
1865+
env.AppendUnique(CLVar='bar')
18591866
result = env['CLVar']
18601867
assert isinstance(result, CLVar), repr(result)
18611868
assert result == ['bar'], result
18621869

1870+
18631871
def test_Clone(self) -> None:
18641872
"""Test construction environment cloning.
18651873
@@ -2501,18 +2509,26 @@ def test_PrependENVPath(self) -> None:
25012509
ENV={'PATH': r'C:\dir\num\one;C:\dir\num\two'},
25022510
MYENV={'MYPATH': r'C:\mydir\num\one;C:\mydir\num\two'},
25032511
)
2512+
25042513
# have to include the pathsep here so that the test will work on UNIX too.
25052514
env1.PrependENVPath('PATH', r'C:\dir\num\two', sep=';')
25062515
env1.PrependENVPath('PATH', r'C:\dir\num\three', sep=';')
25072516
assert (
25082517
env1['ENV']['PATH'] == r'C:\dir\num\three;C:\dir\num\two;C:\dir\num\one'
25092518
), env1['ENV']['PATH']
25102519

2520+
2521+
# add nonexisting - at front
25112522
env1.PrependENVPath('MYPATH', r'C:\mydir\num\three', 'MYENV', sep=';')
2523+
assert (
2524+
env1['MYENV']['MYPATH'] == r'C:\mydir\num\three;C:\mydir\num\one;C:\mydir\num\two'
2525+
), env1['MYENV']['MYPATH']
2526+
2527+
# add existing - moves to the front
25122528
env1.PrependENVPath('MYPATH', r'C:\mydir\num\one', 'MYENV', sep=';')
2513-
# this should do nothing since delete_existing is 0
2529+
# this should do nothing since delete_existing is false
25142530
env1.PrependENVPath(
2515-
'MYPATH', r'C:\mydir\num\three', 'MYENV', sep=';', delete_existing=0
2531+
'MYPATH', r'C:\mydir\num\three', 'MYENV', sep=';', delete_existing=False
25162532
)
25172533
assert (
25182534
env1['MYENV']['MYPATH'] == r'C:\mydir\num\one;C:\mydir\num\three;C:\mydir\num\two'
@@ -2525,6 +2541,7 @@ def test_PrependENVPath(self) -> None:
25252541
env1.PrependENVPath('PATH', env1.fs.Dir('sub2'), sep=';')
25262542
assert env1['ENV']['PATH'] == 'sub2;sub1;' + p, env1['ENV']['PATH']
25272543

2544+
25282545
def test_PrependUnique(self) -> None:
25292546
"""Test prepending unique values to construction variables
25302547
@@ -2570,32 +2587,36 @@ def test_PrependUnique(self) -> None:
25702587
assert env['CCC2'] == ['c2'], env['CCC2']
25712588
assert env['DDD1'] == ['a', 'b', 'c'], env['DDD1']
25722589

2573-
env.PrependUnique(DDD1 = 'b', delete_existing=1)
2574-
assert env['DDD1'] == ['b', 'a', 'c'], env['DDD1'] # b moves to front
2575-
env.PrependUnique(DDD1 = ['a','c'], delete_existing=1)
2576-
assert env['DDD1'] == ['a', 'c', 'b'], env['DDD1'] # a & c move to front
2577-
env.PrependUnique(DDD1 = ['d','e','d'], delete_existing=1)
2590+
env.PrependUnique(DDD1='b', delete_existing=True)
2591+
assert env['DDD1'] == ['b', 'a', 'c'], env['DDD1'] # b moves to front
2592+
env.PrependUnique(DDD1=['a', 'c'], delete_existing=True)
2593+
assert env['DDD1'] == ['a', 'c', 'b'], env['DDD1'] # a & c move to front
2594+
env.PrependUnique(DDD1=['d', 'e', 'd'], delete_existing=True)
25782595
assert env['DDD1'] == ['d', 'e', 'a', 'c', 'b'], env['DDD1']
25792596

2597+
# issue regression: substrings should not be deleted
2598+
env.PrependUnique(BBB4='b4.newer', delete_existing=True)
2599+
assert env['BBB4'] == ['b4.newer', 'b4.new', 'b4'], env['BBB4']
25802600

25812601
env['CLVar'] = CLVar([])
2582-
env.PrependUnique(CLVar = 'bar')
2602+
env.PrependUnique(CLVar='bar')
25832603
result = env['CLVar']
25842604
assert isinstance(result, CLVar), repr(result)
25852605
assert result == ['bar'], result
25862606

25872607
env['CLVar'] = CLVar(['abc'])
2588-
env.PrependUnique(CLVar = 'bar')
2608+
env.PrependUnique(CLVar='bar')
25892609
result = env['CLVar']
25902610
assert isinstance(result, CLVar), repr(result)
25912611
assert result == ['bar', 'abc'], result
25922612

25932613
env['CLVar'] = CLVar(['bar'])
2594-
env.PrependUnique(CLVar = 'bar')
2614+
env.PrependUnique(CLVar='bar')
25952615
result = env['CLVar']
25962616
assert isinstance(result, CLVar), repr(result)
25972617
assert result == ['bar'], result
25982618

2619+
25992620
def test_Replace(self) -> None:
26002621
"""Test replacing construction variables in an Environment
26012622

0 commit comments

Comments
 (0)