diff --git a/CHANGES.txt b/CHANGES.txt index 0c2f11fbe..6316df5ca 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -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 diff --git a/RELEASE.txt b/RELEASE.txt index 2cbba03ee..531cf6050 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -143,6 +143,8 @@ DOCUMENTATION - Improve the wording of Configure methods. +- Improve the wording of AppendENVPath and PrependENVPath in manpage. + DEVELOPMENT ----------- @@ -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 diff --git a/SCons/Environment.xml b/SCons/Environment.xml index 296a890b8..fda4c464a 100644 --- a/SCons/Environment.xml +++ b/SCons/Environment.xml @@ -778,26 +778,41 @@ See also &f-link-env-AppendUnique;, -Append path elements specified by newpath -to the given search path string or list name -in mapping envname in the &consenv;. -Supplying envname is optional: -the default is the execution environment &cv-link-ENV;. -Optional sep is used as the search path separator, -the default is the platform's separator (os.pathsep). -A path element will only appear once. -Any duplicates in newpath are dropped, -keeping the last appearing (to preserve path order). -If delete_existing -is False (the default) -any addition duplicating an existing path element is ignored; -if delete_existing -is True 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 -os.path.normpath -and -os.path.normcase). +Append directory paths from +newpath to +a search-path entry name +in &consvar; envname +in the current enviromment (env). +If envname is not given, +the default is "ENV" +(see &cv-link-ENV;). +envname is expected +to refer to a dictionary-like object; +if it does not exist in env +it will be created as an initially empty dict. +newpath +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 +(os.pathsep), +or, if specified, by the value of sep. +Top-relative path strings (starting with #) are recognized. +The type of the existing value +of name is preserved. + + + +Paths will only appear once. +Duplicate paths in newpath are removed, +preserving the last occurrence to maintain path order. +If delete_existing 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. @@ -1119,8 +1134,8 @@ env4 = env.Clone(tools=['msvc', MyTool]) The parse_flags 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;). @@ -2863,26 +2878,41 @@ and &f-link-env-PrependUnique;. -Prepend path elements specified by newpath -to the given search path string or list name -in mapping envname in the &consenv;. -Supplying envname is optional: -the default is the execution environment &cv-link-ENV;. -Optional sep is used as the search path separator, -the default is the platform's separator (os.pathsep). -A path element will only appear once. -Any duplicates in newpath are dropped, -keeping the first appearing (to preserve path order). -If delete_existing -is False -any addition duplicating an existing path element is ignored; -if delete_existing -is True (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 -os.path.normpath -and -os.path.normcase). +Prepend directory paths from +newpath to +a search-path entry name +in &consvar; envname +in the current enviromment (env). +If envname is not given, +the default is "ENV" +(see &cv-link-ENV;). +envname is expected +to refer to a dictionary-like object; +if it does not exist in env +it will be created as an initially empty dict. +newpath +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 +(os.pathsep), +or, if specified, by the value of sep. +Top-relative path strings (starting with #) are recognized. +The type of the existing value +of name is preserved. + + + +Paths will only appear once. +Duplicate paths in newpath are removed, +preserving the first occurrence to maintain path order. +If delete_existing 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. diff --git a/SCons/Util/UtilTests.py b/SCons/Util/UtilTests.py index 88ff923a6..581d435e8 100644 --- a/SCons/Util/UtilTests.py +++ b/SCons/Util/UtilTests.py @@ -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""" diff --git a/SCons/Util/envs.py b/SCons/Util/envs.py index 9c97da6e9..d71bbee9b 100644 --- a/SCons/Util/envs.py +++ b/SCons/Util/envs.py @@ -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) @@ -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) diff --git a/doc/generated/functions.gen b/doc/generated/functions.gen index 72d3aa4c3..e54063f4a 100644 --- a/doc/generated/functions.gen +++ b/doc/generated/functions.gen @@ -707,26 +707,40 @@ See also &f-link-env-AppendUnique;, env.AppendENVPath(name, newpath, [envname, sep, delete_existing=False]) -Append path elements specified by newpath -to the given search path string or list name -in mapping envname in the &consenv;. -Supplying envname is optional: -the default is the execution environment &cv-link-ENV;. -Optional sep is used as the search path separator, -the default is the platform's separator (os.pathsep). -A path element will only appear once. -Any duplicates in newpath are dropped, -keeping the last appearing (to preserve path order). -If delete_existing -is False (the default) -any addition duplicating an existing path element is ignored; -if delete_existing -is True 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 -os.path.normpath -and -os.path.normcase). +Append directory paths from +newpath to +a search-path entry name +in &consvar; envname +in the current enviromment (env). +If envname is not given, +the default is "ENV" +(see &cv-link-ENV;). +envname is expected +to refer to a dictionary-like object; +if it does not exist in env +it will be created as an initially empty dict. +newpath +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 +(os.pathsep), +or, if specified, by the value of sep. +Top-relative path strings (starting with #) are recognized. +The type of the existing value +of name is preserved. + + + +Paths will only appear once. +Duplicate paths in newpath are removed, +preserving the last occurrence to maintain path order. +If delete_existing is true (the default), +existing duplicates are removed before appending, +otherwise, new duplicates are skipped. +Paths are normalized during comparisons to avoid problems of +"same path, spelled differently", +but retain their original form in the result. @@ -1031,8 +1045,8 @@ env4 = env.Clone(tools=['msvc', MyTool]) The parse_flags 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;). @@ -3381,26 +3395,40 @@ and &f-link-env-PrependUnique;. env.PrependENVPath(name, newpath, [envname, sep, delete_existing=True]) -Prepend path elements specified by newpath -to the given search path string or list name -in mapping envname in the &consenv;. -Supplying envname is optional: -the default is the execution environment &cv-link-ENV;. -Optional sep is used as the search path separator, -the default is the platform's separator (os.pathsep). -A path element will only appear once. -Any duplicates in newpath are dropped, -keeping the first appearing (to preserve path order). -If delete_existing -is False -any addition duplicating an existing path element is ignored; -if delete_existing -is True (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 -os.path.normpath -and -os.path.normcase). +Prepend directory paths from +newpath to +a search-path entry name +in &consvar; envname +in the current enviromment (env). +If envname is not given, +the default is "ENV" +(see &cv-link-ENV;). +envname is expected +to refer to a dictionary-like object; +if it does not exist in env +it will be created as an initially empty dict. +newpath +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 +(os.pathsep), +or, if specified, by the value of sep. +Top-relative path strings (starting with #) are recognized. +The type of the existing value +of name is preserved. + + + +Paths will only appear once. +Duplicate paths in newpath are removed, +preserving the first occurrence to maintain path order. +If delete_existing is true (the default), +existing duplicates are removed before prepending, +otherwise, new duplicates are skipped. +Paths are normalized during comparisons to avoid problems of +"same path, spelled differently", +but retain their original form in the result.