PageRenderTime 51ms CodeModel.GetById 13ms RepoModel.GetById 1ms app.codeStats 0ms

/python/lib/grizzled/grizzled/file/includer.py

https://gitlab.com/gregtyka/frankenserver
Python | 473 lines | 380 code | 26 blank | 67 comment | 15 complexity | 802f1d0a90526aef683ce91bf09dd07c MD5 | raw file
  1. #!/usr/bin/env python
  2. # NOTE: Documentation is intended to be processed by epydoc and contains
  3. # epydoc markup.
  4. '''
  5. Introduction
  6. ============
  7. The ``grizzled.file.includer`` module contains a class that can be used to
  8. process includes within a text file, returning a file-like object. It also
  9. contains some utility functions that permit using include-enabled files in
  10. other contexts.
  11. Include Syntax
  12. ==============
  13. The *include* syntax is defined by a regular expression; any line that matches
  14. the regular expression is treated as an *include* directive. The default
  15. regular expression matches include directives like this::
  16. %include "/absolute/path/to/file"
  17. %include "../relative/path/to/file"
  18. %include "local_reference"
  19. %include "http://localhost/path/to/my.config"
  20. Relative and local file references are relative to the including file or URL.
  21. That, if an ``Includer`` is processing file "/home/bmc/foo.txt" and encounters
  22. an attempt to include file "bar.txt", it will assume "bar.txt" is to be found
  23. in "/home/bmc".
  24. Similarly, if an ``Includer`` is processing URL "http://localhost/bmc/foo.txt"
  25. and encounters an attempt to include file "bar.txt", it will assume "bar.txt"
  26. is to be found at "http://localhost/bmc/bar.txt".
  27. Nested includes are permitted; that is, an included file may, itself, include
  28. other files. The maximum recursion level is configurable and defaults to 100.
  29. The include syntax can be changed by passing a different regular expression to
  30. the ``Includer`` class constructor.
  31. Usage
  32. =====
  33. This module provides an ``Includer`` class, which processes include directives
  34. in a file and behaves like a file-like object. See the class documentation for
  35. more details.
  36. The module also provides a ``preprocess()`` convenience function that can be
  37. used to preprocess a file; it returns the path to the resulting preprocessed
  38. file.
  39. Examples
  40. ========
  41. Preprocess a file containing include directives, then read the result:
  42. .. python::
  43. import includer
  44. import sys
  45. inc = includer.Includer(path)
  46. for line in inc:
  47. sys.stdout.write(line)
  48. Use an include-enabled file with the standard Python logging module:
  49. .. python::
  50. import logging
  51. import includer
  52. logging.fileConfig(includer.preprocess("mylog.cfg"))
  53. '''
  54. __docformat__ = "restructuredtext en"
  55. __all__ = ['Includer', 'IncludeError', 'preprocess']
  56. # ---------------------------------------------------------------------------
  57. # Imports
  58. # ---------------------------------------------------------------------------
  59. import logging
  60. import os
  61. import sys
  62. import re
  63. import tempfile
  64. import atexit
  65. import urllib2
  66. import urlparse
  67. import grizzled.exception
  68. from grizzled.file import unlink_quietly
  69. # ---------------------------------------------------------------------------
  70. # Exports
  71. # ---------------------------------------------------------------------------
  72. __all__ = ['IncludeError', 'Includer', 'preprocess']
  73. # ---------------------------------------------------------------------------
  74. # Logging
  75. # ---------------------------------------------------------------------------
  76. log = logging.getLogger('includer')
  77. # ---------------------------------------------------------------------------
  78. # Public classes
  79. # ---------------------------------------------------------------------------
  80. class IncludeError(grizzled.exception.ExceptionWithMessage):
  81. """
  82. Thrown by ``Includer`` when an error occurs while processing the file.
  83. An ``IncludeError`` object always contains a single string value that
  84. contains an error message describing the problem.
  85. """
  86. pass
  87. class Includer(object):
  88. '''
  89. An ``Includer`` object preprocesses a path or file-like object,
  90. expanding include references. The resulting ``Includer`` object is a
  91. file-like object, offering the same methods and capabilities as an open
  92. file.
  93. By default, ``Includer`` supports this include syntax::
  94. %include "path"
  95. %include "url"
  96. However, the include directive syntax is controlled by a regular
  97. expression, so it can be configured.
  98. See the module documentation for details.
  99. '''
  100. def __init__(self,
  101. source,
  102. include_regex='^%include\s"([^"]+)"',
  103. max_nest_level=100,
  104. output=None):
  105. """
  106. Create a new ``Includer`` object.
  107. :Parameters:
  108. source : file or str
  109. The source to be read and expanded. May be an open file-like
  110. object, a path name, or a URL string.
  111. include_regex : str
  112. Regular expression defining the include syntax. Must contain a
  113. single parenthetical group that can be used to extract the
  114. included file or URL.
  115. max_nest_level : int
  116. Maximum include nesting level. Exceeding this level will cause
  117. ``Includer`` to throw an ``IncludeError``.
  118. output : str or file
  119. A string (path name) or file-like object to which to save the
  120. expanded output.
  121. :raise IncludeError: On error
  122. """
  123. if isinstance(source, str):
  124. f, is_url, name = self.__open(source, None, False)
  125. else:
  126. # Assume file-like object.
  127. f = source
  128. is_url = False
  129. try:
  130. name = source.name
  131. except AttributeError:
  132. name = None
  133. self.closed = False
  134. self.mode = None
  135. self.__include_pattern = re.compile(include_regex)
  136. self.__name = name
  137. if output == None:
  138. from cStringIO import StringIO
  139. output = StringIO()
  140. self.__maxnest = max_nest_level
  141. self.__nested = 0
  142. self.__process_includes(f, name, is_url, output)
  143. self.__f = output
  144. self.__f.seek(0)
  145. @property
  146. def name(self):
  147. """
  148. Get the name of the file being processed.
  149. """
  150. return self.__name
  151. def __iter__(self):
  152. return self
  153. def next(self):
  154. """A file object is its own iterator.
  155. :rtype: string
  156. :return: the next line from the file
  157. :raise StopIteration: end of file
  158. :raise IncludeError: on error
  159. """
  160. line = self.readline()
  161. if (line == None) or (len(line) == 0):
  162. raise StopIteration
  163. return line
  164. def close(self):
  165. """Close the includer, preventing any further I/O operations."""
  166. if not self.closed:
  167. self.closed = true
  168. self.__f.close()
  169. del self.__f
  170. def fileno(self):
  171. """
  172. Get the file descriptor. Returns the descriptor of the file being
  173. read.
  174. :rtype: int
  175. :return: the file descriptor of the file being read
  176. """
  177. _complain_if_closed(self.closed)
  178. return self.__f.fileno()
  179. def isatty(self):
  180. """
  181. Determine whether the file being processed is a TTY or not.
  182. :return: ``True`` or ``False``
  183. """
  184. _complain_if_closed(self.closed)
  185. return self.__f.isatty()
  186. def seek(self, pos, mode=0):
  187. """
  188. Seek to the specified file offset in the include-processed file.
  189. :Parameters:
  190. pos : int
  191. file offset
  192. mode : int
  193. the seek mode, as specified to a Python file's ``seek()``
  194. method
  195. """
  196. self.__f.seek(pos, mode)
  197. def tell(self):
  198. """
  199. Get the current file offset.
  200. :rtype: int
  201. :return: current file offset
  202. """
  203. _complain_if_closed(self.closed)
  204. return self.__f.tell()
  205. def read(self, n=-1):
  206. """
  207. Read *n* bytes from the open file.
  208. :Parameters:
  209. n : int
  210. Number of bytes to read. A negative number instructs
  211. the method to read all remaining bytes.
  212. :return: the bytes read
  213. """
  214. _complain_if_closed(self.closed)
  215. return self.__f.read(n)
  216. def readline(self, length=-1):
  217. """
  218. Read the next line from the file.
  219. :Parameters:
  220. length : int
  221. a length hint, or negative if you don't care
  222. :rtype: str
  223. :return: the line read
  224. """
  225. _complain_if_closed(self.closed)
  226. return self.__f.readline(length)
  227. def readlines(self, sizehint=0):
  228. """
  229. Read all remaining lines in the file.
  230. :rtype: array
  231. :return: array of lines
  232. """
  233. _complain_if_closed(self.closed)
  234. return self.__f.readlines(sizehint)
  235. def truncate(self, size=None):
  236. """Not supported, since ``Includer`` objects are read-only."""
  237. raise IncludeError, 'Includers are read-only file objects.'
  238. def write(self, s):
  239. """Not supported, since ``Includer`` objects are read-only."""
  240. raise IncludeError, 'Includers are read-only file objects.'
  241. def writelines(self, iterable):
  242. """Not supported, since ``Includer`` objects are read-only."""
  243. raise IncludeError, 'Includers are read-only file objects.'
  244. def flush(self):
  245. """No-op."""
  246. pass
  247. def getvalue(self):
  248. """
  249. Retrieve the entire contents of the file, which includes expanded,
  250. at any time before the ``close()`` method is called.
  251. :rtype: string
  252. :return: a single string containing the contents of the file
  253. """
  254. return ''.join(self.readlines())
  255. def __process_includes(self, file_in, filename, is_url, file_out):
  256. log.debug('Processing includes in "%s", is_url=%s' % (filename, is_url))
  257. for line in file_in:
  258. match = self.__include_pattern.search(line)
  259. if match:
  260. if self.__nested >= self.__maxnest:
  261. raise IncludeError, 'Exceeded maximum include recursion ' \
  262. 'depth of %d' % self.__maxnest
  263. inc_name = match.group(1)
  264. logging.debug('Found include directive: %s' % line[:-1])
  265. f, included_is_url, included_name = self.__open(inc_name,
  266. filename,
  267. is_url)
  268. self.__nested += 1
  269. self.__process_includes(f, filename, is_url, file_out)
  270. self.__nested -= 1
  271. else:
  272. file_out.write(line)
  273. def __open(self, name_to_open, enclosing_file, enclosing_file_is_url):
  274. is_url = False
  275. openFunc = None
  276. parsed_url = urlparse.urlparse(name_to_open)
  277. # Account for Windows drive letters.
  278. if (parsed_url.scheme != '') and (len(parsed_url.scheme) > 1):
  279. openFunc = urllib2.urlopen
  280. is_url = True
  281. else:
  282. # It's not a URL. What we do now depends on the including file.
  283. if enclosing_file_is_url:
  284. # Use the parent URL as the base URL.
  285. name_to_open = urlparse.urljoin(enclosing_file, name_to_open)
  286. open_func = urllib2.urlopen
  287. is_url = True
  288. elif not os.path.isabs(name_to_open):
  289. # Not an absolute file. Base it on the parent.
  290. enclosing_dir = None
  291. if enclosing_file == None:
  292. enclosing_dir = os.getcwd()
  293. else:
  294. enclosing_dir = os.path.dirname(enclosing_file)
  295. name_to_open = os.path.join(enclosing_dir, name_to_open)
  296. open_func = open
  297. else:
  298. open_func = open
  299. assert(name_to_open != None)
  300. assert(open_func != None)
  301. try:
  302. log.debug('Opening "%s"' % name_to_open)
  303. f = open_func(name_to_open)
  304. except:
  305. raise IncludeError, 'Unable to open "%s" as a file or a URL' %\
  306. name_to_open
  307. return (f, is_url, name_to_open)
  308. # ---------------------------------------------------------------------------
  309. # Public functions
  310. # ---------------------------------------------------------------------------
  311. def preprocess(file_or_url, output=None, temp_suffix='.txt', temp_prefix='inc'):
  312. """
  313. Process all include directives in the specified file, returning a path
  314. to a temporary file that contains the results of the expansion. The
  315. temporary file is automatically removed when the program exits, though
  316. the caller is free to remove it whenever it is no longer needed.
  317. :Parameters:
  318. file_or_url : file or str
  319. URL or path to file to be expanded; or, a file-like object
  320. output : file
  321. A file or file-like object to receive the output.
  322. temp_suffix : str
  323. suffix to use with temporary file that holds preprocessed output
  324. temp_prefix : str
  325. prefix to use with temporary file that holds preprocessed output
  326. :rtype: string
  327. :return: ``output``, if ``output`` is not ``None``; otherwise, the path to
  328. temporary file containing expanded content
  329. """
  330. result = None
  331. path = None
  332. if not output:
  333. fd, path = tempfile.mkstemp(suffix=temp_suffix, prefix=temp_prefix)
  334. output = open(path, 'w')
  335. atexit.register(unlink_quietly, path)
  336. os.close(fd)
  337. result = path
  338. else:
  339. result = output
  340. Includer(file_or_url, output=output)
  341. return result
  342. # ---------------------------------------------------------------------------
  343. # Private functions
  344. # ---------------------------------------------------------------------------
  345. def _complain_if_closed(closed):
  346. if closed:
  347. raise IncludeError, "I/O operation on closed file"
  348. # ---------------------------------------------------------------------------
  349. # Main program (for testing)
  350. # ---------------------------------------------------------------------------
  351. if __name__ == '__main__':
  352. format = '%(asctime)s %(name)s %(levelname)s %(message)s'
  353. logging.basicConfig(level=logging.DEBUG, format=format)
  354. for file in sys.argv[1:]:
  355. import cStringIO as StringIO
  356. out = StringIO.StringIO()
  357. preprocess(file, output=out)
  358. header = 'File: %s, via preprocess()'
  359. sep = '-' * len(header)
  360. print '\n%s\n%s\n%s\n' % (sep, header, sep)
  361. for line in out.readlines():
  362. sys.stdout.write(line)
  363. print sep
  364. inc = Includer(file)
  365. header = 'File: %s, via Includer'
  366. sep = '-' * len(header)
  367. print '\n%s\n%s\n%s\n' % (sep, header, sep)
  368. for line in inc:
  369. sys.stdout.write(line)
  370. print '%s' % sep