Skip to content

Doc and test tweaks for ENVPath functions #4743

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Aug 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,8 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER
if existing value already contained more than one of added value.
- Tweak runtest.py and test framework to more reliably get the requested
Python binary (for odd Windows setups + Python launcher)
- Improve the wording of AppendENVPath and PrependENVPath in manpage.
- Add more unit tests to internal AppendPath, PrependPath functions.


RELEASE 4.9.1 - Thu, 27 Mar 2025 11:40:20 -0700
Expand Down
4 changes: 4 additions & 0 deletions RELEASE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ DOCUMENTATION

- Improve the wording of Configure methods.

- Improve the wording of AppendENVPath and PrependENVPath in manpage.

DEVELOPMENT
-----------

Expand Down Expand Up @@ -193,6 +195,8 @@ DEVELOPMENT
iterative speedup test from 200 to 250. Increasing the workload
should reduce the likelihood that the ninja tests are slower.

- Add more unit tests to internal AppendPath, PrependPath functions.

Thanks to the following contributors listed below for their contributions to this release.
==========================================================================================
.. code-block:: text
Expand Down
114 changes: 72 additions & 42 deletions SCons/Environment.xml
Original file line number Diff line number Diff line change
Expand Up @@ -778,26 +778,41 @@ See also &f-link-env-AppendUnique;,
</arguments>
<summary>
<para>
Append path elements specified by <parameter>newpath</parameter>
to the given search path string or list <parameter>name</parameter>
in mapping <parameter>envname</parameter> in the &consenv;.
Supplying <parameter>envname</parameter> is optional:
the default is the execution environment &cv-link-ENV;.
Optional <parameter>sep</parameter> is used as the search path separator,
the default is the platform's separator (<systemitem>os.pathsep</systemitem>).
A path element will only appear once.
Any duplicates in <parameter>newpath</parameter> are dropped,
keeping the last appearing (to preserve path order).
If <parameter>delete_existing</parameter>
is <constant>False</constant> (the default)
any addition duplicating an existing path element is ignored;
if <parameter>delete_existing</parameter>
is <constant>True</constant> the existing value will
be dropped and the path element will be added at the end.
To help maintain uniqueness all paths are normalized (using
<systemitem>os.path.normpath</systemitem>
and
<systemitem>os.path.normcase</systemitem>).
Append directory paths from
<parameter>newpath</parameter> to
a search-path entry <parameter>name</parameter>
in &consvar; <parameter>envname</parameter>
in the current enviromment (<parameter>env</parameter>).
If <parameter>envname</parameter> is not given,
the default is <literal>"ENV"</literal>
(see &cv-link-ENV;).
<parameter>envname</parameter> is expected
to refer to a dictionary-like object;
if it does not exist in <parameter>env</parameter>
it will be created as an initially empty dict.
<parameter>newpath</parameter>
may be specified as a string,
a directory node, or a list of strings.
If a string, it may contain multiple paths separated
by the system path separator
(<systemitem>os.pathsep</systemitem>),
or, if specified, by the value of <parameter>sep</parameter>.
Top-relative path strings (starting with <literal>#</literal>) are recognized.
The type of the existing value
of <parameter>name</parameter> is preserved.
</para>

<para>
Paths will only appear once.
Duplicate paths in <parameter>newpath</parameter> are removed,
preserving the last occurrence to maintain path order.
If <parameter>delete_existing</parameter> is true (the default),
existing duplicates are removed before appending,
otherwise, new duplicates are skipped.
During comparisons, paths are normalized, to avoid
issues with case differences (on case-insensitive filesystems)
and with relative paths that may refer back to the same directory.
The stored values are not modified by this process.
</para>

<para>
Expand Down Expand Up @@ -1119,8 +1134,8 @@ env4 = env.Clone(tools=['msvc', MyTool])
The
<parameter>parse_flags</parameter>
keyword argument is also recognized, to allow merging command-line
style arguments into the appropriate construction
variables (see &f-link-env-MergeFlags;).
style arguments into the appropriate &consvars;
(see &f-link-env-MergeFlags;).
</para>

<example_commands>
Expand Down Expand Up @@ -2863,26 +2878,41 @@ and &f-link-env-PrependUnique;.
</arguments>
<summary>
<para>
Prepend path elements specified by <parameter>newpath</parameter>
to the given search path string or list <parameter>name</parameter>
in mapping <parameter>envname</parameter> in the &consenv;.
Supplying <parameter>envname</parameter> is optional:
the default is the execution environment &cv-link-ENV;.
Optional <parameter>sep</parameter> is used as the search path separator,
the default is the platform's separator (<systemitem>os.pathsep</systemitem>).
A path element will only appear once.
Any duplicates in <parameter>newpath</parameter> are dropped,
keeping the first appearing (to preserve path order).
If <parameter>delete_existing</parameter>
is <constant>False</constant>
any addition duplicating an existing path element is ignored;
if <parameter>delete_existing</parameter>
is <constant>True</constant> (the default) the existing value will
be dropped and the path element will be inserted at the beginning.
To help maintain uniqueness all paths are normalized (using
<systemitem>os.path.normpath</systemitem>
and
<systemitem>os.path.normcase</systemitem>).
Prepend directory paths from
<parameter>newpath</parameter> to
a search-path entry <parameter>name</parameter>
in &consvar; <parameter>envname</parameter>
in the current enviromment (<parameter>env</parameter>).
If <parameter>envname</parameter> is not given,
the default is <literal>"ENV"</literal>
(see &cv-link-ENV;).
<parameter>envname</parameter> is expected
to refer to a dictionary-like object;
if it does not exist in <parameter>env</parameter>
it will be created as an initially empty dict.
<parameter>newpath</parameter>
may be specified as a string,
a directory node, or a list of strings.
If a string, it may contain multiple paths separated
by the system path separator
(<systemitem>os.pathsep</systemitem>),
or, if specified, by the value of <parameter>sep</parameter>.
Top-relative path strings (starting with <literal>#</literal>) are recognized.
The type of the existing value
of <parameter>name</parameter> is preserved.
</para>

<para>
Paths will only appear once.
Duplicate paths in <parameter>newpath</parameter> are removed,
preserving the first occurrence to maintain path order.
If <parameter>delete_existing</parameter> is true (the default),
existing duplicates are removed before prepending,
otherwise, new duplicates are skipped.
During comparisons, paths are normalized, to avoid
issues with case differences (on case-insensitive filesystems)
and with relative paths that may refer back to the same directory.
The stored values are not modified by this process.
</para>

<para>
Expand Down
132 changes: 86 additions & 46 deletions SCons/Util/UtilTests.py
Original file line number Diff line number Diff line change
Expand Up @@ -545,55 +545,95 @@ def test_get_native_path(self) -> None:

def test_PrependPath(self) -> None:
"""Test prepending to a path"""
p1: list | str = r'C:\dir\num\one;C:\dir\num\two'
p2: list | str = r'C:\mydir\num\one;C:\mydir\num\two'
# have to include the pathsep here so that the test will work on UNIX too.
p1 = PrependPath(p1, r'C:\dir\num\two', sep=';')
p1 = PrependPath(p1, r'C:\dir\num\three', sep=';')
assert p1 == r'C:\dir\num\three;C:\dir\num\two;C:\dir\num\one', p1

p2 = PrependPath(p2, r'C:\mydir\num\three', sep=';')
p2 = PrependPath(p2, r'C:\mydir\num\one', sep=';')
assert p2 == r'C:\mydir\num\one;C:\mydir\num\three;C:\mydir\num\two', p2

# check (only) first one is kept if there are dupes in new
p3: list | str = r'C:\dir\num\one'
p3 = PrependPath(p3, r'C:\dir\num\two;C:\dir\num\three;C:\dir\num\two', sep=';')
assert p3 == r'C:\dir\num\two;C:\dir\num\three;C:\dir\num\one', p3
# have to specify the pathsep when adding so it's cross-platform
# new duplicates existing - "moves to front"
with self.subTest():
p1: list | str = r'C:\dir\num\one;C:\dir\num\two'
p1 = PrependPath(p1, r'C:\dir\num\two', sep=';')
p1 = PrependPath(p1, r'C:\dir\num\three', sep=';')
self.assertEqual(p1, r'C:\dir\num\three;C:\dir\num\two;C:\dir\num\one')

# ... except with delete_existing false
with self.subTest():
p2: list | str = r'C:\dir\num\one;C:\dir\num\two'
p2 = PrependPath(p2, r'C:\dir\num\two', sep=';', delete_existing=False)
p2 = PrependPath(p2, r'C:\dir\num\three', sep=';', delete_existing=False)
self.assertEqual(p2, r'C:\dir\num\three;C:\dir\num\one;C:\dir\num\two')

# only last one is kept if there are dupes in new
with self.subTest():
p3: list | str = r'C:\dir\num\one'
p3 = PrependPath(p3, r'C:\dir\num\two;C:\dir\num\three;C:\dir\num\two', sep=';')
self.assertEqual(p3, r'C:\dir\num\two;C:\dir\num\three;C:\dir\num\one')

# try prepending a Dir Node
with self.subTest():
p4: list | str = r'C:\dir\num\one'
test = TestCmd.TestCmd(workdir='')
test.subdir('sub')
subdir = test.workpath('sub')
p4 = PrependPath(p4, subdir, sep=';')
self.assertEqual(p4, rf'{subdir};C:\dir\num\one')

# try with initial list, adding string (result stays a list)
with self.subTest():
p5: list = [r'C:\dir\num\one', r'C:\dir\num\two']
p5 = PrependPath(p5, r'C:\dir\num\two', sep=';')
self.assertEqual(p5, [r'C:\dir\num\two', r'C:\dir\num\one'])
p5 = PrependPath(p5, r'C:\dir\num\three', sep=';')
self.assertEqual(p5, [r'C:\dir\num\three', r'C:\dir\num\two', r'C:\dir\num\one'])

# try with initial string, adding list (result stays a string)
with self.subTest():
p6: list | str = r'C:\dir\num\one;C:\dir\num\two'
p6 = PrependPath(p6, [r'C:\dir\num\two', r'C:\dir\num\three'], sep=';')
self.assertEqual(p6, r'C:\dir\num\two;C:\dir\num\three;C:\dir\num\one')


def test_AppendPath(self) -> None:
"""Test appending to a path."""
p1: list | str = r'C:\dir\num\one;C:\dir\num\two'
p2: list | str = r'C:\mydir\num\one;C:\mydir\num\two'
# have to include the pathsep here so that the test will work on UNIX too.
p1 = AppendPath(p1, r'C:\dir\num\two', sep=';')
p1 = AppendPath(p1, r'C:\dir\num\three', sep=';')
assert p1 == r'C:\dir\num\one;C:\dir\num\two;C:\dir\num\three', p1

p2 = AppendPath(p2, r'C:\mydir\num\three', sep=';')
p2 = AppendPath(p2, r'C:\mydir\num\one', sep=';')
assert p2 == r'C:\mydir\num\two;C:\mydir\num\three;C:\mydir\num\one', p2

# check (only) last one is kept if there are dupes in new
p3: list | str = r'C:\dir\num\one'
p3 = AppendPath(p3, r'C:\dir\num\two;C:\dir\num\three;C:\dir\num\two', sep=';')
assert p3 == r'C:\dir\num\one;C:\dir\num\three;C:\dir\num\two', p3

def test_PrependPathPreserveOld(self) -> None:
"""Test prepending to a path while preserving old paths"""
p1: list | str = r'C:\dir\num\one;C:\dir\num\two'
# have to include the pathsep here so that the test will work on UNIX too.
p1 = PrependPath(p1, r'C:\dir\num\two', sep=';', delete_existing=False)
p1 = PrependPath(p1, r'C:\dir\num\three', sep=';')
assert p1 == r'C:\dir\num\three;C:\dir\num\one;C:\dir\num\two', p1

def test_AppendPathPreserveOld(self) -> None:
"""Test appending to a path while preserving old paths"""
p1: list | str = r'C:\dir\num\one;C:\dir\num\two'
# have to include the pathsep here so that the test will work on UNIX too.
p1 = AppendPath(p1, r'C:\dir\num\one', sep=';', delete_existing=False)
p1 = AppendPath(p1, r'C:\dir\num\three', sep=';')
assert p1 == r'C:\dir\num\one;C:\dir\num\two;C:\dir\num\three', p1
# have to specify the pathsep when adding so it's cross-platform
# new duplicates existing - "moves to end"
with self.subTest():
p1: list | str = r'C:\dir\num\one;C:\dir\num\two'
p1 = AppendPath(p1, r'C:\dir\num\two', sep=';')
p1 = AppendPath(p1, r'C:\dir\num\three', sep=';')
self.assertEqual(p1, r'C:\dir\num\one;C:\dir\num\two;C:\dir\num\three')

# ... except with delete_existing false
with self.subTest():
p2: list | str = r'C:\dir\num\one;C:\dir\num\two'
p2 = AppendPath(p1, r'C:\dir\num\one', sep=';', delete_existing=False)
p2 = AppendPath(p1, r'C:\dir\num\three', sep=';')
self.assertEqual(p2, r'C:\dir\num\one;C:\dir\num\two;C:\dir\num\three')

# only last one is kept if there are dupes in new
with self.subTest():
p3: list | str = r'C:\dir\num\one'
p3 = AppendPath(p3, r'C:\dir\num\two;C:\dir\num\three;C:\dir\num\two', sep=';')
self.assertEqual(p3, r'C:\dir\num\one;C:\dir\num\three;C:\dir\num\two')

# try appending a Dir Node
with self.subTest():
p4: list | str = r'C:\dir\num\one'
test = TestCmd.TestCmd(workdir='')
test.subdir('sub')
subdir = test.workpath('sub')
p4 = AppendPath(p4, subdir, sep=';')
self.assertEqual(p4, rf'C:\dir\num\one;{subdir}')

# try with initial list, adding string (result stays a list)
with self.subTest():
p5: list = [r'C:\dir\num\one', r'C:\dir\num\two']
p5 = AppendPath(p5, r'C:\dir\num\two', sep=';')
p5 = AppendPath(p5, r'C:\dir\num\three', sep=';')
self.assertEqual(p5, [r'C:\dir\num\one', r'C:\dir\num\two', r'C:\dir\num\three'])

# try with initia string, adding list (result stays a string)
with self.subTest():
p6: list | str = r'C:\dir\num\one;C:\dir\num\two'
p6 = AppendPath(p6, [r'C:\dir\num\two', r'C:\dir\num\three'], sep=';')
self.assertEqual(p6, r'C:\dir\num\one;C:\dir\num\two;C:\dir\num\three')

def test_addPathIfNotExists(self) -> None:
"""Test the AddPathIfNotExists() function"""
Expand Down
18 changes: 10 additions & 8 deletions SCons/Util/envs.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ def PrependPath(

Will only add any particular path once (leaving the first one it
encounters and ignoring the rest, to preserve path order), and will
:mod:`os.path.normpath` and :mod:`os.path.normcase` all paths to help
assure this. This can also handle the case where *oldpath*
is a list instead of a string, in which case a list will be returned
instead of a string. For example:
:mod:`os.path.normpath` and :mod:`os.path.normcase` all paths
for comparisons to help assure this. *oldpath* may be a list
instead of a string, in which case a list is returned.

Example:

>>> p = PrependPath("/foo/bar:/foo", "/biz/boom:/foo")
>>> print(p)
Expand Down Expand Up @@ -120,10 +121,11 @@ def AppendPath(

Will only add any particular path once (leaving the last one it
encounters and ignoring the rest, to preserve path order), and will
:mod:`os.path.normpath` and :mod:`os.path.normcase` all paths to help
assure this. This can also handle the case where *oldpath*
is a list instead of a string, in which case a list will be returned
instead of a string. For example:
:mod:`os.path.normpath` and :mod:`os.path.normcase` all paths
for comparisons to help assure this. *oldpath* may be a list
instead of a string, in which case a list is returned.

Example:

>>> p = AppendPath("/foo/bar:/foo", "/biz/boom:/foo")
>>> print(p)
Expand Down
Loading