Source code for craftr.path

# Copyright (C) 2016  Niklas Rosenstein
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE 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.

from craftr import module, session
from os.path import *
from os import sep, pathsep, curdir, pardir, getcwd

import collections
import ctypes
import errno
import glob2
import os
import sys


[docs]def isglob(path): ''' Returns True if *path* is a glob-able pattern, False if not. ''' return any(x in path for x in '*?')
[docs]def autoglob(path, parent=None): ''' Returns `glob(path)` if *path* is actually a glob-style pattern. If it is not, it will return `[path]` as is, not checking wether it exists or not. ''' if isglob(path): return glob(path, parent=parent) else: return [path]
[docs]def glob(*patterns, exclude=None, parent=None): ''' Wrapper for `glob2.glob()` that accepts an arbitrary number of patterns and matches them. The paths are normalized with `normpath()`. If called from within a module, relative patterns are assumed relative to the modules parent directory. If *exclude* is specified, it must be a string or a list of strings that is/contains glob patterns or filenames to be removed from the result before returning.''' if not parent and module: parent = module.project_dir result = [] for pattern in patterns: if not isabs(pattern): pattern = join(parent, pattern) result += glob2.glob(normpath(pattern)) if isinstance(exclude, str): exclude = [exclude] if exclude is not None: for pattern in exclude: if not isabs(pattern): pattern = join(parent, pattern) if not isglob(pattern): print('>>>', normpath(pattern), result) result.remove(normpath(pattern)) else: for item in glob2.glob(normpath(pattern)): result.remove(item) return result
[docs]def listdir(path, abs=True): ''' This version of `os.listdir` yields absolute paths. ''' if abs: return (os.path.join(path, x) for x in os.listdir(path)) else: return iter(os.listdir(path))
[docs]def commonpath(paths): ''' Returns the longest sub-path of each pathname in the sequence *paths*. Raises `ValueError` if *paths* is empty or contains both relative and absolute pathnames. If there is only one item in *paths*, the parent directory is returned.''' if not paths: raise ValueError('paths is empty') parts = [] has_abs = None for path in paths: if not os.path.isabs(path): if has_abs is None: has_abs = False elif has_abs: raise ValueError('paths contains relative and absolute pathnames') else: if has_abs is None: has_abs = True elif not has_abs: raise ValueError('paths contains relative and absolute pathnames') path = normpath(path) parts.append(path.split(os.sep)) if len(parts) == 1: return dirname(os.sep.join(parts[0])) common = parts[0] for elements in parts[1:]: if len(elements) < len(common): common = common[:len(elements)] for index, elem in enumerate(elements): if index >= len(common): break if elem != common[index]: common = common[:index] break if not common: break common = os.sep.join(common) if not has_abs: common = os.path.relpath(common) return common
[docs]def normpath(path, parent_dir=None, abs=True): ''' Normalizes a filesystem path. Also expands user variables. If a *parent_dir* is specified, a relative path is considered relative to that directory and converted to an absolute path. The default parent directory is the current working directory. *path* may be an iterable other than a string in which case the function is applied recursively to all its items and a list is returned instead of a string. If *abs* is True, the path is returned as an absolute path always, otherwise the path is returned in its original structure.''' if isinstance(path, str): path = os.path.expanduser(path) if abs and not os.path.isabs(path): if parent_dir is None: parent_dir = os.getcwd() path = os.path.join(parent_dir, path) if os.name == 'nt': path = path.lower() return os.path.normpath(path) elif isinstance(path, collections.Iterable): result = [] for item in path: result.append(normpath(item, parent_dir)) return result else: raise TypeError('normpath() expected string or iterable')
[docs]def addprefix(subject, prefix): ''' Given a filename, this function will prepend the specified prefix to the base of the filename and return it. *filename* may be an iterable other than a string in which case the function is applied recursively and a list is being returned instead of a string. __Important__: This is *not* the directy equivalent of `addsuffix()` as it considered *subject* to be a filename and appends the *prefix* only to the files base name. ''' if not prefix: return subject if isinstance(subject, str): dir_, base = split(subject) return join(dir_, prefix + base) elif isinstance(subject, collections.Iterable): result = [] for item in subject: result.append(addprefix(item, prefix)) return result else: raise TypeError('addprefix() expected string or iterable')
[docs]def addsuffix(subject, suffix, replace=False): ''' Given a string, this function appends *suffix* to the end of the string and returns the new string. *subject* may be an iterable other than a string in which case the function will be applied recursively on all its items and a list is being returned instead of a string. If the *replace* argument is True, the suffix will be replaced instead of being just appended. Make sure to include a period in the *suffix* parameter value. ''' if not suffix and not replace: return subject if isinstance(subject, str): if replace: subject = rmvsuffix(subject) if suffix: subject += suffix return subject elif isinstance(subject, collections.Iterable): result = [] for item in subject: result.append(addsuffix(item, suffix, replace)) return result else: raise TypeError('addsuffix() expected string or iterable')
[docs]def setsuffix(subject, suffix): ''' Remove the existing suffix from *subject* and add *suffix* instead. The *suffix* must contain the dot at the beginning. ''' return addsuffix(subject, suffix, replace=True)
[docs]def rmvsuffix(subject): ''' Given a filename, this function removes the the suffix of the filename and returns it. If the filename had no suffix to begin with, it is returned unchanged. *subject* may be an iterable other than a string in which case the function is applied recursively to its items and a list is returned instead of a string. ''' if isinstance(subject, str): index = subject.rfind('.') if index > subject.replace('\\', '/').rfind('/'): subject = subject[:index] return subject elif isinstance(subject, collections.Iterable): result = [] for item in subject: result.append(rmvsuffix(item)) return result else: raise TypeError('rmvsuffix() expected string or iterable')
[docs]def move(filename, basedir, newbase): ''' Given a filename and two directory names, this function generates a new filename which is constructed from the relative path of the filename and the first directory and the joint of this relative path with the second directory. This is useful to generate a new filename in a different directory based on another. Craftr uses this function to generate object filenames. Example: >>> move('src/main.c', 'src', 'build/obj') build/obj/main.c *path* may be an iterable other than a string in which case the function is applied recursively to all its items and a list is returned instead of a string. ''' if isinstance(filename, str): rel = relpath(filename, basedir) if rel == os.curdir or rel.startswith(os.pardir): raise ValueError('pathname not a subpath of basedir', filename, basedir) return join(newbase, relpath(filename, basedir)) elif isinstance(filename, collections.Iterable): result = [] for item in filename: result.append(move(item, basedir, newbase)) return result else: raise TypeError('move() expected string or iterable')
[docs]def local(path): ''' Can only be called from a module context. Returns *path* normalized, assumed relative to the current modules project directory. ''' return normpath(path, module.project_dir)
[docs]def iter_tree(dirname, depth=1): ''' Iterates over all files in *dirname* and its sub-directories up to the specified *depth*. If *dirname* is a list, this scheme will be applied for all items in the list. ''' if not isinstance(dirname, (tuple, list)): dirname = [dirname] def recursion(dirname, depth): try: items = os.listdir(dirname) except OSError: return for path in items: path = os.path.join(dirname, path) yield path if depth > 0 and os.path.isdir(path): yield from recursion(path, depth - 1) for path in dirname: yield from recursion(path, depth)
[docs]def relpath(path, start='.', only_sub=False): ''' Like the original `os.path.relpath()` function, but with the *only_sub* parameter. If *only_sub* is True and *path* is not a subpath of *start*, the *path* is returned unchanged. ''' if not only_sub: return os.path.relpath(path, start) else: try: res = os.path.relpath(path, start) except ValueError: return path # On windows if drive letters don't match if res.startswith(pardir): return path return res
[docs]def split_path(path): ''' Splits *path* into a list of its parts. ''' result = [] path = normpath(path) path, base = split(path) while base: result.append(base) path, base = split(path) result.reverse() return result
[docs]def makedirs(path): ''' Simple `os.makedirs()` clone that does not error if *path* is already an existing directory. ''' if not os.path.isdir(path): os.makedirs(path)
[docs]def get_long_path_name(path): ''' On Windows, this function returns the correct capitalization for *path*. On all other systems, this returns *path* unchanged. ''' # xxx: what about cygwin? if sys.platform.startswith('win32'): # Thanks to http://stackoverflow.com/a/3694799/791713 assert sys.platform.startswith('win32') buf = ctypes.create_unicode_buffer(len(path) + 1) GetLongPathNameW = ctypes.windll.kernel32.GetLongPathNameW res = GetLongPathNameW(path, buf, len(path) + 1) if res == 0 or res > 260: return path else: return buf.value else: return path
[docs]def silent_remove(filename): ''' Remove the file *filename* if it exists and be silent if it does not. Returns True if the file was removed, False if it did not exist. Raises an error in all other cases. ''' try: os.remove(filename) return True except OSError as exc: if exc.errno != errno.ENOENT: raise return False