A fork of hyde, the static site generation. Some patches will be pushed upstream.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

367 lines
11 KiB

  1. # -*- coding: utf-8 -*-
  2. """
  3. Unified object oriented interface for interacting with file system objects.
  4. File system operations in python are distributed across modules: os, os.path,
  5. fnamtch, shutil and distutils. This module attempts to make the right choices
  6. for common operations to provide a single interface.
  7. """
  8. import codecs
  9. import contextlib
  10. import logging
  11. from logging import NullHandler
  12. import os
  13. import shutil
  14. from distutils import dir_util
  15. import functools
  16. # pylint: disable-msg=E0611
  17. logger = logging.getLogger('fs')
  18. logger.addHandler(NullHandler())
  19. __all__ = ['File', 'Folder']
  20. class FS(object):
  21. """
  22. The base file system object
  23. """
  24. def __init__(self, path):
  25. super(FS, self).__init__()
  26. self.path = str(path).strip()
  27. def __str__(self):
  28. return self.path
  29. def __repr__(self):
  30. return self.path
  31. def __eq__(self, other):
  32. return str(self) == str(other)
  33. def __ne__(self, other):
  34. return str(self) != str(other)
  35. @property
  36. def exists(self):
  37. """
  38. Does the file system object exist?
  39. """
  40. return os.path.exists(self.path)
  41. @property
  42. def name(self):
  43. """
  44. Returns the name of the FS object with its extension
  45. """
  46. return os.path.basename(self.path)
  47. @property
  48. def parent(self):
  49. """
  50. The parent folder. Returns a `Folder` object.
  51. """
  52. return Folder(os.path.dirname(self.path))
  53. def ancestors(self, stop=None):
  54. """
  55. Generates the parents until stop or the absolute
  56. root directory is reached.
  57. """
  58. f = self
  59. while f.parent != stop:
  60. if f.parent == f:
  61. return
  62. yield f.parent
  63. f = f.parent
  64. def get_fragment(self, root):
  65. """
  66. Gets the fragment of the current path starting at root.
  67. """
  68. return functools.reduce(lambda f, p: Folder(p.name).child(f), self.ancestors(stop=root), self.name)
  69. def get_mirror(self, target_root, source_root=None):
  70. """
  71. Returns a File or Folder object that reperesents if the entire fragment of this
  72. directory starting with `source_root` were copied to `target_root`.
  73. >>> Folder('/usr/local/hyde/stuff').get_mirror('/usr/tmp', source_root='/usr/local/hyde')
  74. Folder('/usr/tmp/stuff')
  75. """
  76. fragment = self.get_fragment(source_root if source_root else self.parent)
  77. return Folder(target_root).child(fragment)
  78. @staticmethod
  79. def file_or_folder(path):
  80. """
  81. Returns a File or Folder object that would represent the given path.
  82. """
  83. target = str(path)
  84. return Folder(target) if os.path.isdir(target) else File(target)
  85. def __get_destination__(self, destination):
  86. """
  87. Returns a File or Folder object that would represent this entity
  88. if it were copied or moved to `destination`.
  89. """
  90. if (isinstance(destination, File) or os.path.isfile(str(destination))):
  91. return destination
  92. else:
  93. return FS.file_or_folder(Folder(destination).child(self.name))
  94. class File(FS):
  95. """
  96. The File object.
  97. """
  98. def __init__(self, path):
  99. super(File, self).__init__(path)
  100. @property
  101. def name_without_extension(self):
  102. """
  103. Returns the name of the FS object without its extension
  104. """
  105. return os.path.splitext(self.name)[0]
  106. @property
  107. def extension(self):
  108. """
  109. File extension prefixed with a dot.
  110. """
  111. return os.path.splitext(self.path)[1]
  112. @property
  113. def kind(self):
  114. """
  115. File extension without dot prefix.
  116. """
  117. return self.extension.lstrip(".")
  118. def read_all(self, encoding='utf-8'):
  119. """
  120. Reads from the file and returns the content as a string.
  121. """
  122. logger.info("Reading everything from %s" % self)
  123. with codecs.open(self.path, 'r', encoding) as fin:
  124. read_text = fin.read()
  125. return read_text
  126. def write(self, text, encoding="utf-8"):
  127. """
  128. Writes the given text to the file using the given encoding.
  129. """
  130. logger.info("Writing to %s" % self)
  131. with codecs.open(self.path, 'w', encoding) as fout:
  132. fout.write(text)
  133. def copy_to(self, destination):
  134. """
  135. Copies the file to the given destination. Returns a File
  136. object that represents the target file. `destination` must
  137. be a File or Folder object.
  138. """
  139. target = self.__get_destination__(destination)
  140. logger.info("Copying %s to %s" % (self, target))
  141. shutil.copy(self.path, str(destination))
  142. return target
  143. class FSVisitor(object):
  144. """
  145. Implements syntactic sugar for walking and listing folders
  146. """
  147. def __init__(self, folder, pattern=None):
  148. super(FSVisitor, self).__init__()
  149. self.folder = folder
  150. self.pattern = pattern
  151. def folder_visitor(self, f):
  152. """
  153. Decorator for `visit_folder` protocol
  154. """
  155. self.visit_folder = f
  156. return f
  157. def file_visitor(self, f):
  158. """
  159. Decorator for `visit_file` protocol
  160. """
  161. self.visit_file = f
  162. return f
  163. def finalizer(self, f):
  164. """
  165. Decorator for `visit_complete` protocol
  166. """
  167. self.visit_complete = f
  168. return f
  169. def __enter__(self):
  170. return self
  171. def __exit__(self, exc_type, exc_val, exc_tb): pass
  172. class FolderWalker(FSVisitor):
  173. """
  174. Walks the entire hirearchy of this directory starting with itself.
  175. Calls self.visit_folder first and then calls self.visit_file for
  176. any files found. After all files and folders have been exhausted
  177. self.visit_complete is called.
  178. If a pattern is provided, only the files that match the pattern are
  179. processed.
  180. If visitor.visit_folder returns False, the files in the folder are not
  181. processed.
  182. """
  183. def __exit__(self, exc_type, exc_val, exc_tb):
  184. """
  185. Automatically walk the folder when the context manager is exited.
  186. """
  187. def __visit_folder__(folder):
  188. process_folder = True
  189. if hasattr(self,'visit_folder'):
  190. process_folder = self.visit_folder(folder)
  191. # If there is no return value assume true
  192. #
  193. if process_folder is None:
  194. process_folder = True
  195. return process_folder
  196. def __visit_file__(a_file):
  197. if hasattr(self,'visit_file'):
  198. self.visit_file(a_file)
  199. def __visit_complete__():
  200. if hasattr(self,'visit_complete'):
  201. self.visit_complete()
  202. for root, dirs, a_files in os.walk(self.folder.path):
  203. folder = Folder(root)
  204. if not __visit_folder__(folder):
  205. dirs[:] = []
  206. continue
  207. for a_file in a_files:
  208. if not self.pattern or fnmatch.fnmatch(a_file, self.pattern):
  209. __visit_file__(File(folder.child(a_file)))
  210. __visit_complete__()
  211. class FolderLister(FSVisitor):
  212. """
  213. Lists the contents of this directory starting with itself.
  214. Calls self.visit_folder first and then calls self.visit_file for
  215. any files found. After all files and folders have been exhausted
  216. self.visit_complete is called.
  217. If a pattern is provided, only the files that match the pattern are
  218. processed.
  219. """
  220. def __exit__(self, exc_type, exc_val, exc_tb):
  221. """
  222. Automatically list the folder contents when the context manager is exited.
  223. """
  224. a_files = os.listdir(self.folder.path)
  225. for a_file in a_files:
  226. path = self.folder.child(a_file)
  227. if os.path.isdir(path) and hasattr(self, 'visit_folder'):
  228. self.visit_folder(Folder(path))
  229. elif hasattr(self, 'visit_file'):
  230. if not self.pattern or fnmatch.fnmatch(a_file, self.pattern):
  231. self.visit_file(File(path))
  232. if hasattr(self,'visit_complete'):
  233. self.visit_complete()
  234. class Folder(FS):
  235. """
  236. Represents a directory.
  237. """
  238. def __init__(self, path):
  239. super(Folder, self).__init__(path)
  240. def child_folder(self, fragment):
  241. """
  242. Returns a folder object by combining the fragment to this folder's path
  243. """
  244. return Folder(os.path.join(self.path, fragment))
  245. def child(self, name):
  246. """
  247. Returns a path of a child item represented by `name`.
  248. """
  249. return os.path.join(self.path, name)
  250. def make(self):
  251. """
  252. Creates this directory and any of the missing directories in the path.
  253. Any errors that may occur are eaten.
  254. """
  255. try:
  256. if not self.exists:
  257. logger.info("Creating %s" % self.path)
  258. os.makedirs(self.path)
  259. except os.error:
  260. pass
  261. return self
  262. def delete(self):
  263. """
  264. Deletes the directory if it exists.
  265. """
  266. if self.exists:
  267. logger.info("Deleting %s" % self.path)
  268. shutil.rmtree(self.path)
  269. def copy_to(self, destination):
  270. """
  271. Copies this directory to the given destination. Returns a Folder object
  272. that represents the moved directory.
  273. """
  274. target = self.__get_destination__(destination)
  275. logger.info("Copying %s to %s" % (self, target))
  276. shutil.copytree(self.path, str(target))
  277. return target
  278. def _create_target_tree(self, target):
  279. """
  280. There is a bug in dir_util that makes `copy_tree` crash if a folder in
  281. the tree has been deleted before and readded now. To workaround the
  282. bug, we first walk the tree and create directories that are needed.
  283. """
  284. with self.walk() as walker:
  285. @walker.folder_visitor
  286. def visit_folder(folder):
  287. """
  288. Create the mirror directory
  289. """
  290. Folder(folder.get_mirror(target)).make()
  291. def copy_contents_to(self, destination):
  292. """
  293. Copies the contents of this directory to the given destination.
  294. Returns a Folder object that represents the moved directory.
  295. """
  296. logger.info("Copying contents of %s to %s" % (self, destination))
  297. self._create_target_tree(Folder(destination))
  298. dir_util.copy_tree(self.path, str(destination))
  299. return Folder(destination)
  300. def walk(self, pattern=None):
  301. """
  302. Walks this folder using `FolderWalker`
  303. """
  304. return FolderWalker(self, pattern)
  305. def list(self, pattern=None):
  306. """
  307. Lists this folder using `FolderLister`
  308. """
  309. return FolderLister(self, pattern)