comparison version.py @ 324:ce79bf5fa463

- the cut and paste between file and dir conditions is always a bad thing - i made one function (hg_version) to basically call and parse hg - i made a function to include the cases of what might be returned by imp.find_modules (_input_id) - the check for a .hg folder was insufficient. Lots of things could go wrong. Instead I use the return code from the Popen process. The return code catches this and any other problem that hg runs into. - its easier to offer more rcs support in future (cvs,svn,git)
author James Bergstra <bergstrj@iro.umontreal.ca>
date Thu, 12 Jun 2008 20:54:49 -0400
parents 44f94ffe28f7
children 7a734dba4cac
comparison
equal deleted inserted replaced
323:44f94ffe28f7 324:ce79bf5fa463
1 import subprocess as _subprocess 1 import subprocess as _subprocess
2 import imp as _imp 2 import imp as _imp
3 import sys 3 import sys
4 import os 4 import os
5 5
6 _cache = {} 6
7 7
8 def src_version(module_name): 8 def src_version(module_name):
9 """Return compact identifier of module code. 9 """Return compact identifier of module code.
10 10
11 @return: compact identifier of module code. 11 @return: compact identifier of module code.
121 _cache[module_name] = tokens[0] 121 _cache[module_name] = tokens[0]
122 122
123 123
124 return _cache[module_name] 124 return _cache[module_name]
125 125
126 126 _unknown_version = 'unknown version'
127
128 def hg_version(dirname, filenames=None):
129 """Return current changeset of directory I{dirname}.
130
131 @type filename: list of str (or default: None)
132 @param filename: if specified, we ignore modifications to other files.
133
134 @rtype: tuple (last changeset, modified)
135
136 """
137 if type(filenames) not in (list, tuple, type(None)):
138 raise TypeError(filenames)
139
140 #may raise exception, for example if hg is not visible via PATH
141 status_proc = _subprocess.Popen(('hg','st'), cwd=dirname,
142 stdout=_subprocess.PIPE, stderr=_subprocess.PIPE)
143 status = status_proc.communicate()[0] #read stdout into buffer
144 if status_proc.returncode != 0:
145 raise OSError('hg returned %i, maybe %s is not under hg control?',
146 (status_proc.returncode, dirname))
147
148 #may raise exception, for example if hg is not visible via PATH
149 id_proc = _subprocess.Popen(('hg','id', '-i'), cwd=dirname,
150 stdout=_subprocess.PIPE, stderr=_subprocess.PIPE)
151 id_stdout = id_proc.communicate()[0]
152 if id_proc.returncode != 0:
153 raise OSError('hg returned %i, maybe %s is not under hg control?',
154 (id_proc.returncode, dirname))
155
156 care_about = (lambda some_file : True) if filenames is None \
157 else (lambda some_file : some_file in filenames)
158
159 # parse status codes for what we care about
160 care_about_mod = False
161 for line in status.split('\n'):
162 if not line: #empty lines happen
163 continue
164 line_file = line[2:]
165 if line[0] != '?' and care_about(line_file):
166 care_about_mod = True
167 #raise Exception('Uncommitted modification',
168 #os.path.join(dirname, line_file))
169 if line[0] == '?' and line[-3:] == '.py':
170 print >> sys.stderr, 'WARNING: untracked file', os.path.join(dirname, line_file)
171
172 # id_stdout is 12 hex digits followed by '+\n' or '\n'
173 # return the trailing '+' character only if there were changes to files that
174 # the caller cares about (named in filenames)
175 modified = (id_stdout[12] == '+')
176 assert len(id_stdout) in (13, 14) #sanity check
177 if modified and care_about_mod :
178 return id_stdout[:13]
179 else:
180 return id_stdout[:12]
181
182 def _import_id_py_source(location):
183 try:
184 dirname = os.path.dirname(location[1])
185 basename = os.path.basename(location[1])
186 return hg_version(dirname, [basename])
187 except OSError, e:
188 print >> sys.stderr, 'IGNORNING', e
189 return _unknown_version + ' PY_SOURCE'
190
191 def _import_id_py_compiled(location):
192 #a .pyc file was found, but no corresponding .py
193 return _unknown_version + ' PYC_COMPILED'
194
195 def _import_id_pkg_directory(location):
196 try:
197 return hg_version(location[1])
198 except OSError, e:
199 print >> sys.stderr, 'IGNORNING', e
200 return _unknown_version + ' PKG_DIRECTORY'
201
202 def _import_id(tag):
203 try :
204 location = _imp.find_module(tag)
205 except ImportError, e: #raise when tag is not found
206 return e #put this in the cache, import_id will raise it
207
208 #the find_module was successful, location is valid
209 resource_type = location[2][2]
210
211 if resource_type == _imp.PY_SOURCE:
212 return _import_id_py_source(location)
213 if resource_type == _imp.PY_COMPILED:
214 return _import_id_py_compiled(location)
215 if resource_type == _imp.C_EXTENSION:
216 raise NoteImplementedError
217 if resource_type == _imp.PY_RESOURCE:
218 raise NoteImplementedError
219 if resource_type == _imp.PKG_DIRECTORY:
220 return _import_id_pkg_directory(location)
221 if resource_type == _imp.C_BUILTIN:
222 raise NoteImplementedError
223 if resource_type == _imp.PY_FROZEN:
224 raise NoteImplementedError
225
226 assert False #the list of resource types above should be exhaustive
227
228 def import_id(tag):
229 """Return an identifier of the code imported by 'import <tag>'.
230
231 @param tag: a module or file name
232 @type tag: string
233
234 @rtype: string
235 @return: identifier of the code imported by 'import <tag>'.
236
237 This high-level function might do different things depending on, for
238 example, whether I{tag} identifies a file or a directory, or whether the
239 named entity is under some sort of version/revision control.
240
241 Versions are sought in the following order:
242 0. If I{tag} is 'python' then sys.version will be returned
243 1. If I{tag} names a file or folder under revision control, this function
244 will attempt to guess which one, and return a string that identifies the
245 running code (a revision id, not the whole file!)
246 2. If I{tag} names a module with a __version__ attribute, then that
247 attribute will be returned as a string.
248 3. The string starting with 'unknown version' will be returned for other valid modules.
249 4. An exception will be raise for non-existent modules.
250
251 @note: This function may import the named entity in order to return a
252 __version__ module attribute.
253
254 """
255 if tag not in import_id.cache:
256 import_id.cache[tag] = _import_id(tag)
257
258 #in the case of bad module names, we cached the ImportError exception
259 rval = import_id.cache[tag]
260 if isinstance(rval, Exception):
261 raise rval
262 return rval
263 import_id.cache = {'python':sys.version}
127 264
128 def get_all_src_versions() : 265 def get_all_src_versions() :
129 """ 266 """
130 Get the version of all loaded module. 267 Get the version of all loaded module.
131 Calls src_version on all loaded modules. These modules are found 268 Calls src_version on all loaded modules. These modules are found
137 @SEE src_version 274 @SEE src_version
138 """ 275 """
139 allmodules = sys.modules 276 allmodules = sys.modules
140 d = dict() 277 d = dict()
141 for m in allmodules : 278 for m in allmodules :
142 d[m] = src_version(m) 279 try:
280 d[m] = import_id(m)
281 except:
282 pass
143 return d 283 return d
144 284
145 285
146 if __name__ == "__main__" : 286 if __name__ == "__main__" :
147 287
148 if len(sys.argv) == 2 : 288 if len(sys.argv) == 2 :
149 print 'testing on', sys.argv[1] 289 print 'testing on', sys.argv[1]
150 print src_version(sys.argv[1]) 290 print import_id(sys.argv[1])
151 291