1# ##### BEGIN GPL LICENSE BLOCK #####
2#
3#  This program is free software; you can redistribute it and/or
4#  modify it under the terms of the GNU General Public License
5#  as published by the Free Software Foundation; either version 2
6#  of the License, or (at your option) any later version.
7#
8#  This program is distributed in the hope that it will be useful,
9#  but WITHOUT ANY WARRANTY; without even the implied warranty of
10#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11#  GNU General Public License for more details.
12#
13#  You should have received a copy of the GNU General Public License
14#  along with this program; if not, write to the Free Software Foundation,
15#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16#
17# ##### END GPL LICENSE BLOCK #####
18
19# <pep8-80 compliant>
20
21"""
22This module has a similar scope to os.path, containing utility
23functions for dealing with paths in Blender.
24"""
25
26__all__ = (
27    "abspath",
28    "basename",
29    "clean_name",
30    "display_name",
31    "display_name_to_filepath",
32    "display_name_from_filepath",
33    "ensure_ext",
34    "extensions_image",
35    "extensions_movie",
36    "extensions_audio",
37    "is_subdir",
38    "module_names",
39    "native_pathsep",
40    "reduce_dirs",
41    "relpath",
42    "resolve_ncase",
43)
44
45import bpy as _bpy
46import os as _os
47
48from _bpy_path import (
49    extensions_audio,
50    extensions_movie,
51    extensions_image,
52)
53
54
55def _getattr_bytes(var, attr):
56    return var.path_resolve(attr, False).as_bytes()
57
58
59def abspath(path, start=None, library=None):
60    """
61    Returns the absolute path relative to the current blend file
62    using the "//" prefix.
63
64    :arg start: Relative to this path,
65       when not set the current filename is used.
66    :type start: string or bytes
67    :arg library: The library this path is from. This is only included for
68       convenience, when the library is not None its path replaces *start*.
69    :type library: :class:`bpy.types.Library`
70    """
71    if isinstance(path, bytes):
72        if path.startswith(b"//"):
73            if library:
74                start = _os.path.dirname(
75                    abspath(_getattr_bytes(library, "filepath")))
76            return _os.path.join(
77                _os.path.dirname(_getattr_bytes(_bpy.data, "filepath"))
78                if start is None else start,
79                path[2:],
80            )
81    else:
82        if path.startswith("//"):
83            if library:
84                start = _os.path.dirname(
85                    abspath(library.filepath))
86            return _os.path.join(
87                _os.path.dirname(_bpy.data.filepath)
88                if start is None else start,
89                path[2:],
90            )
91
92    return path
93
94
95def relpath(path, start=None):
96    """
97    Returns the path relative to the current blend file using the "//" prefix.
98
99    :arg path: An absolute path.
100    :type path: string or bytes
101    :arg start: Relative to this path,
102       when not set the current filename is used.
103    :type start: string or bytes
104    """
105    if isinstance(path, bytes):
106        if not path.startswith(b"//"):
107            if start is None:
108                start = _os.path.dirname(_getattr_bytes(_bpy.data, "filepath"))
109            return b"//" + _os.path.relpath(path, start)
110    else:
111        if not path.startswith("//"):
112            if start is None:
113                start = _os.path.dirname(_bpy.data.filepath)
114            return "//" + _os.path.relpath(path, start)
115
116    return path
117
118
119def is_subdir(path, directory):
120    """
121    Returns true if *path* in a subdirectory of *directory*.
122    Both paths must be absolute.
123
124    :arg path: An absolute path.
125    :type path: string or bytes
126    """
127    from os.path import normpath, normcase, sep
128    path = normpath(normcase(path))
129    directory = normpath(normcase(directory))
130    if len(path) > len(directory):
131        sep = sep.encode('ascii') if isinstance(directory, bytes) else sep
132        if path.startswith(directory.rstrip(sep) + sep):
133            return True
134    return False
135
136
137def clean_name(name, replace="_"):
138    """
139    Returns a name with characters replaced that
140    may cause problems under various circumstances,
141    such as writing to a file.
142    All characters besides A-Z/a-z, 0-9 are replaced with "_"
143    or the *replace* argument if defined.
144    """
145
146    if replace != "_":
147        if len(replace) != 1 or ord(replace) > 255:
148            raise ValueError("Value must be a single ascii character")
149
150    def maketrans_init():
151        trans_cache = clean_name._trans_cache
152        trans = trans_cache.get(replace)
153        if trans is None:
154            bad_chars = (
155                0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
156                0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
157                0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
158                0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
159                0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
160                0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2e, 0x2f, 0x3a,
161                0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x5b, 0x5c,
162                0x5d, 0x5e, 0x60, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f,
163                0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,
164                0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f,
165                0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,
166                0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f,
167                0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,
168                0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf,
169                0xb0, 0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7,
170                0xb8, 0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf,
171                0xc0, 0xc1, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7,
172                0xc8, 0xc9, 0xca, 0xcb, 0xcc, 0xcd, 0xce, 0xcf,
173                0xd0, 0xd1, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7,
174                0xd8, 0xd9, 0xda, 0xdb, 0xdc, 0xdd, 0xde, 0xdf,
175                0xe0, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7,
176                0xe8, 0xe9, 0xea, 0xeb, 0xec, 0xed, 0xee, 0xef,
177                0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7,
178                0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe,
179            )
180            trans = str.maketrans({char: replace for char in bad_chars})
181            trans_cache[replace] = trans
182        return trans
183
184    trans = maketrans_init()
185    return name.translate(trans)
186
187
188clean_name._trans_cache = {}
189
190
191def _clean_utf8(name):
192    if type(name) == bytes:
193        return name.decode("utf8", "replace")
194    else:
195        return name.encode("utf8", "replace").decode("utf8")
196
197
198_display_name_literals = {
199    ":": "_colon_",
200    "+": "_plus_",
201}
202
203
204def display_name(name, *, has_ext=True):
205    """
206    Creates a display string from name to be used menus and the user interface.
207    Capitalize the first letter in all lowercase names,
208    mixed case names are kept as is. Intended for use with
209    filenames and module names.
210    """
211
212    if has_ext:
213        name = _os.path.splitext(basename(name))[0]
214
215    # string replacements
216    for disp_value, file_value in _display_name_literals.items():
217        name = name.replace(file_value, disp_value)
218
219    # strip to allow underscore prefix
220    # (when paths can't start with numbers for eg).
221    name = name.replace("_", " ").lstrip(" ")
222
223    if name.islower():
224        name = name.lower().title()
225
226    name = _clean_utf8(name)
227    return name
228
229
230def display_name_to_filepath(name):
231    """
232    Performs the reverse of display_name using literal versions of characters
233    which aren't supported in a filepath.
234    """
235    for disp_value, file_value in _display_name_literals.items():
236        name = name.replace(disp_value, file_value)
237    return name
238
239
240def display_name_from_filepath(name):
241    """
242    Returns the path stripped of directory and extension,
243    ensured to be utf8 compatible.
244    """
245
246    name = _os.path.splitext(basename(name))[0]
247    name = _clean_utf8(name)
248    return name
249
250
251def resolve_ncase(path):
252    """
253    Resolve a case insensitive path on a case sensitive system,
254    returning a string with the path if found else return the original path.
255    """
256
257    def _ncase_path_found(path):
258        if not path or _os.path.exists(path):
259            return path, True
260
261        # filename may be a directory or a file
262        filename = _os.path.basename(path)
263        dirpath = _os.path.dirname(path)
264
265        suffix = path[:0]  # "" but ensure byte/str match
266        if not filename:  # dir ends with a slash?
267            if len(dirpath) < len(path):
268                suffix = path[:len(path) - len(dirpath)]
269
270            filename = _os.path.basename(dirpath)
271            dirpath = _os.path.dirname(dirpath)
272
273        if not _os.path.exists(dirpath):
274            if dirpath == path:
275                return path, False
276
277            dirpath, found = _ncase_path_found(dirpath)
278
279            if not found:
280                return path, False
281
282        # at this point, the directory exists but not the file
283
284        # we are expecting 'dirpath' to be a directory, but it could be a file
285        if _os.path.isdir(dirpath):
286            try:
287                files = _os.listdir(dirpath)
288            except PermissionError:
289                # We might not have the permission to list dirpath...
290                return path, False
291        else:
292            return path, False
293
294        filename_low = filename.lower()
295        f_iter_nocase = None
296
297        for f_iter in files:
298            if f_iter.lower() == filename_low:
299                f_iter_nocase = f_iter
300                break
301
302        if f_iter_nocase:
303            return _os.path.join(dirpath, f_iter_nocase) + suffix, True
304        else:
305            # can't find the right one, just return the path as is.
306            return path, False
307
308    ncase_path, found = _ncase_path_found(path)
309    return ncase_path if found else path
310
311
312def ensure_ext(filepath, ext, case_sensitive=False):
313    """
314    Return the path with the extension added if it is not already set.
315
316    :arg ext: The extension to check for, can be a compound extension. Should
317              start with a dot, such as '.blend' or '.tar.gz'.
318    :type ext: string
319    :arg case_sensitive: Check for matching case when comparing extensions.
320    :type case_sensitive: bool
321    """
322
323    if case_sensitive:
324        if filepath.endswith(ext):
325            return filepath
326    else:
327        if filepath[-len(ext):].lower().endswith(ext.lower()):
328            return filepath
329
330    return filepath + ext
331
332
333def module_names(path, recursive=False):
334    """
335    Return a list of modules which can be imported from *path*.
336
337    :arg path: a directory to scan.
338    :type path: string
339    :arg recursive: Also return submodule names for packages.
340    :type recursive: bool
341    :return: a list of string pairs (module_name, module_file).
342    :rtype: list
343    """
344
345    from os.path import join, isfile
346
347    modules = []
348
349    for filename in sorted(_os.listdir(path)):
350        if filename == "modules":
351            pass  # XXX, hard coded exception.
352        elif filename.endswith(".py") and filename != "__init__.py":
353            fullpath = join(path, filename)
354            modules.append((filename[0:-3], fullpath))
355        elif not filename.startswith("."):
356            # Skip hidden files since they are used by for version control.
357            directory = join(path, filename)
358            fullpath = join(directory, "__init__.py")
359            if isfile(fullpath):
360                modules.append((filename, fullpath))
361                if recursive:
362                    for mod_name, mod_path in module_names(directory, True):
363                        modules.append(("%s.%s" % (filename, mod_name),
364                                        mod_path,
365                                        ))
366
367    return modules
368
369
370def basename(path):
371    """
372    Equivalent to os.path.basename, but skips a "//" prefix.
373
374    Use for Windows compatibility.
375    """
376    return _os.path.basename(path[2:] if path[:2] in {"//", b"//"} else path)
377
378
379def native_pathsep(path):
380    """
381    Replace the path separator with the systems native ``os.sep``.
382    """
383    if type(path) is str:
384        if _os.sep == "/":
385            return path.replace("\\", "/")
386        else:
387            if path.startswith("//"):
388                return "//" + path[2:].replace("/", "\\")
389            else:
390                return path.replace("/", "\\")
391    else:  # bytes
392        if _os.sep == "/":
393            return path.replace(b"\\", b"/")
394        else:
395            if path.startswith(b"//"):
396                return b"//" + path[2:].replace(b"/", b"\\")
397            else:
398                return path.replace(b"/", b"\\")
399
400
401def reduce_dirs(dirs):
402    """
403    Given a sequence of directories, remove duplicates and
404    any directories nested in one of the other paths.
405    (Useful for recursive path searching).
406
407    :arg dirs: Sequence of directory paths.
408    :type dirs: sequence
409    :return: A unique list of paths.
410    :rtype: list
411    """
412    dirs = list({_os.path.normpath(_os.path.abspath(d)) for d in dirs})
413    dirs.sort(key=lambda d: len(d))
414    for i in range(len(dirs) - 1, -1, -1):
415        for j in range(i):
416            print(i, j)
417            if len(dirs[i]) == len(dirs[j]):
418                break
419            elif is_subdir(dirs[i], dirs[j]):
420                del dirs[i]
421                break
422    return dirs
423