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.
 
 
 

678 lines
19 KiB

  1. """
  2. Unified interface for performing file system tasks. Uses os, os.path. shutil
  3. and distutil to perform the tasks. The behavior of some functions is slightly
  4. contaminated with requirements from Hyde: For example, the backup function
  5. deletes the directory that is being backed up.
  6. """
  7. import os
  8. import shutil
  9. import codecs
  10. import fnmatch
  11. from datetime import datetime
  12. # pylint: disable-msg=E0611
  13. from distutils import dir_util, file_util
  14. @staticmethod
  15. def filter_hidden_inplace(item_list):
  16. """
  17. Given a list of filenames, removes filenames for invisible files (whose
  18. names begin with dots) or files whose names end in tildes '~'.
  19. Does not remove files with the filname '.htaccess'.
  20. The list is modified in-place; this function has no return value.
  21. """
  22. if not item_list:
  23. return
  24. wanted = filter(
  25. lambda item:
  26. not ((item.startswith('.') and item != ".htaccess")
  27. or item.endswith('~')),
  28. item_list)
  29. count = len(item_list)
  30. good_item_count = len(wanted)
  31. if count == good_item_count:
  32. return
  33. item_list[:good_item_count] = wanted
  34. for _ in range(good_item_count, count):
  35. item_list.pop()
  36. @staticmethod
  37. def get_path_fragment(root_dir, a_dir):
  38. """
  39. Gets the path fragment starting at root_dir to a_dir
  40. """
  41. current_dir = a_dir
  42. current_fragment = ''
  43. while not current_dir == root_dir:
  44. (current_dir, current_fragment_part) = os.path.split(current_dir)
  45. current_fragment = os.path.join(
  46. current_fragment_part, current_fragment)
  47. return current_fragment
  48. @staticmethod
  49. def get_mirror_dir(directory, source_root, mirror_root, ignore_root = False):
  50. """
  51. Returns the mirror directory from source_root to mirror_root
  52. """
  53. current_fragment = get_path_fragment(source_root, directory)
  54. if not current_fragment:
  55. return mirror_root
  56. mirror_directory = mirror_root
  57. if not ignore_root:
  58. mirror_directory = os.path.join(
  59. mirror_root,
  60. os.path.basename(source_root))
  61. mirror_directory = os.path.join(
  62. mirror_directory, current_fragment)
  63. return mirror_directory
  64. @staticmethod
  65. def mirror_dir_tree(directory, source_root, mirror_root, ignore_root = False):
  66. """
  67. Create the mirror directory tree
  68. """
  69. mirror_directory = get_mirror_dir(
  70. directory, source_root,
  71. mirror_root, ignore_root)
  72. try:
  73. os.makedirs(mirror_directory)
  74. except os.error:
  75. pass
  76. return mirror_directory
  77. class FileSystemEntity(object):
  78. """
  79. Base class for files and folders.
  80. """
  81. def __init__(self, path):
  82. super(FileSystemEntity, self).__init__()
  83. if path is FileSystemEntity:
  84. self.path = path.path
  85. else:
  86. self.path = path
  87. def __str__(self):
  88. return self.path
  89. def __repr__(self):
  90. return self.path
  91. def allow(self, include=None, exclude=None):
  92. """
  93. Given a set of wilcard patterns in the include and exclude arguments,
  94. tests if the patterns allow this item for processing.
  95. The exclude parameter is processed first as a broader filter and then
  96. include is used as a narrower filter to override the results for more
  97. specific files.
  98. Example:
  99. exclude = (".*", "*~")
  100. include = (".htaccess")
  101. """
  102. if not include:
  103. include = ()
  104. if not exclude:
  105. exclude = ()
  106. if reduce(lambda result,
  107. pattern: result or
  108. fnmatch.fnmatch(self.name, pattern), include, False):
  109. return True
  110. if reduce(lambda result, pattern:
  111. result and not fnmatch.fnmatch(self.name, pattern),
  112. exclude, True):
  113. return True
  114. return False
  115. @property
  116. def humblepath(self):
  117. """
  118. Expands variables, user, normalizes path and case and coverts
  119. to absolute.
  120. """
  121. return os.path.abspath(
  122. os.path.normpath(
  123. os.path.normcase(
  124. os.path.expandvars(
  125. os.path.expanduser(self.path)))))
  126. def same_as(self, other):
  127. """
  128. Checks if the path of this object is same as `other`. `other` must
  129. be a FileSystemEntity.
  130. """
  131. return (self.humblepath.rstrip(os.sep) ==
  132. other.humblepath.rstrip(os.sep))
  133. @property
  134. def exists(self):
  135. """
  136. Checks if the entity exists in the file system.
  137. """
  138. return os.path.exists(self.path)
  139. @property
  140. def isdir(self):
  141. """
  142. Is this a folder.
  143. """
  144. return os.path.isdir(self.path)
  145. @property
  146. def stats(self):
  147. """
  148. Shortcut for os.stat.
  149. """
  150. return os.stat(self.path)
  151. @property
  152. def name(self):
  153. """
  154. Name of the entity. Calls os.path.basename.
  155. """
  156. return os.path.basename(self.path)
  157. @property
  158. def parent(self):
  159. """
  160. The parent folder. Returns a `Folder` object.
  161. """
  162. return Folder(os.path.dirname(self.path))
  163. def __get_destination__(self, destination):
  164. """
  165. Returns a File or Folder object that would represent this entity
  166. if it were copied or moved to `destination`. `destination` must be
  167. an instance of File or Folder.
  168. """
  169. if os.path.isdir(str(destination)):
  170. target = destination.child(self.name)
  171. if os.path.isdir(self.path):
  172. return Folder(target)
  173. else: return File(target)
  174. else:
  175. return destination
  176. # pylint: disable-msg=R0904,W0142
  177. class File(FileSystemEntity):
  178. """
  179. Encapsulates commonly used functions related to files.
  180. """
  181. def __init__(self, path):
  182. super(File, self).__init__(path)
  183. @property
  184. def size(self):
  185. """
  186. Gets the file size
  187. """
  188. return os.path.getsize(self.path)
  189. #return 1
  190. def has_extension(self, extension):
  191. """
  192. Checks if this file has the given extension.
  193. """
  194. return self.extension == extension
  195. def delete(self):
  196. """
  197. Deletes if the file exists.
  198. """
  199. if self.exists:
  200. os.remove(self.path)
  201. @property
  202. def last_modified(self):
  203. """
  204. Returns a datetime object representing the last modified time.
  205. Calls os.path.getmtime.
  206. """
  207. return datetime.fromtimestamp(os.path.getmtime(self.path))
  208. def changed_since(self, basetime):
  209. """
  210. Returns True if the file has been changed since the given time.
  211. """
  212. return self.last_modified > basetime
  213. def older_than(self, another_file):
  214. """
  215. Checks if this file is older than the given file. Uses last_modified to
  216. determine age.
  217. """
  218. return another_file.last_modified > self.last_modified
  219. @property
  220. def path_without_extension(self):
  221. """
  222. The full path of the file without its extension.
  223. """
  224. return os.path.splitext(self.path)[0]
  225. @property
  226. def name_without_extension(self):
  227. """
  228. Name of the file without its extension.
  229. """
  230. return os.path.splitext(self.name)[0]
  231. @property
  232. def extension(self):
  233. """
  234. File's extension prefixed with a dot.
  235. """
  236. return os.path.splitext(self.path)[1]
  237. @property
  238. def kind(self):
  239. """
  240. File's extension without a dot prefix.
  241. """
  242. return self.extension.lstrip(".")
  243. def move_to(self, destination):
  244. """
  245. Moves the file to the given destination. Returns a File
  246. object that represents the target file. `destination` must
  247. be a File or Folder object.
  248. """
  249. shutil.move(self.path, str(destination))
  250. return self.__get_destination__(destination)
  251. def copy_to(self, destination):
  252. """
  253. Copies the file to the given destination. Returns a File
  254. object that represents the target file. `destination` must
  255. be a File or Folder object.
  256. """
  257. shutil.copy(self.path, str(destination))
  258. return self.__get_destination__(destination)
  259. def write(self, text, encoding="utf-8"):
  260. """
  261. Writes the given text to the file using the given encoding.
  262. """
  263. fout = codecs.open(self.path, 'w', encoding)
  264. fout.write(text)
  265. fout.close()
  266. def read_all(self):
  267. """
  268. Reads from the file and returns the content as a string.
  269. """
  270. fin = codecs.open(self.path, 'r')
  271. read_text = fin.read()
  272. fin.close()
  273. return read_text
  274. # pylint: disable-msg=R0904,W0142
  275. class Folder(FileSystemEntity):
  276. """
  277. Encapsulates commonly used directory functions.
  278. """
  279. def __init__(self, path):
  280. super(Folder, self).__init__(path)
  281. def __str__(self):
  282. return self.path
  283. def __repr__(self):
  284. return self.path
  285. def delete(self):
  286. """
  287. Deletes the directory if it exists.
  288. """
  289. if self.exists:
  290. shutil.rmtree(self.path)
  291. def depth(self):
  292. """
  293. Returns the number of ancestors of this directory.
  294. """
  295. return len(self.path.split(os.sep))
  296. def make(self):
  297. """
  298. Creates this directory and any of the missing directories in the path.
  299. Any errors that may occur are eaten.
  300. """
  301. try:
  302. if not self.exists:
  303. os.makedirs(self.path)
  304. except os.error:
  305. pass
  306. return self
  307. def is_parent_of(self, other_entity):
  308. """
  309. Returns True if this directory is a direct parent of the the given
  310. directory.
  311. """
  312. return self.same_as(other_entity.parent)
  313. def is_ancestor_of(self, other_entity):
  314. """
  315. Returns True if this directory is in the path of the given directory.
  316. Note that this will return True if the given directory is same as this.
  317. """
  318. folder = other_entity
  319. while not folder.parent.same_as(folder):
  320. folder = folder.parent
  321. if self.same_as(folder):
  322. return True
  323. return False
  324. def child(self, name):
  325. """
  326. Returns a path of a child item represented by `name`.
  327. """
  328. return os.path.join(self.path, name)
  329. def child_folder(self, *args):
  330. """
  331. Returns a Folder object by joining the path component in args
  332. to this directory's path.
  333. """
  334. return Folder(os.path.join(self.path, *args))
  335. def child_folder_with_fragment(self, fragment):
  336. """
  337. Returns a Folder object by joining the fragment to
  338. this directory's path.
  339. """
  340. return Folder(os.path.join(self.path, fragment.lstrip(os.sep)))
  341. def get_fragment(self, root):
  342. """
  343. Returns the path fragment of this directory starting with the given
  344. directory.
  345. """
  346. return get_path_fragment(str(root), self.path)
  347. def get_mirror_folder(self, root, mirror_root, ignore_root=False):
  348. """
  349. Returns a Folder object that reperesents if the entire fragment of this
  350. directory starting with `root` were copied to `mirror_root`. If ignore_root
  351. is True, the mirror does not include `root` directory itself.
  352. Example:
  353. Current Directory: /usr/local/hyde/stuff
  354. root: /usr/local/hyde
  355. mirror_root: /usr/tmp
  356. Result:
  357. if ignore_root == False:
  358. Folder(/usr/tmp/hyde/stuff)
  359. if ignore_root == True:
  360. Folder(/usr/tmp/stuff)
  361. """
  362. path = get_mirror_dir(self.path,
  363. str(root), str(mirror_root), ignore_root)
  364. return Folder(path)
  365. def create_mirror_folder(self, root, mirror_root, ignore_root=False):
  366. """
  367. Creates the mirror directory returned by `get_mirror_folder`
  368. """
  369. mirror_folder = self.get_mirror_folder(
  370. root, mirror_root, ignore_root)
  371. mirror_folder.make()
  372. return mirror_folder
  373. def backup(self, destination):
  374. """
  375. Creates a backup of this directory in the given destination. The backup is
  376. suffixed with a number for uniqueness. Deletes this directory after backup
  377. is complete.
  378. """
  379. new_name = self.name
  380. count = 0
  381. dest = Folder(destination.child(new_name))
  382. while(True):
  383. dest = Folder(destination.child(new_name))
  384. if not dest.exists:
  385. break
  386. else:
  387. count = count + 1
  388. new_name = self.name + str(count)
  389. dest.make()
  390. dest.move_contents_of(self)
  391. self.delete()
  392. return dest
  393. def move_to(self, destination):
  394. """
  395. Moves this directory to the given destination. Returns a Folder object
  396. that represents the moved directory.
  397. """
  398. shutil.copytree(self.path, str(destination))
  399. shutil.rmtree(self.path)
  400. return self.__get_destination__(destination)
  401. def copy_to(self, destination):
  402. """
  403. Copies this directory to the given destination. Returns a Folder object
  404. that represents the moved directory.
  405. """
  406. shutil.copytree(self.path, str(destination))
  407. return self.__get_destination__(destination)
  408. def move_folder_from(self, source, incremental=False):
  409. """
  410. Moves the given source directory to this directory. If incremental is True
  411. only newer objects are overwritten.
  412. """
  413. self.copy_folder_from(source, incremental)
  414. shutil.rmtree(str(source))
  415. def copy_folder_from(self, source, incremental=False):
  416. """
  417. Copies the given source directory to this directory. If incremental is True
  418. only newer objects are overwritten.
  419. """
  420. # There is a bug in dir_util that makes copy_tree crash if a folder in
  421. # the tree has been deleted before and readded now. To workaround the
  422. # bug, we first walk the tree and create directories that are needed.
  423. #
  424. # pylint: disable-msg=C0111,W0232
  425. target_root = self
  426. # pylint: disable-msg=R0903
  427. class _DirCreator:
  428. @staticmethod
  429. def visit_folder(folder):
  430. target = folder.get_mirror_folder(
  431. source.parent, target_root, ignore_root=True)
  432. target.make()
  433. source.walk(_DirCreator)
  434. dir_util.copy_tree(str(source),
  435. self.child(source.name),
  436. update=incremental)
  437. def move_contents_of(self, source, move_empty_folders=True,
  438. incremental=False):
  439. """
  440. Moves the contents of the given source directory to this directory. If
  441. incremental is True only newer objects are overwritten.
  442. """
  443. # pylint: disable-msg=C0111,W0232
  444. class _Mover:
  445. @staticmethod
  446. def visit_folder(folder):
  447. self.move_folder_from(folder, incremental)
  448. @staticmethod
  449. def visit_file(a_file):
  450. self.move_file_from(a_file, incremental)
  451. source.list(_Mover, move_empty_folders)
  452. def copy_contents_of(self, source, copy_empty_folders=True,
  453. incremental=False):
  454. """
  455. Copies the contents of the given source directory to this directory. If
  456. incremental is True only newer objects are overwritten.
  457. """
  458. # pylint: disable-msg=C0111,W0232
  459. class _Copier:
  460. @staticmethod
  461. def visit_folder(folder):
  462. self.copy_folder_from(folder, incremental)
  463. @staticmethod
  464. def visit_file(a_file):
  465. self.copy_file_from(a_file, incremental)
  466. source.list(_Copier, copy_empty_folders)
  467. def move_file_from(self, source, incremental=False):
  468. """
  469. Moves the given source file to this directory. If incremental is True the
  470. move is performed only if the source file is newer.
  471. """
  472. self.copy_file_from(source, incremental)
  473. source.delete()
  474. def copy_file_from(self, source, incremental=False):
  475. """
  476. Copies the given source file to this directory. If incremental is True the
  477. move is performed only if the source file is newer.
  478. """
  479. file_util.copy_file(str(source), self.path, update=incremental)
  480. def list(self, visitor, list_empty_folders=True):
  481. """
  482. Calls the visitor.visit_file or visitor.visit_folder for each file or folder
  483. in this directory. If list_empty_folders is False folders that are empty are
  484. skipped.
  485. """
  486. a_files = os.listdir(self.path)
  487. for a_file in a_files:
  488. path = os.path.join(self.path, str(a_file))
  489. if os.path.isdir(path):
  490. if not list_empty_folders:
  491. if Folder(path).empty():
  492. continue
  493. visitor.visit_folder(Folder(path))
  494. else:
  495. visitor.visit_file(File(path))
  496. def empty(self):
  497. """
  498. Checks if this directory or any of its subdirectories contain files.
  499. """
  500. paths = os.listdir(self.path)
  501. for path in paths:
  502. if os.path.isdir(path):
  503. if not Folder(path).empty():
  504. return False
  505. else:
  506. return False
  507. return True
  508. def walk(self, visitor = None, pattern = None):
  509. """
  510. Walks the entire hirearchy of this directory starting with itself.
  511. Calls visitor.visit_folder first and then calls visitor.visit_file for
  512. any files found. After all files and folders have been exhausted
  513. visitor.visit_complete is called.
  514. If a pattern is provided, only the files that match the pattern are
  515. processed.
  516. If visitor.visit_folder returns False, the files in the folder are not
  517. processed.
  518. """
  519. def __visit_folder__(visitor, folder):
  520. process_folder = True
  521. if visitor and hasattr(visitor,'visit_folder'):
  522. process_folder = visitor.visit_folder(folder)
  523. # If there is no return value assume true
  524. #
  525. if process_folder is None:
  526. process_folder = True
  527. return process_folder
  528. def __visit_file__(visitor, a_file):
  529. if visitor and hasattr(visitor,'visit_file'):
  530. visitor.visit_file(a_file)
  531. def __visit_complete__(visitor):
  532. if visitor and hasattr(visitor,'visit_complete'):
  533. visitor.visit_complete()
  534. for root, dirs, a_files in os.walk(self.path):
  535. folder = Folder(root)
  536. if not __visit_folder__(visitor, folder):
  537. dirs[:] = []
  538. continue
  539. for a_file in a_files:
  540. if not pattern or fnmatch.fnmatch(a_file, pattern):
  541. __visit_file__(visitor, File(folder.child(a_file)))
  542. __visit_complete__(visitor)