diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 4a6828f0..a722adc1 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,6 +4,9 @@ Release notes vNext ----- +- Add preserve_spaces argument in commoncode.paths.portable_filename. + This argument will prevent the replacement of spaces in filenames. + Version 21.6.11 --------------- @@ -32,7 +35,7 @@ Version 21.5.12 - Add new function to find a command or shared object file in the PATH (e.g. in environment variables). See commoncode.command.find_in_path() -- Add new simplified the commoncode.command.execute() function. +- Add new simplified the commoncode.command.execute() function. - Add support for Python 3.10 - Update tests to cope with Python 3.6 bug https://bugs.python.org/issue26919 - Adopt latest skeleton with configure scripts updates diff --git a/src/commoncode/paths.py b/src/commoncode/paths.py index af8784b9..402ef14f 100644 --- a/src/commoncode/paths.py +++ b/src/commoncode/paths.py @@ -26,7 +26,7 @@ # Build OS-portable and safer paths -def safe_path(path, posix=False): +def safe_path(path, posix=False, preserve_spaces=False): """ Convert `path` to a safe and portable POSIX path usable on multiple OSes. The returned path is an ASCII-only byte string, resolved for relative @@ -34,6 +34,8 @@ def safe_path(path, posix=False): The `path` is treated as a POSIX path if `posix` is True or as a Windows path with blackslash separators otherwise. + + If `preserve_spaces` is True, then the spaces in `path` will not be replaced. """ # if the path is UTF, try to use unicode instead if not isinstance(path, str): @@ -50,7 +52,7 @@ def safe_path(path, posix=False): _pathmod, path_sep = path_handlers(path, posix) segments = [s.strip() for s in path.split(path_sep) if s.strip()] - segments = [portable_filename(s) for s in segments] + segments = [portable_filename(s, preserve_spaces=preserve_spaces) for s in segments] if not segments: return '_' @@ -133,12 +135,16 @@ def resolve(path, posix=True): legal_punctuation = r"!\#$%&\(\)\+,\-\.;\=@\[\]_\{\}\~" +legal_spaces = r" " legal_chars = r'A-Za-z0-9' + legal_punctuation +legal_chars_inc_spaces = legal_chars + legal_spaces illegal_chars_re = r'[^' + legal_chars + r']' +illegal_chars_exc_spaces_re = r'[^' + legal_chars_inc_spaces + r']' replace_illegal_chars = re.compile(illegal_chars_re).sub +replace_illegal_chars_exc_spaces = re.compile(illegal_chars_exc_spaces_re).sub -def portable_filename(filename): +def portable_filename(filename, preserve_spaces=False): """ Return a new name for `filename` that is portable across operating systems. @@ -156,13 +162,18 @@ def portable_filename(filename): Also inspired by Werkzeug: https://raw.githubusercontent.com/pallets/werkzeug/8c2d63ce247ba1345e1b9332a68ceff93b2c07ab/werkzeug/utils.py + + If `preserve_spaces` is True, then spaces in `filename` will not be replaced. """ filename = toascii(filename, translit=True) if not filename: return '_' - filename = replace_illegal_chars('_', filename) + if preserve_spaces: + filename = replace_illegal_chars_exc_spaces('_', filename) + else: + filename = replace_illegal_chars('_', filename) # these are illegal both upper and lowercase and with or without an extension # we insert an underscore after the base name. diff --git a/tests/test_paths.py b/tests/test_paths.py index 90d2824b..10c42e02 100644 --- a/tests/test_paths.py +++ b/tests/test_paths.py @@ -127,6 +127,10 @@ def test_portable_filename(self): expected = 'A___file__with_Spaces.mov' assert paths.portable_filename("A:\\ file/ with Spaces.mov") == expected + # Test `preserve_spaces` option. Spaces should not be replaced + expected = 'Program Files (x86)' + assert paths.portable_filename("Program Files (x86)", preserve_spaces=True) == expected + # Unresolved relative paths will be treated as a single filename. Use # resolve instead if you want to resolve paths: expected = '___.._.._etc_passwd'