Package epydoc :: Module util
[hide private]
[frames] | no frames]

Source Code for Module epydoc.util

  1  # epydoc -- Utility functions 
  2  # 
  3  # Copyright (C) 2005 Edward Loper 
  4  # Author: Edward Loper <edloper@loper.org> 
  5  # URL: <http://epydoc.sf.net> 
  6  # 
  7  # $Id: util.py 1721 2008-02-15 01:02:30Z edloper $ 
  8   
  9  """ 
 10  Miscellaneous utility functions that are used by multiple modules. 
 11   
 12  @group Python source types: is_module_file, is_package_dir, is_pyname, 
 13      py_src_filename 
 14  @group Text processing: wordwrap, decode_with_backslashreplace, 
 15      plaintext_to_html 
 16  """ 
 17  __docformat__ = 'epytext en' 
 18   
 19  import os, os.path, re, sys 
 20   
 21  ###################################################################### 
 22  ## Python Source Types 
 23  ###################################################################### 
 24   
 25  PY_SRC_EXTENSIONS = ['.py', '.pyw'] 
 26  PY_BIN_EXTENSIONS = ['.pyc', '.so', '.pyd'] 
 27   
28 -def is_module_file(path):
29 # Make sure it's a file name. 30 if not isinstance(path, basestring): 31 return False 32 (dir, filename) = os.path.split(path) 33 (basename, extension) = os.path.splitext(filename) 34 return (os.path.isfile(path) and 35 re.match('[a-zA-Z_]\w*$', basename) and 36 extension in PY_SRC_EXTENSIONS+PY_BIN_EXTENSIONS)
37
38 -def is_src_filename(filename):
39 if not isinstance(filename, basestring): return False 40 if not os.path.exists(filename): return False 41 return os.path.splitext(filename)[1] in PY_SRC_EXTENSIONS
42
43 -def is_package_dir(dirname):
44 """ 45 Return true if the given directory is a valid package directory 46 (i.e., it names a directory that contains a valid __init__ file, 47 and its name is a valid identifier). 48 """ 49 # Make sure it's a directory name. 50 if not isinstance(dirname, basestring): 51 return False 52 if not os.path.isdir(dirname): 53 return False 54 dirname = os.path.abspath(dirname) 55 # Make sure it's a valid identifier. (Special case for 56 # "foo/", where os.path.split -> ("foo", "").) 57 (parent, dir) = os.path.split(dirname) 58 if dir == '': (parent, dir) = os.path.split(parent) 59 60 # The following constraint was removed because of sourceforge 61 # bug #1787028 -- in some cases (eg eggs), it's too strict. 62 #if not re.match('\w+$', dir): 63 # return False 64 65 for name in os.listdir(dirname): 66 filename = os.path.join(dirname, name) 67 if name.startswith('__init__.') and is_module_file(filename): 68 return True 69 else: 70 return False
71
72 -def is_pyname(name):
73 return re.match(r"\w+(\.\w+)*$", name)
74
75 -def py_src_filename(filename):
76 basefile, extension = os.path.splitext(filename) 77 if extension in PY_SRC_EXTENSIONS: 78 return filename 79 else: 80 for ext in PY_SRC_EXTENSIONS: 81 if os.path.isfile('%s%s' % (basefile, ext)): 82 return '%s%s' % (basefile, ext) 83 else: 84 raise ValueError('Could not find a corresponding ' 85 'Python source file for %r.' % filename)
86
87 -def munge_script_name(filename):
88 name = os.path.split(filename)[1] 89 name = re.sub(r'\W', '_', name) 90 return 'script-'+name
91 92 ###################################################################### 93 ## Text Processing 94 ###################################################################### 95
96 -def decode_with_backslashreplace(s):
97 r""" 98 Convert the given 8-bit string into unicode, treating any 99 character c such that ord(c)<128 as an ascii character, and 100 converting any c such that ord(c)>128 into a backslashed escape 101 sequence. 102 103 >>> decode_with_backslashreplace('abc\xff\xe8') 104 u'abc\\xff\\xe8' 105 """ 106 # s.encode('string-escape') is not appropriate here, since it 107 # also adds backslashes to some ascii chars (eg \ and '). 108 assert isinstance(s, str) 109 return (s 110 .decode('latin1') 111 .encode('ascii', 'backslashreplace') 112 .decode('ascii'))
113
114 -def wordwrap(str, indent=0, right=75, startindex=0, splitchars=''):
115 """ 116 Word-wrap the given string. I.e., add newlines to the string such 117 that any lines that are longer than C{right} are broken into 118 shorter lines (at the first whitespace sequence that occurs before 119 index C{right}). If the given string contains newlines, they will 120 I{not} be removed. Any lines that begin with whitespace will not 121 be wordwrapped. 122 123 @param indent: If specified, then indent each line by this number 124 of spaces. 125 @type indent: C{int} 126 @param right: The right margin for word wrapping. Lines that are 127 longer than C{right} will be broken at the first whitespace 128 sequence before the right margin. 129 @type right: C{int} 130 @param startindex: If specified, then assume that the first line 131 is already preceeded by C{startindex} characters. 132 @type startindex: C{int} 133 @param splitchars: A list of non-whitespace characters which can 134 be used to split a line. (E.g., use '/\\' to allow path names 135 to be split over multiple lines.) 136 @rtype: C{str} 137 """ 138 if splitchars: 139 chunks = re.split(r'( +|\n|[^ \n%s]*[%s])' % 140 (re.escape(splitchars), re.escape(splitchars)), 141 str.expandtabs()) 142 else: 143 chunks = re.split(r'( +|\n)', str.expandtabs()) 144 result = [' '*(indent-startindex)] 145 charindex = max(indent, startindex) 146 for chunknum, chunk in enumerate(chunks): 147 if (charindex+len(chunk) > right and charindex > 0) or chunk == '\n': 148 result.append('\n' + ' '*indent) 149 charindex = indent 150 if chunk[:1] not in ('\n', ' '): 151 result.append(chunk) 152 charindex += len(chunk) 153 else: 154 result.append(chunk) 155 charindex += len(chunk) 156 return ''.join(result).rstrip()+'\n'
157
158 -def plaintext_to_html(s):
159 """ 160 @return: An HTML string that encodes the given plaintext string. 161 In particular, special characters (such as C{'<'} and C{'&'}) 162 are escaped. 163 @rtype: C{string} 164 """ 165 s = s.replace('&', '&amp;').replace('"', '&quot;') 166 s = s.replace('<', '&lt;').replace('>', '&gt;') 167 return s
168
169 -def plaintext_to_latex(str, nbsp=0, breakany=0):
170 """ 171 @return: A LaTeX string that encodes the given plaintext string. 172 In particular, special characters (such as C{'$'} and C{'_'}) 173 are escaped, and tabs are expanded. 174 @rtype: C{string} 175 @param breakany: Insert hyphenation marks, so that LaTeX can 176 break the resulting string at any point. This is useful for 177 small boxes (e.g., the type box in the variable list table). 178 @param nbsp: Replace every space with a non-breaking space 179 (C{'~'}). 180 """ 181 # These get converted to hyphenation points later 182 if breakany: str = re.sub('(.)', '\\1\1', str) 183 184 # These get converted to \textbackslash later. 185 str = str.replace('\\', '\0') 186 187 # Expand tabs 188 str = str.expandtabs() 189 190 # These elements need to be backslashed. 191 str = re.sub(r'([#$&%_\${}])', r'\\\1', str) 192 193 # These elements have special names. 194 str = str.replace('|', '{\\textbar}') 195 str = str.replace('<', '{\\textless}') 196 str = str.replace('>', '{\\textgreater}') 197 str = str.replace('^', '{\\textasciicircum}') 198 str = str.replace('~', '{\\textasciitilde}') 199 str = str.replace('\0', r'{\textbackslash}') 200 201 # replace spaces with non-breaking spaces 202 if nbsp: str = str.replace(' ', '~') 203 204 # Convert \1's to hyphenation points. 205 if breakany: str = str.replace('\1', r'\-') 206 207 return str
208
209 -class RunSubprocessError(OSError):
210 - def __init__(self, cmd, out, err):
211 OSError.__init__(self, '%s failed' % cmd[0]) 212 self.out = out 213 self.err = err
214
215 -def run_subprocess(cmd, data=None):
216 """ 217 Execute the command C{cmd} in a subprocess. 218 219 @param cmd: The command to execute, specified as a list 220 of string. 221 @param data: A string containing data to send to the 222 subprocess. 223 @return: A tuple C{(out, err)}. 224 @raise OSError: If there is any problem executing the 225 command, or if its exitval is not 0. 226 """ 227 if isinstance(cmd, basestring): 228 cmd = cmd.split() 229 230 # Under Python 2.4+, use subprocess 231 try: 232 from subprocess import Popen, PIPE 233 pipe = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) 234 out, err = pipe.communicate(data) 235 if hasattr(pipe, 'returncode'): 236 if pipe.returncode == 0: 237 return out, err 238 else: 239 raise RunSubprocessError(cmd, out, err) 240 else: 241 # Assume that there was an error iff anything was written 242 # to the child's stderr. 243 if err == '': 244 return out, err 245 else: 246 raise RunSubprocessError(cmd, out, err) 247 except ImportError: 248 pass 249 250 # Under Python 2.3 or earlier, on unix, use popen2.Popen3 so we 251 # can access the return value. 252 import popen2 253 if hasattr(popen2, 'Popen3'): 254 pipe = popen2.Popen3(' '.join(cmd), True) 255 to_child = pipe.tochild 256 from_child = pipe.fromchild 257 child_err = pipe.childerr 258 if data: 259 to_child.write(data) 260 to_child.close() 261 out = err = '' 262 while pipe.poll() is None: 263 out += from_child.read() 264 err += child_err.read() 265 out += from_child.read() 266 err += child_err.read() 267 if pipe.wait() == 0: 268 return out, err 269 else: 270 raise RunSubprocessError(cmd, out, err) 271 272 # Under Python 2.3 or earlier, on non-unix, use os.popen3 273 else: 274 to_child, from_child, child_err = os.popen3(' '.join(cmd), 'b') 275 if data: 276 try: 277 to_child.write(data) 278 # Guard for a broken pipe error 279 except IOError, e: 280 raise OSError(e) 281 to_child.close() 282 out = from_child.read() 283 err = child_err.read() 284 # Assume that there was an error iff anything was written 285 # to the child's stderr. 286 if err == '': 287 return out, err 288 else: 289 raise RunSubprocessError(cmd, out, err)
290 291 ###################################################################### 292 ## Terminal Control 293 ###################################################################### 294
295 -class TerminalController:
296 """ 297 A class that can be used to portably generate formatted output to 298 a terminal. See 299 U{http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/475116} 300 for documentation. (This is a somewhat stripped-down version.) 301 """ 302 BOL = '' #: Move the cursor to the beginning of the line 303 UP = '' #: Move the cursor up one line 304 DOWN = '' #: Move the cursor down one line 305 LEFT = '' #: Move the cursor left one char 306 RIGHT = '' #: Move the cursor right one char 307 CLEAR_EOL = '' #: Clear to the end of the line. 308 CLEAR_LINE = '' #: Clear the current line; cursor to BOL. 309 BOLD = '' #: Turn on bold mode 310 NORMAL = '' #: Turn off all modes 311 COLS = 75 #: Width of the terminal (default to 75) 312 UNDERLINE = '' #: Underline the text 313 REVERSE = '' #: Reverse the foreground & background 314 BLACK = BLUE = GREEN = CYAN = RED = MAGENTA = YELLOW = WHITE = '' 315 316 _STRING_CAPABILITIES = """ 317 BOL=cr UP=cuu1 DOWN=cud1 LEFT=cub1 RIGHT=cuf1 REVERSE=rev 318 CLEAR_EOL=el BOLD=bold UNDERLINE=smul NORMAL=sgr0""".split() 319 _COLORS = """BLACK BLUE GREEN CYAN RED MAGENTA YELLOW WHITE""".split() 320 _ANSICOLORS = "BLACK RED GREEN YELLOW BLUE MAGENTA CYAN WHITE".split() 321 322 #: If this is set to true, then new TerminalControllers will 323 #: assume that the terminal is not capable of doing manipulation 324 #: of any kind. 325 FORCE_SIMPLE_TERM = False 326
327 - def __init__(self, term_stream=sys.stdout):
328 # If the stream isn't a tty, then assume it has no capabilities. 329 if not term_stream.isatty(): return 330 if self.FORCE_SIMPLE_TERM: return 331 332 # Curses isn't available on all platforms 333 try: import curses 334 except: 335 # If it's not available, then try faking enough to get a 336 # simple progress bar. 337 self.BOL = '\r' 338 self.CLEAR_LINE = '\r' + ' '*self.COLS + '\r' 339 340 # Check the terminal type. If we fail, then assume that the 341 # terminal has no capabilities. 342 try: curses.setupterm() 343 except: return 344 345 # Look up numeric capabilities. 346 self.COLS = curses.tigetnum('cols') 347 348 # Look up string capabilities. 349 for capability in self._STRING_CAPABILITIES: 350 (attrib, cap_name) = capability.split('=') 351 setattr(self, attrib, self._tigetstr(cap_name) or '') 352 if self.BOL and self.CLEAR_EOL: 353 self.CLEAR_LINE = self.BOL+self.CLEAR_EOL 354 355 # Colors 356 set_fg = self._tigetstr('setf') 357 if set_fg: 358 for i,color in zip(range(len(self._COLORS)), self._COLORS): 359 setattr(self, color, curses.tparm(set_fg, i) or '') 360 set_fg_ansi = self._tigetstr('setaf') 361 if set_fg_ansi: 362 for i,color in zip(range(len(self._ANSICOLORS)), self._ANSICOLORS): 363 setattr(self, color, curses.tparm(set_fg_ansi, i) or '')
364
365 - def _tigetstr(self, cap_name):
366 # String capabilities can include "delays" of the form "$<2>". 367 # For any modern terminal, we should be able to just ignore 368 # these, so strip them out. 369 import curses 370 cap = curses.tigetstr(cap_name) or '' 371 return re.sub(r'\$<\d+>[/*]?', '', cap)
372
373 - def render(self, template):
374 """ 375 Replace each $-substitutions in the given template string with 376 the corresponding terminal control string (if it's defined) or 377 '' (if it's not). 378 """ 379 return re.sub(r'\$\$|\${\w+}', self._render_sub, template)
380
381 - def _render_sub(self, match):
382 s = match.group() 383 if s == '$$': return s 384 else: return getattr(self, s[2:-1])
385