From f80095b86330115c0daadf22452f08935f02b76c Mon Sep 17 00:00:00 2001 From: Mats Wichmann Date: Sat, 10 Sep 2022 16:43:09 -0600 Subject: Handle a list as source in Copy Copy (in the copy_func()) unconditionally converts the source argument to a string. This breaks the case where it was passed as a list - it should remain a list with the members converted to string, rather than being a single string. We assume a list source is supported, since the code just a few lines lower down detects a list and iterates through it. A test is added in test/Copy-Action.py to try to detect this case - it fails before the change and passes after. With the change to make a list argument work, found that the message printed by the matching strfunction (originally a lambda) needed the same adjustment, if the caller did not supply a list that was already strings (e.g. a list of Nodes, as from calling Glob()). Fixed up. At the same time, did some fiddling with the other Action Functions. Chmod had not been adjusted to emit the new Python standard for octal values (0o755 instead of 0755) - the change is only for the message, not for the function, but required aligning the test with the new format. Added some docstrings. Fixes #3009 Signed-off-by: Mats Wichmann --- CHANGES.txt | 7 +++ RELEASE.txt | 13 ++++++ SCons/Defaults.py | 120 +++++++++++++++++++++++++++++++++---------------- doc/man/scons.xml | 22 ++++++--- doc/user/factories.xml | 4 +- test/Chmod.py | 63 +++++++++++++++----------- test/Copy-Action.py | 6 +++ 7 files changed, 161 insertions(+), 74 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index 8e32f3d..e895b29 100755 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -27,6 +27,13 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER From Ryan Saunders: - Fixed runtest.py failure on Windows caused by excessive escaping of the path to python.exe. + From Mats Wichmann: + - A list argument as the source to the Copy() action function is now + correctly handled by converting elements to string. Copy bails if + asked to copy a list to an existing non-directory destination. + Both the implementation and the strfunction which prints the progress + message were adjusted. Fixes #3009. + RELEASE 4.4.0 - Sat, 30 Jul 2022 14:08:29 -0700 From Joseph Brill: diff --git a/RELEASE.txt b/RELEASE.txt index 93d47f1..da4c7c8 100755 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -31,6 +31,8 @@ CHANGED/ENHANCED EXISTING FUNCTIONALITY - Added -fsanitize support to ParseFlags(). This will propagate to CCFLAGS and LINKFLAGS. - Calling EnsureSConsVersion() and EnsurePythonVersion() won't initialize DefaultEnvironment anymore. +- The console message from the Chmod() action function now displays + octal modes using the modern Python syntax (0o755 rather than 0755). FIXES ----- @@ -38,6 +40,9 @@ FIXES - List fixes of outright bugs - Added missing newline to generated compilation database (compile_commands.json) +- A list argument as the source to the Copy() action function is now handled. + Both the implementation and the strfunction which prints the progress + message were adjusted. IMPROVEMENTS ------------ @@ -51,6 +56,12 @@ PACKAGING - List changes in the way SCons is packaged and/or released +- SCons now has three requirements files: requirements.txt describes + requirements to run scons; requirements-dev.txt requirements to + develop it - mainly things needed to run the testsuite; + requirements_pkg.txt are the requirements to do a full build + (including docs build) with an intent to create the packages. + DOCUMENTATION ------------- @@ -58,6 +69,8 @@ DOCUMENTATION typo fixes, even if they're mentioned in src/CHANGES.txt to give the contributor credit) +- Updated the --hash-format manpage entry. + DEVELOPMENT ----------- diff --git a/SCons/Defaults.py b/SCons/Defaults.py index 65bd75a..218eb10 100644 --- a/SCons/Defaults.py +++ b/SCons/Defaults.py @@ -41,10 +41,12 @@ import SCons.Action import SCons.Builder import SCons.CacheDir import SCons.Environment +import SCons.Errors import SCons.PathList import SCons.Scanner.Dir import SCons.Subst import SCons.Tool +import SCons.Util # A placeholder for a default Environment (for fetching source files # from source code management systems and the like). This must be @@ -79,7 +81,6 @@ def DefaultEnvironment(*args, **kw): """ global _default_env if not _default_env: - import SCons.Util _default_env = SCons.Environment.Environment(*args, **kw) _default_env.Decider('content') global DefaultEnvironment @@ -157,15 +158,19 @@ LdModuleLinkAction = SCons.Action.Action("$LDMODULECOM", "$LDMODULECOMSTR") ActionFactory = SCons.Action.ActionFactory -def get_paths_str(dest): - # If dest is a list, we need to manually call str() on each element +def get_paths_str(dest) -> str: + """Generates a string from *dest* for use in a strfunction. + + If *dest* is a list, manually converts each elem to a string. + """ + def quote(arg): + return f'"{arg}"' + if SCons.Util.is_List(dest): - elem_strs = [] - for element in dest: - elem_strs.append('"' + str(element) + '"') - return '[' + ', '.join(elem_strs) + ']' + elem_strs = [quote(d) for d in dest] + return f'[{", ".join(elem_strs)}]' else: - return '"' + str(dest) + '"' + return quote(dest) permission_dic = { @@ -187,8 +192,14 @@ permission_dic = { } -def chmod_func(dest, mode): - import SCons.Util +def chmod_func(dest, mode) -> None: + """Implementation of the Chmod action function. + + *mode* can be either an integer (normally expressed in octal mode, + as in 0o755) or a string following the syntax of the POSIX chmod + command (for example "ugo+w"). The latter must be converted, since + the underlying Python only takes the numeric form. + """ from string import digits SCons.Node.FS.invalidate_node_memos(dest) if not SCons.Util.is_List(dest): @@ -231,41 +242,63 @@ def chmod_func(dest, mode): os.chmod(str(element), curr_perm & ~new_perm) -def chmod_strfunc(dest, mode): - import SCons.Util +def chmod_strfunc(dest, mode) -> str: + """strfunction for the Chmod action function.""" if not SCons.Util.is_String(mode): - return 'Chmod(%s, 0%o)' % (get_paths_str(dest), mode) + return f'Chmod({get_paths_str(dest)}, {mode:#o})' else: - return 'Chmod(%s, "%s")' % (get_paths_str(dest), str(mode)) + return f'Chmod({get_paths_str(dest)}, "{mode}")' + Chmod = ActionFactory(chmod_func, chmod_strfunc) -def copy_func(dest, src, symlinks=True): - """ - If symlinks (is true), then a symbolic link will be + +def copy_func(dest, src, symlinks=True) -> int: + """Implementation of the Copy action function. + + Copies *src* to *dest*. If *src* is a list, *dest* must be + a directory, or not exist (will be created). + + Since Python :mod:`shutil` methods, which know nothing about + SCons Nodes, will be called to perform the actual copying, + args are converted to strings first. + + If *symlinks* evaluates true, then a symbolic link will be shallow copied and recreated as a symbolic link; otherwise, copying a symbolic link will be equivalent to copying the symbolic link's final target regardless of symbolic link depth. """ dest = str(dest) - src = str(src) + src = [str(n) for n in src] if SCons.Util.is_List(src) else str(src) SCons.Node.FS.invalidate_node_memos(dest) - if SCons.Util.is_List(src) and os.path.isdir(dest): + if SCons.Util.is_List(src): + if not os.path.exists(dest): + os.makedirs(dest, exist_ok=True) + elif not os.path.isdir(dest): + # is Python's NotADirectoryError more appropriate? + raise SCons.Errors.UserError( + f'Copy() called with list src but dest "{dest}" is not a directory' + ) + for file in src: shutil.copy2(file, dest) return 0 + elif os.path.islink(src): if symlinks: - return os.symlink(os.readlink(src), dest) - else: - return copy_func(dest, os.path.realpath(src)) + os.symlink(os.readlink(src), dest) + return 0 + + return copy_func(dest, os.path.realpath(src)) + elif os.path.isfile(src): shutil.copy2(src, dest) return 0 + else: shutil.copytree(src, dest, symlinks) # copytree returns None in python2 and destination string in python3 @@ -273,13 +306,20 @@ def copy_func(dest, src, symlinks=True): return 0 -Copy = ActionFactory( - copy_func, - lambda dest, src, symlinks=True: 'Copy("%s", "%s")' % (dest, src) -) +def copy_strfunc(dest, src, symlinks=True) -> str: + """strfunction for the Copy action function.""" + return f'Copy({get_paths_str(dest)}, {get_paths_str(src)})' + + +Copy = ActionFactory(copy_func, copy_strfunc) -def delete_func(dest, must_exist=0): +def delete_func(dest, must_exist=False) -> None: + """Implementation of the Delete action function. + + Lets the Python :func:`os.unlink` raise an error if *dest* does not exist, + unless *must_exist* evaluates false (the default). + """ SCons.Node.FS.invalidate_node_memos(dest) if not SCons.Util.is_List(dest): dest = [dest] @@ -296,14 +336,16 @@ def delete_func(dest, must_exist=0): os.unlink(entry) -def delete_strfunc(dest, must_exist=0): - return 'Delete(%s)' % get_paths_str(dest) +def delete_strfunc(dest, must_exist=False) -> str: + """strfunction for the Delete action function.""" + return f'Delete({get_paths_str(dest)})' Delete = ActionFactory(delete_func, delete_strfunc) -def mkdir_func(dest): +def mkdir_func(dest) -> None: + """Implementation of the Mkdir action function.""" SCons.Node.FS.invalidate_node_memos(dest) if not SCons.Util.is_List(dest): dest = [dest] @@ -311,22 +353,23 @@ def mkdir_func(dest): os.makedirs(str(entry), exist_ok=True) -Mkdir = ActionFactory(mkdir_func, - lambda _dir: 'Mkdir(%s)' % get_paths_str(_dir)) +Mkdir = ActionFactory(mkdir_func, lambda _dir: f'Mkdir({get_paths_str(_dir)})') -def move_func(dest, src): +def move_func(dest, src) -> None: + """Implementation of the Move action function.""" SCons.Node.FS.invalidate_node_memos(dest) SCons.Node.FS.invalidate_node_memos(src) shutil.move(src, dest) -Move = ActionFactory(move_func, - lambda dest, src: 'Move("%s", "%s")' % (dest, src), - convert=str) +Move = ActionFactory( + move_func, lambda dest, src: f'Move("{dest}", "{src}")', convert=str +) -def touch_func(dest): +def touch_func(dest) -> None: + """Implementation of the Touch action function.""" SCons.Node.FS.invalidate_node_memos(dest) if not SCons.Util.is_List(dest): dest = [dest] @@ -341,8 +384,7 @@ def touch_func(dest): os.utime(file, (atime, mtime)) -Touch = ActionFactory(touch_func, - lambda file: 'Touch(%s)' % get_paths_str(file)) +Touch = ActionFactory(touch_func, lambda file: f'Touch({get_paths_str(file)})') # Internal utility functions diff --git a/doc/man/scons.xml b/doc/man/scons.xml index 2155e9f..19dbb76 100644 --- a/doc/man/scons.xml +++ b/doc/man/scons.xml @@ -6438,21 +6438,26 @@ changes the permissions on the specified dest file or directory to the specified mode -which can be octal or string, similar to the bash command. +which can be octal or string, similar to the POSIX +chmod command. Examples: Execute(Chmod('file', 0o755)) -env.Command('foo.out', 'foo.in', - [Copy('$TARGET', '$SOURCE'), - Chmod('$TARGET', 0o755)]) +env.Command( + 'foo.out', + 'foo.in', + [Copy('$TARGET', '$SOURCE'), Chmod('$TARGET', 0o755)], +) Execute(Chmod('file', "ugo+w")) -env.Command('foo.out', 'foo.in', - [Copy('$TARGET', '$SOURCE'), - Chmod('$TARGET', "ugo+w")]) +env.Command( + 'foo.out', + 'foo.in', + [Copy('$TARGET', '$SOURCE'), Chmod('$TARGET', "ugo+w")], +) @@ -6473,6 +6478,9 @@ that will copy the source file or directory to the dest destination file or directory. +If src is a list, +dest must be a directory +if it already exists. Examples: diff --git a/doc/user/factories.xml b/doc/user/factories.xml index bb68504..362b6f0 100644 --- a/doc/user/factories.xml +++ b/doc/user/factories.xml @@ -190,10 +190,10 @@ touch $* # Symbolic link shallow copied as a new symbolic link: -Command("LinkIn", "LinkOut", Copy("$TARGET", "$SOURCE"[, True])) +Command("LinkIn", "LinkOut", Copy("$TARGET", "$SOURCE", symlinks=True)) # Symbolic link target copied as a file or directory: -Command("LinkIn", "FileOrDirectoryOut", Copy("$TARGET", "$SOURCE", False)) +Command("LinkIn", "FileOrDirectoryOut", Copy("$TARGET", "$SOURCE", symlinks=False)) diff --git a/test/Chmod.py b/test/Chmod.py index 64f4ed9..7af95b4 100644 --- a/test/Chmod.py +++ b/test/Chmod.py @@ -1,6 +1,8 @@ #!/usr/bin/env python # -# __COPYRIGHT__ +# MIT License +# +# Copyright The SCons Foundation # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -20,9 +22,6 @@ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" """ Verify that the Chmod() Action works. @@ -43,26 +42,36 @@ Execute(Chmod('f1', 0o666)) Execute(Chmod(('f1-File'), 0o666)) Execute(Chmod('d2', 0o777)) Execute(Chmod(Dir('d2-Dir'), 0o777)) + def cat(env, source, target): target = str(target[0]) with open(target, "wb") as f: for src in source: with open(str(src), "rb") as infp: f.write(infp.read()) + Cat = Action(cat) env = Environment() -env.Command('bar.out', 'bar.in', [Cat, - Chmod("f3", 0o666), - Chmod("d4", 0o777)]) -env = Environment(FILE = 'f5') +env.Command( + 'bar.out', + 'bar.in', + [Cat, Chmod("f3", 0o666), Chmod("d4", 0o777)], +) +env = Environment(FILE='f5') env.Command('f6.out', 'f6.in', [Chmod('$FILE', 0o666), Cat]) -env.Command('f7.out', 'f7.in', [Cat, - Chmod('Chmod-$SOURCE', 0o666), - Chmod('${TARGET}-Chmod', 0o666)]) +env.Command( + 'f7.out', + 'f7.in', + [Cat, Chmod('Chmod-$SOURCE', 0o666), Chmod('${TARGET}-Chmod', 0o666)], +) # Make sure Chmod works with a list of arguments -env = Environment(FILE = 'f9') -env.Command('f8.out', 'f8.in', [Chmod(['$FILE', File('f10')], 0o666), Cat]) +env = Environment(FILE='f9') +env.Command( + 'f8.out', + 'f8.in', + [Chmod(['$FILE', File('f10')], 0o666), Cat], +) Execute(Chmod(['d11', Dir('d12')], 0o777)) Execute(Chmod('f13', "a=r")) Execute(Chmod('f14', "ogu+w")) @@ -117,28 +126,30 @@ os.chmod(test.workpath('f15'), 0o444) os.chmod(test.workpath('d16'), 0o555) os.chmod(test.workpath('d17'), 0o555) os.chmod(test.workpath('d18'), 0o555) -expect = test.wrap_stdout(read_str = """\ -Chmod("f1", 0666) -Chmod("f1-File", 0666) -Chmod("d2", 0777) -Chmod("d2-Dir", 0777) -Chmod(["d11", "d12"], 0777) + +expect = test.wrap_stdout( + read_str = """\ +Chmod("f1", 0o666) +Chmod("f1-File", 0o666) +Chmod("d2", 0o777) +Chmod("d2-Dir", 0o777) +Chmod(["d11", "d12"], 0o777) Chmod("f13", "a=r") Chmod("f14", "ogu+w") Chmod("f15", "ug=rw, go+ rw") Chmod("d16", "0777") Chmod(["d17", "d18"], "ogu = rwx") """, - build_str = """\ + build_str = """\ cat(["bar.out"], ["bar.in"]) -Chmod("f3", 0666) -Chmod("d4", 0777) -Chmod("f5", 0666) +Chmod("f3", 0o666) +Chmod("d4", 0o777) +Chmod("f5", 0o666) cat(["f6.out"], ["f6.in"]) cat(["f7.out"], ["f7.in"]) -Chmod("Chmod-f7.in", 0666) -Chmod("f7.out-Chmod", 0666) -Chmod(["f9", "f10"], 0666) +Chmod("Chmod-f7.in", 0o666) +Chmod("f7.out-Chmod", 0o666) +Chmod(["f9", "f10"], 0o666) cat(["f8.out"], ["f8.in"]) """) test.run(options = '-n', arguments = '.', stdout = expect) diff --git a/test/Copy-Action.py b/test/Copy-Action.py index e135a9f..e2e01ef 100644 --- a/test/Copy-Action.py +++ b/test/Copy-Action.py @@ -40,6 +40,8 @@ test.write('SConstruct', """\ Execute(Copy('f1.out', 'f1.in')) Execute(Copy(File('d2.out'), 'd2.in')) Execute(Copy('d3.out', File('f3.in'))) +# Issue #3009: make sure it's not mangled if src is a list. +Execute(Copy('d7.out', Glob('f?.in'))) def cat(env, source, target): target = str(target[0]) @@ -71,6 +73,7 @@ test.subdir('d2.in') test.write(['d2.in', 'file'], "d2.in/file\n") test.write('f3.in', "f3.in\n") test.subdir('d3.out') +test.subdir('d7.out') test.write('bar.in', "bar.in\n") test.write('f4.in', "f4.in\n") test.subdir('d5.in') @@ -101,6 +104,7 @@ expect = test.wrap_stdout( Copy("f1.out", "f1.in") Copy("d2.out", "d2.in") Copy("d3.out", "f3.in") +Copy("d7.out", ["f1.in", "f3.in", "f4.in", "f6.in", "f7.in", "f8.in", "f9.in"]) """, build_str="""\ cat(["bar.out"], ["bar.in"]) @@ -123,6 +127,7 @@ test.run(options='-n', arguments='.', stdout=expect) test.must_not_exist('f1.out') test.must_not_exist('d2.out') test.must_not_exist(os.path.join('d3.out', 'f3.in')) +test.must_not_exist(os.path.join('d7.out', 'f7.in')) test.must_not_exist('f4.out') test.must_not_exist('d5.out') test.must_not_exist(os.path.join('d6.out', 'f6.in')) @@ -141,6 +146,7 @@ test.run() test.must_match('f1.out', "f1.in\n", mode='r') test.must_match(['d2.out', 'file'], "d2.in/file\n", mode='r') test.must_match(['d3.out', 'f3.in'], "f3.in\n", mode='r') +test.must_match(['d7.out', 'f7.in'], "f7.in\n", mode='r') test.must_match('f4.out', "f4.in\n", mode='r') test.must_match(['d5.out', 'file'], "d5.in/file\n", mode='r') test.must_match(['d6.out', 'f6.in'], "f6.in\n", mode='r') -- cgit v0.12 From 767cc21f9f2f75ef5cf62fc6ea8d2343530287ed Mon Sep 17 00:00:00 2001 From: Mats Wichmann Date: Sun, 11 Sep 2022 17:14:25 -0600 Subject: Tweak Copy() changes: Simplify check for copy-a-list case: just try the os.makedirs, catch the exception, and raise a BuildError which seems to fit better. Add a second test for copying lists in test/Copy-Action: now tests both a list of explicit strings and a list of Nodes. Signed-off-by: Mats Wichmann --- CHANGES.txt | 4 ++-- SCons/Defaults.py | 11 +++++------ test/Copy-Action.py | 5 +++++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index e895b29..addc5f7 100755 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -29,8 +29,8 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER From Mats Wichmann: - A list argument as the source to the Copy() action function is now - correctly handled by converting elements to string. Copy bails if - asked to copy a list to an existing non-directory destination. + correctly handled by converting elements to string. Copy errors out + if asked to copy a list to an existing non-directory destination. Both the implementation and the strfunction which prints the progress message were adjusted. Fixes #3009. diff --git a/SCons/Defaults.py b/SCons/Defaults.py index 218eb10..32b924f 100644 --- a/SCons/Defaults.py +++ b/SCons/Defaults.py @@ -276,14 +276,13 @@ def copy_func(dest, src, symlinks=True) -> int: SCons.Node.FS.invalidate_node_memos(dest) if SCons.Util.is_List(src): - if not os.path.exists(dest): + # this fails only if dest exists and is not a dir + try: os.makedirs(dest, exist_ok=True) - elif not os.path.isdir(dest): - # is Python's NotADirectoryError more appropriate? - raise SCons.Errors.UserError( - f'Copy() called with list src but dest "{dest}" is not a directory' + except FileExistsError: + raise SCons.Errors.BuildError( + errstr=f'Error: Copy() called with list src but "{dest}" is not a directory' ) - for file in src: shutil.copy2(file, dest) return 0 diff --git a/test/Copy-Action.py b/test/Copy-Action.py index e2e01ef..1b1356e 100644 --- a/test/Copy-Action.py +++ b/test/Copy-Action.py @@ -41,6 +41,8 @@ Execute(Copy('f1.out', 'f1.in')) Execute(Copy(File('d2.out'), 'd2.in')) Execute(Copy('d3.out', File('f3.in'))) # Issue #3009: make sure it's not mangled if src is a list. +# make sure both list-of-str and list-of-Node work +Execute(Copy('d7.out', ['f10.in', 'f11.in'])) Execute(Copy('d7.out', Glob('f?.in'))) def cat(env, source, target): @@ -104,6 +106,7 @@ expect = test.wrap_stdout( Copy("f1.out", "f1.in") Copy("d2.out", "d2.in") Copy("d3.out", "f3.in") +Copy("d7.out", ["f10.in", "f11.in"]) Copy("d7.out", ["f1.in", "f3.in", "f4.in", "f6.in", "f7.in", "f8.in", "f9.in"]) """, build_str="""\ @@ -128,6 +131,7 @@ test.must_not_exist('f1.out') test.must_not_exist('d2.out') test.must_not_exist(os.path.join('d3.out', 'f3.in')) test.must_not_exist(os.path.join('d7.out', 'f7.in')) +test.must_not_exist(os.path.join('d7.out', 'f11.in')) test.must_not_exist('f4.out') test.must_not_exist('d5.out') test.must_not_exist(os.path.join('d6.out', 'f6.in')) @@ -147,6 +151,7 @@ test.must_match('f1.out', "f1.in\n", mode='r') test.must_match(['d2.out', 'file'], "d2.in/file\n", mode='r') test.must_match(['d3.out', 'f3.in'], "f3.in\n", mode='r') test.must_match(['d7.out', 'f7.in'], "f7.in\n", mode='r') +test.must_match(['d7.out', 'f11.in'], "f11.in\n", mode='r') test.must_match('f4.out', "f4.in\n", mode='r') test.must_match(['d5.out', 'file'], "d5.in/file\n", mode='r') test.must_match(['d6.out', 'f6.in'], "f6.in\n", mode='r') -- cgit v0.12 From 7c2d4271d57d2da462859d29435740de3b412d10 Mon Sep 17 00:00:00 2001 From: Mats Wichmann Date: Sun, 11 Sep 2022 17:56:52 -0600 Subject: Copy(): tweak the error message for copy-a-list Signed-off-by: Mats Wichmann --- SCons/Defaults.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/SCons/Defaults.py b/SCons/Defaults.py index 32b924f..40c3e4a 100644 --- a/SCons/Defaults.py +++ b/SCons/Defaults.py @@ -281,7 +281,11 @@ def copy_func(dest, src, symlinks=True) -> int: os.makedirs(dest, exist_ok=True) except FileExistsError: raise SCons.Errors.BuildError( - errstr=f'Error: Copy() called with list src but "{dest}" is not a directory' + errstr=( + 'Error: Copy() called with a list of sources, ' + 'which requires target to be a directory, ' + f'but "{dest}" is not a directory.' + ) ) for file in src: shutil.copy2(file, dest) @@ -300,8 +304,6 @@ def copy_func(dest, src, symlinks=True) -> int: else: shutil.copytree(src, dest, symlinks) - # copytree returns None in python2 and destination string in python3 - # A error is raised in both cases, so we can just return 0 for success return 0 -- cgit v0.12