A fork of hyde, the static site generation. Some patches will be pushed upstream.
 
 
 

565 lines
16 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 logging
  10. from logging import NullHandler
  11. import mimetypes
  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 = os.path.expandvars(os.path.expanduser(
  27. str(path).strip().rstrip(os.sep)))
  28. def __str__(self):
  29. return self.path
  30. def __repr__(self):
  31. return self.path
  32. def __eq__(self, other):
  33. return str(self) == str(other)
  34. def __ne__(self, other):
  35. return str(self) != str(other)
  36. @property
  37. def fully_expanded_path(self):
  38. """
  39. Returns the absolutely absolute path. Calls os.(
  40. normpath, normcase, expandvars and expanduser).
  41. """
  42. return os.path.abspath(
  43. os.path.normpath(
  44. os.path.normcase(
  45. os.path.expandvars(
  46. os.path.expanduser(self.path)))))
  47. @property
  48. def exists(self):
  49. """
  50. Does the file system object exist?
  51. """
  52. return os.path.exists(self.path)
  53. @property
  54. def name(self):
  55. """
  56. Returns the name of the FS object with its extension
  57. """
  58. return os.path.basename(self.path)
  59. @property
  60. def parent(self):
  61. """
  62. The parent folder. Returns a `Folder` object.
  63. """
  64. return Folder(os.path.dirname(self.path))
  65. @property
  66. def depth(self):
  67. """
  68. Returns the number of ancestors of this directory.
  69. """
  70. return len(self.path.rstrip(os.sep).split(os.sep))
  71. def ancestors(self, stop=None):
  72. """
  73. Generates the parents until stop or the absolute
  74. root directory is reached.
  75. """
  76. f = self
  77. while f.parent != stop:
  78. if f.parent == f:
  79. return
  80. yield f.parent
  81. f = f.parent
  82. def is_descendant_of(self, ancestor):
  83. """
  84. Checks if this folder is inside the given ancestor.
  85. """
  86. stop = Folder(ancestor)
  87. for folder in self.ancestors():
  88. if folder == stop:
  89. return True
  90. if stop.depth > folder.depth:
  91. return False
  92. return False
  93. def get_relative_path(self, root):
  94. """
  95. Gets the fragment of the current path starting at root.
  96. """
  97. if self == root:
  98. return ''
  99. return functools.reduce(lambda f, p: Folder(p.name).child(f),
  100. self.ancestors(stop=root),
  101. self.name)
  102. def get_mirror(self, target_root, source_root=None):
  103. """
  104. Returns a File or Folder object that reperesents if the entire
  105. fragment of this directory starting with `source_root` were copied
  106. to `target_root`.
  107. >>> Folder('/usr/local/hyde/stuff').get_mirror('/usr/tmp',
  108. source_root='/usr/local/hyde')
  109. Folder('/usr/tmp/stuff')
  110. """
  111. fragment = self.get_relative_path(
  112. source_root if source_root else self.parent)
  113. return Folder(target_root).child(fragment)
  114. @staticmethod
  115. def file_or_folder(path):
  116. """
  117. Returns a File or Folder object that would represent the given path.
  118. """
  119. target = str(path)
  120. return Folder(target) if os.path.isdir(target) else File(target)
  121. def __get_destination__(self, destination):
  122. """
  123. Returns a File or Folder object that would represent this entity
  124. if it were copied or moved to `destination`.
  125. """
  126. if isinstance(destination, File) or os.path.isfile(str(destination)):
  127. return destination
  128. else:
  129. return FS.file_or_folder(Folder(destination).child(self.name))
  130. class File(FS):
  131. """
  132. The File object.
  133. """
  134. def __init__(self, path):
  135. super(File, self).__init__(path)
  136. @property
  137. def name_without_extension(self):
  138. """
  139. Returns the name of the FS object without its extension
  140. """
  141. return os.path.splitext(self.name)[0]
  142. @property
  143. def extension(self):
  144. """
  145. File extension prefixed with a dot.
  146. """
  147. return os.path.splitext(self.path)[1]
  148. @property
  149. def kind(self):
  150. """
  151. File extension without dot prefix.
  152. """
  153. return self.extension.lstrip(".")
  154. @property
  155. def mimetype(self):
  156. (mime, encoding) = mimetypes.guess_type(self.path)
  157. return mime
  158. @property
  159. def is_binary(self):
  160. """Return true if this is a binary file."""
  161. with open(self.path, 'rb') as fin:
  162. CHUNKSIZE = 1024
  163. while 1:
  164. chunk = fin.read(CHUNKSIZE)
  165. if '\0' in chunk:
  166. return True
  167. if len(chunk) < CHUNKSIZE:
  168. break
  169. return False
  170. @staticmethod
  171. def make_temp(text):
  172. """
  173. Creates a temprorary file and writes the `text` into it
  174. """
  175. import tempfile
  176. (handle, path) = tempfile.mkstemp(text=True)
  177. os.close(handle)
  178. f = File(path)
  179. f.write(text)
  180. return f
  181. @property
  182. def is_text(self):
  183. """Return true if this is a text file."""
  184. return (not self.is_binary)
  185. @property
  186. def is_image(self):
  187. """Return true if this is an image file."""
  188. return self.mimetype.split("/")[0] == "image"
  189. def read_all(self, encoding='utf-8'):
  190. """
  191. Reads from the file and returns the content as a string.
  192. """
  193. logger.info("Reading everything from %s" % self)
  194. with codecs.open(self.path, 'r', encoding) as fin:
  195. read_text = fin.read()
  196. return read_text
  197. def write(self, text, encoding="utf-8"):
  198. """
  199. Writes the given text to the file using the given encoding.
  200. """
  201. logger.info("Writing to %s" % self)
  202. with codecs.open(self.path, 'w', encoding) as fout:
  203. fout.write(text)
  204. def copy_to(self, destination):
  205. """
  206. Copies the file to the given destination. Returns a File
  207. object that represents the target file. `destination` must
  208. be a File or Folder object.
  209. """
  210. target = self.__get_destination__(destination)
  211. logger.info("Copying %s to %s" % (self, target))
  212. shutil.copy(self.path, str(destination))
  213. return target
  214. def delete(self):
  215. """
  216. Delete the file if it exists.
  217. """
  218. if self.exists:
  219. os.remove(self.path)
  220. class FSVisitor(object):
  221. """
  222. Implements syntactic sugar for walking and listing folders
  223. """
  224. def __init__(self, folder, pattern=None):
  225. super(FSVisitor, self).__init__()
  226. self.folder = folder
  227. self.pattern = pattern
  228. def folder_visitor(self, f):
  229. """
  230. Decorator for `visit_folder` protocol
  231. """
  232. self.visit_folder = f
  233. return f
  234. def file_visitor(self, f):
  235. """
  236. Decorator for `visit_file` protocol
  237. """
  238. self.visit_file = f
  239. return f
  240. def finalizer(self, f):
  241. """
  242. Decorator for `visit_complete` protocol
  243. """
  244. self.visit_complete = f
  245. return f
  246. def __enter__(self):
  247. return self
  248. def __exit__(self, exc_type, exc_val, exc_tb):
  249. pass
  250. class FolderWalker(FSVisitor):
  251. """
  252. Walks the entire hirearchy of this directory starting with itself.
  253. If a pattern is provided, only the files that match the pattern are
  254. processed.
  255. """
  256. def walk(self, walk_folders=False, walk_files=False):
  257. """
  258. A simple generator that yields a File or Folder object based on
  259. the arguments.
  260. """
  261. if not walk_files and not walk_folders:
  262. return
  263. for root, dirs, a_files in os.walk(self.folder.path, followlinks=True):
  264. folder = Folder(root)
  265. if walk_folders:
  266. yield folder
  267. if walk_files:
  268. for a_file in a_files:
  269. if (not self.pattern or
  270. fnmatch.fnmatch(a_file, self.pattern)):
  271. yield File(folder.child(a_file))
  272. def walk_all(self):
  273. """
  274. Yield both Files and Folders as the tree is walked.
  275. """
  276. return self.walk(walk_folders=True, walk_files=True)
  277. def walk_files(self):
  278. """
  279. Yield only Files.
  280. """
  281. return self.walk(walk_folders=False, walk_files=True)
  282. def walk_folders(self):
  283. """
  284. Yield only Folders.
  285. """
  286. return self.walk(walk_folders=True, walk_files=False)
  287. def __exit__(self, exc_type, exc_val, exc_tb):
  288. """
  289. Automatically walk the folder when the context manager is exited.
  290. Calls self.visit_folder first and then calls self.visit_file for
  291. any files found. After all files and folders have been exhausted
  292. self.visit_complete is called.
  293. If visitor.visit_folder returns False, the files in the folder are not
  294. processed.
  295. """
  296. def __visit_folder__(folder):
  297. process_folder = True
  298. if hasattr(self, 'visit_folder'):
  299. process_folder = self.visit_folder(folder)
  300. # If there is no return value assume true
  301. #
  302. if process_folder is None:
  303. process_folder = True
  304. return process_folder
  305. def __visit_file__(a_file):
  306. if hasattr(self, 'visit_file'):
  307. self.visit_file(a_file)
  308. def __visit_complete__():
  309. if hasattr(self, 'visit_complete'):
  310. self.visit_complete()
  311. for root, dirs, a_files in os.walk(self.folder.path):
  312. folder = Folder(root)
  313. if not __visit_folder__(folder):
  314. dirs[:] = []
  315. continue
  316. for a_file in a_files:
  317. if not self.pattern or fnmatch.fnmatch(a_file, self.pattern):
  318. __visit_file__(File(folder.child(a_file)))
  319. __visit_complete__()
  320. class FolderLister(FSVisitor):
  321. """
  322. Lists the contents of this directory.
  323. If a pattern is provided, only the files that match the pattern are
  324. processed.
  325. """
  326. def list(self, list_folders=False, list_files=False):
  327. """
  328. A simple generator that yields a File or Folder object based on
  329. the arguments.
  330. """
  331. a_files = os.listdir(self.folder.path)
  332. for a_file in a_files:
  333. path = self.folder.child(a_file)
  334. if os.path.isdir(path):
  335. if list_folders:
  336. yield Folder(path)
  337. elif list_files:
  338. if not self.pattern or fnmatch.fnmatch(a_file, self.pattern):
  339. yield File(path)
  340. def list_all(self):
  341. """
  342. Yield both Files and Folders as the folder is listed.
  343. """
  344. return self.list(list_folders=True, list_files=True)
  345. def list_files(self):
  346. """
  347. Yield only Files.
  348. """
  349. return self.list(list_folders=False, list_files=True)
  350. def list_folders(self):
  351. """
  352. Yield only Folders.
  353. """
  354. return self.list(list_folders=True, list_files=False)
  355. def __exit__(self, exc_type, exc_val, exc_tb):
  356. """
  357. Automatically list the folder contents when the context manager
  358. is exited.
  359. Calls self.visit_folder first and then calls self.visit_file for
  360. any files found. After all files and folders have been exhausted
  361. self.visit_complete is called.
  362. """
  363. a_files = os.listdir(self.folder.path)
  364. for a_file in a_files:
  365. path = self.folder.child(a_file)
  366. if os.path.isdir(path) and hasattr(self, 'visit_folder'):
  367. self.visit_folder(Folder(path))
  368. elif hasattr(self, 'visit_file'):
  369. if not self.pattern or fnmatch.fnmatch(a_file, self.pattern):
  370. self.visit_file(File(path))
  371. if hasattr(self, 'visit_complete'):
  372. self.visit_complete()
  373. class Folder(FS):
  374. """
  375. Represents a directory.
  376. """
  377. def __init__(self, path):
  378. super(Folder, self).__init__(path)
  379. def child_folder(self, fragment):
  380. """
  381. Returns a folder object by combining the fragment to this folder's path
  382. """
  383. return Folder(os.path.join(self.path, Folder(fragment).path))
  384. def child(self, fragment):
  385. """
  386. Returns a path of a child item represented by `fragment`.
  387. """
  388. return os.path.join(self.path, FS(fragment).path)
  389. def make(self):
  390. """
  391. Creates this directory and any of the missing directories in the path.
  392. Any errors that may occur are eaten.
  393. """
  394. try:
  395. if not self.exists:
  396. logger.info("Creating %s" % self.path)
  397. os.makedirs(self.path)
  398. except os.error:
  399. pass
  400. return self
  401. def delete(self):
  402. """
  403. Deletes the directory if it exists.
  404. """
  405. if self.exists:
  406. logger.info("Deleting %s" % self.path)
  407. shutil.rmtree(self.path)
  408. def copy_to(self, destination):
  409. """
  410. Copies this directory to the given destination. Returns a Folder object
  411. that represents the moved directory.
  412. """
  413. target = self.__get_destination__(destination)
  414. logger.info("Copying %s to %s" % (self, target))
  415. shutil.copytree(self.path, str(target))
  416. return target
  417. def move_to(self, destination):
  418. """
  419. Moves this directory to the given destination. Returns a Folder object
  420. that represents the moved directory.
  421. """
  422. target = self.__get_destination__(destination)
  423. logger.info("Move %s to %s" % (self, target))
  424. shutil.move(self.path, str(target))
  425. return target
  426. def rename_to(self, destination_name):
  427. """
  428. Moves this directory to the given destination. Returns a Folder object
  429. that represents the moved directory.
  430. """
  431. target = self.parent.child_folder(destination_name)
  432. logger.info("Rename %s to %s" % (self, target))
  433. shutil.move(self.path, str(target))
  434. return target
  435. def _create_target_tree(self, target):
  436. """
  437. There is a bug in dir_util that makes `copy_tree` crash if a folder in
  438. the tree has been deleted before and readded now. To workaround the
  439. bug, we first walk the tree and create directories that are needed.
  440. """
  441. source = self
  442. with source.walker as walker:
  443. @walker.folder_visitor
  444. def visit_folder(folder):
  445. """
  446. Create the mirror directory
  447. """
  448. if folder != source:
  449. Folder(folder.get_mirror(target, source)).make()
  450. def copy_contents_to(self, destination):
  451. """
  452. Copies the contents of this directory to the given destination.
  453. Returns a Folder object that represents the moved directory.
  454. """
  455. logger.info("Copying contents of %s to %s" % (self, destination))
  456. target = Folder(destination)
  457. target.make()
  458. self._create_target_tree(target)
  459. dir_util.copy_tree(self.path, str(target))
  460. return target
  461. @property
  462. def walker(self, pattern=None):
  463. """
  464. Return a `FolderWalker` object
  465. """
  466. return FolderWalker(self, pattern)
  467. @property
  468. def lister(self, pattern=None):
  469. """
  470. Return a `FolderLister` object
  471. """
  472. return FolderLister(self, pattern)