1 """
2 File- and directory related helpers for resource utilites.
3 """
4
5
6
7
8
9
10
11 from __future__ import print_function
12
13 import os
14 import re
15 import warnings
16
17 from gavo import base
18 from gavo import rscdef
19 from gavo.base import parsecontext
20
21 -class Error(base.Error):
23
24
25 fnamePat = re.compile(r"([^.]*)(\..*)")
26 -def stingySplitext(fName):
27 """returns name, extension for fName.
28
29 The main difference to os.path.splitext is that the main name is not allowed
30 to contain dots and the extension can contain more than one dot.
31
32 fName is supposed to be a single file name without any path specifier
33 (you might get aways with it if your directores do not contain dots, though).
34 """
35 mat = fnamePat.match(fName)
36 if mat:
37 return mat.group(1), mat.group(2)
38 else:
39 return fName, ""
40
43 """is a name mapper for file rename operations and the like.
44
45 Warning: This whole thing more or less pretends there are no
46 symlinks.
47 """
48 - def __init__(self, map, showOnly=False):
49 self.map, self.showOnly = map, showOnly
50
51 @classmethod
53 """returns a name map for whatever is serialized in the file inF.
54
55 The format of fName is line-base, with each line being one of
56
57 - empty -- ignored
58 - beginning with a hash -- ignored
59 - <old> -> <new> -- introducing a map
60 """
61 map = {}
62 try:
63 for ln in inF:
64 if not ln.strip() or ln.strip().startswith("#"):
65 continue
66 old, new = [s.strip() for s in ln.split("->", 2)]
67 if old in map:
68 raise Error("Two mappings for %s"%old)
69 map[old] = new
70 except ValueError:
71 raise base.ui.logOldExc(Error("Invalid mapping line: %s"%repr(ln)))
72 return cls(map, **kwargs)
73
75 """returns a dictionary old->new of renames within path.
76 """
77 fileMap = {}
78 for dir, subdirs, fNames in os.walk(path):
79 for fName in fNames:
80 stem, ext = stingySplitext(fName)
81 if stem in self.map:
82 fileMap[os.path.join(dir, fName)] = os.path.join(dir,
83 self.map[stem]+ext)
84 return fileMap
85
87 """returns a sequence of (old,new) pairs that, when executed, keep
88 any new from clobbering any existing old.
89
90 The function will raise an Error if there's a cycle in fileMap. fileMap
91 will be destroyed by this procedure
92 """
93 proc = []
94 def addOp(src, dest, sources=None):
95 if sources is None:
96 sources = set()
97 if dest in sources:
98 raise Error("Rename cycle involving %s"%sources)
99 if dest in fileMap:
100 sources.add(src)
101 addOp(dest, fileMap.pop(dest), sources)
102 proc.append((src, dest))
103 while fileMap:
104 addOp(*fileMap.popitem())
105 return proc
106
108 """performs a name map below path.
109
110 The rules are:
111
112 - extensions are ignored -- if we map foo to bar, foo.txt and foo.asc
113 will be renamed bar.txt and foo.txt respectively
114 - the order of statements in the source is irrelevant. However, we try
115 not to clobber anything we've just renamed and will complain about
116 cycles. Also, each file will be renamed not more than once.
117 """
118 fileMap = self.getFileMap(path)
119 for src, dest in self.makeRenameProc(fileMap):
120 if os.path.exists(dest):
121 raise Error("Request to clobber %s"%repr(dest))
122 if os.path.exists(src):
123 if self.showOnly:
124 print("%s -> %s"%(src, dest))
125 else:
126 os.rename(src, dest)
127 else:
128 if not os.path.exists(dest):
129 warnings.warn("Neither source nor dest found in pair %s, %s"%(src,
130 dest))
131
135
138 """iterates over the current sources of the data descriptor ddId (which is
139 qualified like rdId#id
140
141 If you pass something nonempty to args, an iterator over its values
142 will be returned. This is for convenient implementation of scripts
143 that work on CL arguments if given, on all files otherwise.
144 """
145 if args:
146 return iter(args)
147 else:
148 if ddId.count("#")!=1:
149 raise base.ReportableError("iterSources must get a fully qualified id"
150 " (i.e., one with exactly one hash).")
151 dd = parsecontext.resolveCrossId(ddId, rscdef.DataDescriptor)
152 return dd.sources.iterSources()
153