Source code for RRtoolbox.lib.directory

# -*- coding: utf-8 -*-
"""
This module holds all path manipulation methods and a string concept called directory (referenced paths and strings)
designed to support :mod:`config` and be used with :mod:`session`.

keywords:
----------
*path*: it can be to a folder or file or url if specified
*filename*: the file name without its path
*filepath*: the path to a file
*dirname*: the path to a folder
*url*: Universal Resource Locator
"""
from __future__ import print_function
from __future__ import absolute_import
from future import standard_library
standard_library.install_aliases()
from builtins import range


import os
import sys
from functools import wraps
import shutil
from glob import glob
#from urlparse import urlparse
#import urllib
#import urllib2
#import urllib3
#import requests

try:
    # urllib.urlopen disappears in python 3
    from urllib.request import urlopen, URLError
except ImportError:
    from urllib.request import urlopen
    from urllib.error import URLError


[docs]def resource_path(relative_path=""): """ Get absolute path to resource, works for dev and for PyInstaller """ # based on http://stackoverflow.com/a/37920111/5288758 try: # PyInstaller creates a temp folder and stores path in _MEIPASS base_path = sys._MEIPASS except Exception: # base_path = os.path.abspath(".") # working dir base_path = correctPath(__file__, -2) return os.path.join(base_path, relative_path)
[docs]def getFileHandle(path): """ Gets a file handle from url or disk file. :param path: filepath or url :return: file object """ # urllib.urlopen does the same but is deprecated in python 3 # this function intents to overcome this limitation try: f = urlopen(path) except (ValueError, URLError): # invalid URL f = open(path, 'rb') return f
[docs]def getFileSize(path): """ Gets a size from url or disk file. :param path: filepath or url :return: size in bytes """ # urllib.urlopen does the same but is deprecated in python 3 # this function intents to overcome this limitation try: return urlopen(path).info()["Content-Length"] except ValueError: # invalid URL return os.stat(path).st_size
[docs]def checkPath(path): """ checks if path exists. :param path: path to folder or file. :return: True if exits, False if not """ return os.path.exists(path)
[docs]def checkDir(dirname): """ checks if dirname exists. :param dirname: path to folder :return: True if exits, False if not """ return os.path.isdir(dirname)
[docs]def checkURL(url): """ checks if url exists. :param url: path to url :return: True if exits, False if not """ try: urlopen(url) return True except ValueError: # invalid URL return False
[docs]def checkFile(path): """ checks if filepath or filename exists. :param path: filepath or filename :return: True if exits, False if not """ return os.path.isfile(path)
[docs]def getPath(path=__file__): """ Get standard path from path. It supports ~ as home directory. :param path: it can be to a folder or file. Default is __file__ or module's path. If file exists it selects its folder. :return: dirname (path to a folder) .. note:: It is the same as os.path.dirname(os.path.abspath(path)). """ if path.startswith("~"): path = os.path.expanduser("~") + path[1:] if "." in path: # check extension return os.path.dirname(os.path.abspath(path)) # just use os else: return os.path.abspath(path)
[docs]def getData(path=__file__): # FIXME not working windows syntax under linux """ Get standard path from path. :param path: it can be to a folder or file. Default is __file__ or module's path. :return: [drive,dirname,filename,ext]. 1. drive or UNC (Universal Naming Convention) 2. dirname is path to folder. 3. filename is name of file. 4. ext is extension of file. """ if os.path.isdir(path): # is path to folder drive, dirname = os.path.splitdrive(path) return [drive, dirname, "", ""] else: # anything else is treated as file dirname, ext = os.path.splitext(path) # base,filename = os.path.dirname(dirname), os.path.basename(dirname) base, filename = os.path.split(dirname) drive, dirname = os.path.splitdrive( dirname[:len(dirname) - len(filename)]) return [drive, dirname, filename, ext]
[docs]def increment_if_exits(path, add="_{num}", force=None): """ Generates new name if it exits. :param path: absolute path or filename :param add: if fn exists add pattern :param force: (None) force existent files even if they don't. if True treats fn as existent or if it is a list it treats names from the list as existent names. :return: un-existent fn """ # normalize path = os.path.abspath(path) # list of exceptions listing = [] if force is True: listing.append(path) # append itself elif force is not None: for f in force: # normalize all paths listing.append(os.path.abspath(f)) # check existence if path in listing or os.path.exists(path): # get parts data = getData(path) # make pattern from parts pattern = "".join(data[:-1]) + "{}" + data[-1] # get a priory list of files listing.extend(glob(pattern.format("*"))) num = 0 while True: num += 1 path = pattern.format(add.format(num=num)) if path not in listing and not os.path.exists(path): return path # return increment_if_exits(fn=fn, add=add, force=force) # double check else: return path
[docs]def changedir(filepath, dirname, ext=True): """ Change path to file with dirname. :param filepath: path to file. :param dirname: new path to replace in filepath. :param ext: True to keep extension of file if any. :return: directory object of changed path. """ if ext: data = getData(filepath)[2:] else: data = getData(filepath)[2:3] return Directory(dirname) + Directory(data, False)
[docs]def strdifference(s1, s2): """ Get string differences. :param s1: string 1 :param s2: string 2 :return: (splitted string 1, splitted string 2, index). A splitted string is a list with the string parts. Index is a list containing the indexes of different parts of the two splitted strings. """ def forequal(): if equal: streq = "".join(equal) ls1.append(streq) ls2.append(streq) return True return False def fordiff(): if diff1 or diff2: ls1.append("".join(diff1)) ls2.append("".join(diff2)) state.append(len(ls1) - 1) return True return False ls1, ls2, state, equal, diff1, diff2 = [], [], [], [], [], [] i = 0 for i in range(min(len(s1), len(s2))): if s1[i] == s2[i]: equal.append(s1[i]) if fordiff(): diff1, diff2 = [], [] else: diff1.append(s1[i]) diff2.append(s2[i]) if forequal(): equal = [] fordiff() # append for different forequal() # append for equal diff1, diff2 = s1[i + 1:], s2[i + 1:] fordiff() # append remaining return ls1, ls2, state
[docs]def getSplitted(path=__file__): """ Splits a file path by its separators. :param path: it can be to a folder or file. Default is __file__ or module's path. :return: splitted path. """ return path.split(getSep(path) or None)
[docs]def correctSep(path=__file__, separator=os.path.sep): """ Replaces the path separators by custom or OS standard separator. :param path: relative or absolute path (str). Default is __file__ or module's path. :param separator: desired separators, By default uses system separator (os.path.sep). :return: path with corrected separator. """ return separator.join(getSplitted(path))
[docs]def mkPath(path): # FIXME: not sure if this is correct """ Make path (i.e. creating folder) for filepath. :param path: path to nonexistent folder or file. :return: created path. """ path = os.path.abspath(str(path)) try: # if not os.path.exists(path): os.makedirs(path) # not # os.path.isdir(path) os.makedirs(path) except OSError as exception: import errno if exception.errno == errno.EEXIST: if os.path.isfile(path): raise Exception( "Folder not created. There is a file with the same name at {}".format(path)) else: # exception.errno == errno.EACCES: raise exception # Exception("Permission Denied") return path # returns created path
[docs]def rmFile(filepath): """ Remove file. :param filepath: path to file. :return: None """ try: # enclose in try/except, who knows files could be deleted os.remove(filepath) except OSError: pass # file could have been deleted
[docs]def rmPath(path, ignore_errors=False, onerror=None): """ Remove path from path. :param path: path to nonexistent folder or file. :return: None .. seealso:: shutil.rmtree """ path = getPath(path) shutil.rmtree(path, ignore_errors, onerror)
[docs]def getSep(path, pattern='/\\'): """ Get path separator or indicator. :param path: relative or absolute path (str). :param pattern: guess characters to compare path (str). :return: sep (str). .. note:: It is equivalent to os.path.sep but obtained from the given path and patterns. """ i = len(path) while i and path[i - 1] not in pattern: i -= 1 head, tail = path[:i], path[i:] # now tail has no slashes # remove trailing slashes from head, unless it's all slashes head2 = head count = 0 while head2 and head2[-1] in pattern: head2 = head2[:-1] count += 1 character = head[len(head) - count:len(head)] # get the slash character return character
[docs]def quickOps(path, comp): """ (IN DEVELOPMENT) make quick matching operations in path. :param path: path to folder :param comp: pattern :return: Requirements:: path = 'LEVEL1/LEVEL2/LEVEL3/LEVEL4/LEVEL5' print quickOps(path,'../ROOT/../LEVEL1/../LEVEL2/LEVEL3/../../LEVEL4') 'LEVEL4' print quickOps(path,'ROOT/../LEVEL1/LEVEL2/../../LEVEL4') 'LEVEL3/LEVEL4' print quickOps(path,'../LEVEL2/../') 'LEVEL1/LEVEL3/LEVEL4/LEVEL5' print quickOps(path,'../LEVEL2/..') 'LEVEL1/LEVEL3/LEVEL4/LEVEL5/' print quickOps(path,'LEVEL2/../../LEVEL4/') 'LEVEL2/LEVEL3/LEVEL4/' print quickOps(path,'ROOT/../LEVEL2/../../LEVEL4') 'ROOT/LEVEL3/LEVEL4' print quickOps(path,'LEVEL-1/../NEW7/LEVEL8') 'LEVEL-1/LEVEL1/LEVEL2/LEVEL3/LEVEL4/LEVEL5/NEW7/LEVEL8' print """ toCorrect = getSplitted(path) # what has to be corrected toCompare = getSplitted(comp) # what is used to correct size = len(toCompare) test = ("..",) where = [] current = 0 for i, cp in enumerate(toCompare): if cp not in test: # is a level tfrom = i + 1 < size and toCompare[i + 1] in test tto = i > 0 and toCompare[i - 1] in test isin = cp in toCorrect[current:] # TODO: instead of using index i from 'toCompare', find a way to do # it directly in 'toCorrect' if tfrom and tto: if isin: where.append((i, cp, 'del')) elif tfrom: if isin: where.append((i, cp, 'from')) else: where.append((i, cp, 'lput')) elif tto: if isin: where.append((i, cp, 'to')) else: where.append((i, cp, 'rput')) if where: for order, (i, cp, op) in enumerate(where): # index, comparison, operation if cp in toCorrect: del toCorrect[toCorrect.index(cp)] i = toCompare.index(cp) del toCompare[i:i + 2]
[docs]def getShortenedPath(path, comp): """ Path is controlled to give absolute path from relative path or integer. :param path: absolute path (str). :param comp: pattern or relative path (str) or integer representing level of folder determined by the separator Ex. "/level 1/level 2/.../level N or -1". :return: path before matched to comp Ex: "C://level 1//comp --> C://level 1" Example:: >>> path = 'LEVEL1/LEVEL2/LEVEL3/LEVEL4/LEVEL5' >>> print getShortenedPath(path,-2) # minus two levels LEVEL1/LEVEL2/LEVEL3 >>> print getShortenedPath(path,2) # until three levels LEVEL1/LEVEL2 >>> print getShortenedPath(path,'LEVEL1/LEVEL2/LEVEL3/') LEVEL1/LEVEL2/LEVEL3/ >>> print getShortenedPath(path,'LEVEL4/REPLACE5/NEWLEVEL') LEVEL1/LEVEL2/LEVEL3/LEVEL4/REPLACE5/NEWLEVEL >>> print getShortenedPath(path,'../../SHOULD_BE_LEVEL4') LEVEL1/LEVEL2/LEVEL3/SHOULD_BE_LEVEL4 """ indicator = getSep(path) if isinstance(comp, str): toCorrect = getSplitted(path) # what has to be corrected toCompare = getSplitted(comp) # what is used to correct # isrel = not where and ".." in toCompare # isRelative isrel = False for i, cp in enumerate(toCompare): if cp == "..": # go to previous folder del toCorrect[-1] # eliminate last isrel = True elif cp == ".": # stay in folder pass # do nothing elif isrel or cp not in toCorrect: # place new folders in position toCorrect.extend(toCompare[i:]) # add folders break else: # look for folder and place new folders there if cp and cp in toCorrect: toCorrect = toCorrect[:toCorrect.index(cp)] + toCompare[i:] else: toCorrect.extend(toCompare[i:]) # add folders break path2 = indicator.join(toCorrect) return path2 # path.split(comp.split(getSep(comp))[0])[0]+comp else: return indicator.join(path.split(indicator)[:comp])
[docs]def decoratePath(relative, sep=os.path.sep): """ Decorated path is controlled to give absolute path from relative path. :param relative: int or path. :param sep: path separator :return: decorator """ def decorator(f): @wraps(f) def wrapper(*args, **kwargs): path = f(*args, **kwargs) # relative is corrected internally return getShortenedPath(correctSep(path, sep), relative) # return sep.join(getShortenedPath(path, # relative).split(getSep(path))) return wrapper return decorator
[docs]def correctPath(path, relative): """ Get path corrected from its relative path or level index. :param path: path or file name. :param relative: pattern or level in directory. :return: corrected path. """ return decoratePath(relative)(getPath)(path)
[docs]def joinPath(absolute, relative): """ Joins an absolute path to a relative path. :param absolute: directory or path. :param relative: directory or path. :return: joined path. .. note:: It is equivalent to os.path.join but works with directories. """ absolute = str(absolute) relative = str(relative) if relative.startswith("\\") or relative.startswith("/"): relative = relative[1:] # str ensures updated version is processed return os.path.join(absolute, relative)
[docs]class Directory(str): """ semi-mutable string representation of a inmutable string with support for path representations. :param data: list, directory instance, dictionary or string. :param ispath: True to add support for paths. :param copy: when data is a directory if copy is True then this instance data is independent of the passed directory otherwise both directories are a reference to the same dictionary data but they are not the same object. :param kwargs: additional data to add in directory. """ def __new__(cls, data, ispath=None, copy=False, **kwargs): """ Creates and initializes directory. """ # TODO overcome limitation of string not being mutable. use basestring? # TODO keep string inheritance, keep string methods, but enable mutabilite # data can be list, str, directory or dictionary data = Directory.filterdata(data, ispath, kwargs) string = Directory.repr2str(data["repr"], data["ispath"]) self = super(Directory, cls).__new__(cls, string) if copy: # do not use __dict__ to not overlook custom setters for k, v in data.items(): # self.__dict__.update(data) setattr(self, k, v) setattr(self, "repr", string) # self.__dict__["repr"] = [string] else: setattr(self, "__dict__", data) # self.__dict__ = data self.__name__ = "directory" return self
[docs] @staticmethod def filterdata(data, ispath=None, kwargs=None): """ Adequate data for dictionary creation. :param data: any supported object. :param ispath: True to add support for paths. :param kwargs: additional data to add in directory. :return: dictionary """ if isinstance(data, Directory): data = getattr(data, "__dict__") # data.__dict__ elif not isinstance(data, dict): if isinstance(data, list): data = {"repr": data} else: data = {"repr": [data]} if kwargs: data.update(kwargs) # update data with kwargs if ispath is not None: data["ispath"] = ispath if "ispath" not in data: data["ispath"] = True # ispath default if "repr" in data: data["repr"] = Directory.repr2list( data["repr"]) # ensure repr is maintained else: data["repr"] = [""] # repr default return data
[docs] @staticmethod def repr2list(data, level=0): """ Converts the representation of a directory.repr to pickleable. :param data: directory.repr of the form ["string",directory,...,directory.repr]. :return: pickleable list. """ if isinstance(data, list): # list defines levels of directories if level == 0: # [level 0, ..., [level 1, [...[level N]...]], level 0] # process several objects in the list for i, value in enumerate(data): data[i] = Directory.repr2list(value, level + 1) return data else: # convert anything to directory if not in level 0 return Directory(data) elif isinstance(data, str): # if string or directory return data else: # try to convert to directory return Directory(data)
[docs] @staticmethod def repr2str(data, ispath=True): """ Converts the representation of a directory.repr to string. :param data: directory.repr of the form ["string",directory,...,directory.repr]. :return: converted string. """ if isinstance(data, list): if len(data) > 1: string = str(Directory.repr2str(data[0])) for i in data[1:]: if ispath: string = joinPath( string, Directory.repr2str(i)) # join paths else: string += Directory.repr2str(i, ispath) return string else: return str(data[0]) # get single path elif isinstance(data, Directory): return Directory.repr2str(data.repr, data.ispath) else: return str(data) # object must be string
[docs] def correctSTRBuiltin(self): """ Decorate all the built-in functions of class directory. :return: built-in decorated function. """ def decorator(f): @wraps(f) def wrapper(*args, **kwargs): return getattr(str(self), f.__name__, f.__name__)(*args, **kwargs) return wrapper return decorator
[docs] def update_right(self, other): """ Updates representation a the right. :param other: any supported object. :return: new directory referenced to itself. .. note:: Equivalent to self + other e.g. directory([self, other]) """ self.repr.append(other) # it will be parsed at directory creation return Directory(self) # string is immutable and must be renewed
[docs] def update_left(self, other): """ Updates representation a the left. :param other: any supported object. :return: new directory referenced to itself. .. note:: Equivalent to self - other e.g. directory([other, self]) """ self.repr.insert(0, other) # it will be parsed at directory creation return Directory(self) # string is immutable and must be renewed
[docs] def update(self, data=None): """ Return an updated copy with provided data. :param data: any supported object. If None return updated and referenced copy of itself. :return: new directory referenced to itself. """ if isinstance(data, list): # if list if len(data) > len(self.repr): for i in range(len(self.repr)): self.repr[i] = data[i] for j in range(i + 1, len(data)): self.repr.append(data[j]) else: for i in range(len(data)): self.repr[i] = data[i] del self.repr[i + 1:] return Directory(self) # string is immutable and must be renewed elif isinstance(data, dict): # if dictionary for k, v in data.items(): # self.__dict__.update(data) setattr(self, k, v) return Directory(self) elif data: # if not list or dict return self.update([data]) else: # if None return updated version return Directory(self)
[docs] def copy(self): """ Creates copy of itself. :return: non-referenced directory copy. """ return Directory(self, copy=True)
def __str__(self): return Directory.repr2str(self.repr, self.ispath) def __repr__(self): return str(self.repr) def __add__(self, other): if type(other) == str: # it's exactly str an not directory return joinPath(self, other) return self.update_right(other) __iadd__ = __add__ def __sub__(self, other): if type(other) == str: # it's exactly str an not directory return joinPath(other, self) return self.update_left(other) # TODO: implement 2 versions: one with directory-like functionality and # other basic for str support """ ### MAGIC FUNCTIONS def __nonzero__(self): return self == self.update() def __getitem__(self, item): return self.repr[item] def __len__(self): return len(self.repr)"""
[docs]class FileDirectory(Directory): """ Saves contents of a file as with directories. :param data: list, directory instance, dictionary or string. :param filename: name of file. :param path: path to folder where file is (it must finish in /). :param notes: optional description string :param kwargs: additional data to add in directory. """ def __new__(cls, data, filename, path, notes=None): self = super(FileDirectory, cls).__new__(cls, data, False, False) self.filename = filename, self.path = path, self.notes = notes return self
[docs] def makeFile(self): """ Makes a file with its contents to path/filename. :return: True if successful """ path = mkPath(self.path) initfile = joinPath(path, self.filename) with open(initfile, 'wb') as logger: logger.write(str(self)) return True
if __name__ == "__main__": from . import session as sn # TESTS a = Directory("string1", sapo="mamo") b = a.update(["string2", ["string4", "string 5"]]) result = bool(a) # False result = bool(b) # True c = Directory(["string3"]) - a sn.saveSession("test.pkl", {"d", a}) c = sn.readSession("test.pkl") print(type(a) == Directory) print(type(a) is Directory) print(a is Directory) print(a == Directory) print("with str") print(type(a) == str) print(type(a) is str) print(a is str) print(a == str) print(isinstance(a, str)) print(type(a)) print(os.path.splitext(os.path.basename(__file__))) # look here ### path = Directory(["path1", "path2", "path3"]) #path += "path4" print(path) print(os.path.join(path, "new path"))