/hyde/site.py

http://github.com/hyde/hyde · Python · 489 lines · 352 code · 27 blank · 110 comment · 28 complexity · ac9062f39f545855a3c6ed4bbb37de86 MD5 · raw file

  1. # -*- coding: utf-8 -*-
  2. """
  3. Parses & holds information about the site to be generated.
  4. """
  5. import os
  6. import fnmatch
  7. import sys
  8. from functools import wraps
  9. from hyde._compat import parse, quote, str
  10. from hyde.exceptions import HydeException
  11. from hyde.model import Config
  12. from commando.util import getLoggerWithNullHandler
  13. from fswrap import FS, File, Folder
  14. def path_normalized(f):
  15. @wraps(f)
  16. def wrapper(self, path):
  17. return f(self, str(path).replace('/', os.sep))
  18. return wrapper
  19. logger = getLoggerWithNullHandler('hyde.engine')
  20. class Processable(object):
  21. """
  22. A node or resource.
  23. """
  24. def __init__(self, source):
  25. super(Processable, self).__init__()
  26. self.source = FS.file_or_folder(source)
  27. self.is_processable = True
  28. self.uses_template = True
  29. self._relative_deploy_path = None
  30. @property
  31. def name(self):
  32. """
  33. The resource name
  34. """
  35. return self.source.name
  36. def __repr__(self):
  37. return self.path
  38. def __lt__(self, other):
  39. return self.source.path < other.source.path
  40. def __gt__(self, other):
  41. return self.source.path > other.source.path
  42. @property
  43. def path(self):
  44. """
  45. Gets the source path of this node.
  46. """
  47. return self.source.path
  48. def get_relative_deploy_path(self):
  49. """
  50. Gets the path where the file will be created
  51. after its been processed.
  52. """
  53. return self._relative_deploy_path \
  54. if self._relative_deploy_path is not None \
  55. else self.relative_path
  56. def set_relative_deploy_path(self, path):
  57. """
  58. Sets the path where the file ought to be created
  59. after its been processed.
  60. """
  61. self._relative_deploy_path = path
  62. self.site.content.deploy_path_changed(self)
  63. relative_deploy_path = property(get_relative_deploy_path,
  64. set_relative_deploy_path)
  65. @property
  66. def url(self):
  67. """
  68. Returns the relative url for the processable
  69. """
  70. return '/' + self.relative_deploy_path
  71. @property
  72. def full_url(self):
  73. """
  74. Returns the full url for the processable.
  75. """
  76. return self.site.full_url(self.relative_deploy_path)
  77. class Resource(Processable):
  78. """
  79. Represents any file that is processed by hyde
  80. """
  81. def __init__(self, source_file, node):
  82. super(Resource, self).__init__(source_file)
  83. self.source_file = source_file
  84. if not node:
  85. raise HydeException("Resource cannot exist without a node")
  86. if not source_file:
  87. raise HydeException("Source file is required"
  88. " to instantiate a resource")
  89. self.node = node
  90. self.site = node.site
  91. self.simple_copy = False
  92. @property
  93. def relative_path(self):
  94. """
  95. Gets the path relative to the root folder (Content)
  96. """
  97. return self.source_file.get_relative_path(self.node.root.source_folder)
  98. @property
  99. def slug(self):
  100. # TODO: Add a more sophisticated slugify method
  101. return self.source.name_without_extension
  102. class Node(Processable):
  103. """
  104. Represents any folder that is processed by hyde
  105. """
  106. def __init__(self, source_folder, parent=None):
  107. super(Node, self).__init__(source_folder)
  108. if not source_folder:
  109. raise HydeException("Source folder is required"
  110. " to instantiate a node.")
  111. self.root = self
  112. self.module = None
  113. self.site = None
  114. self.source_folder = Folder(str(source_folder))
  115. self.parent = parent
  116. if parent:
  117. self.root = self.parent.root
  118. self.module = self.parent.module if self.parent.module else self
  119. self.site = parent.site
  120. self.child_nodes = []
  121. self.resources = []
  122. def contains_resource(self, resource_name):
  123. """
  124. Returns True if the given resource name exists as a file
  125. in this node's source folder.
  126. """
  127. return File(self.source_folder.child(resource_name)).exists
  128. def get_resource(self, resource_name):
  129. """
  130. Gets the resource if the given resource name exists as a file
  131. in this node's source folder.
  132. """
  133. if self.contains_resource(resource_name):
  134. return self.root.resource_from_path(
  135. self.source_folder.child(resource_name))
  136. return None
  137. def add_child_node(self, folder):
  138. """
  139. Creates a new child node and adds it to the list of child nodes.
  140. """
  141. if folder.parent != self.source_folder:
  142. raise HydeException("The given folder [%s] is not a"
  143. " direct descendant of [%s]" %
  144. (folder, self.source_folder))
  145. node = Node(folder, self)
  146. self.child_nodes.append(node)
  147. return node
  148. def add_child_resource(self, afile):
  149. """
  150. Creates a new resource and adds it to the list of child resources.
  151. """
  152. if afile.parent != self.source_folder:
  153. raise HydeException("The given file [%s] is not"
  154. " a direct descendant of [%s]" %
  155. (afile, self.source_folder))
  156. resource = Resource(afile, self)
  157. self.resources.append(resource)
  158. return resource
  159. def walk(self):
  160. """
  161. Walks the node, first yielding itself then
  162. yielding the child nodes depth-first.
  163. """
  164. yield self
  165. for child in sorted([node for node in self.child_nodes]):
  166. for node in child.walk():
  167. yield node
  168. def rwalk(self):
  169. """
  170. Walk the node upward, first yielding itself then
  171. yielding its parents.
  172. """
  173. x = self
  174. while x:
  175. yield x
  176. x = x.parent
  177. def walk_resources(self):
  178. """
  179. Walks the resources in this hierarchy.
  180. """
  181. for node in self.walk():
  182. for resource in sorted([resource for resource in node.resources]):
  183. yield resource
  184. @property
  185. def relative_path(self):
  186. """
  187. Gets the path relative to the root folder (Content, Media, Layout)
  188. """
  189. return self.source_folder.get_relative_path(self.root.source_folder)
  190. class RootNode(Node):
  191. """
  192. Represents one of the roots of site: Content, Media or Layout
  193. """
  194. def __init__(self, source_folder, site):
  195. super(RootNode, self).__init__(source_folder)
  196. self.site = site
  197. self.node_map = {}
  198. self.node_deploy_map = {}
  199. self.resource_map = {}
  200. self.resource_deploy_map = {}
  201. @path_normalized
  202. def node_from_path(self, path):
  203. """
  204. Gets the node that maps to the given path.
  205. If no match is found it returns None.
  206. """
  207. if Folder(path) == self.source_folder:
  208. return self
  209. return self.node_map.get(str(Folder(path)), None)
  210. @path_normalized
  211. def node_from_relative_path(self, relative_path):
  212. """
  213. Gets the content node that maps to the given relative path.
  214. If no match is found it returns None.
  215. """
  216. return self.node_from_path(
  217. self.source_folder.child(str(relative_path)))
  218. @path_normalized
  219. def resource_from_path(self, path):
  220. """
  221. Gets the resource that maps to the given path.
  222. If no match is found it returns None.
  223. """
  224. return self.resource_map.get(str(File(path)), None)
  225. @path_normalized
  226. def resource_from_relative_path(self, relative_path):
  227. """
  228. Gets the content resource that maps to the given relative path.
  229. If no match is found it returns None.
  230. """
  231. return self.resource_from_path(
  232. self.source_folder.child(relative_path))
  233. def deploy_path_changed(self, item):
  234. """
  235. Handles the case where the relative deploy path of a
  236. resource has changed.
  237. """
  238. self.resource_deploy_map[str(item.relative_deploy_path)] = item
  239. @path_normalized
  240. def resource_from_relative_deploy_path(self, relative_deploy_path):
  241. """
  242. Gets the content resource whose deploy path maps to
  243. the given relative path. If no match is found it returns None.
  244. """
  245. if relative_deploy_path in self.resource_deploy_map:
  246. return self.resource_deploy_map[relative_deploy_path]
  247. return self.resource_from_relative_path(relative_deploy_path)
  248. def add_node(self, a_folder):
  249. """
  250. Adds a new node to this folder's hierarchy.
  251. Also adds it to the hashtable of path to node associations
  252. for quick lookup.
  253. """
  254. folder = Folder(a_folder)
  255. node = self.node_from_path(folder)
  256. if node:
  257. logger.debug("Node exists at [%s]" % node.relative_path)
  258. return node
  259. if not folder.is_descendant_of(self.source_folder):
  260. raise HydeException("The given folder [%s] does not"
  261. " belong to this hierarchy [%s]" %
  262. (folder, self.source_folder))
  263. p_folder = folder
  264. parent = None
  265. hierarchy = []
  266. while not parent:
  267. hierarchy.append(p_folder)
  268. p_folder = p_folder.parent
  269. parent = self.node_from_path(p_folder)
  270. hierarchy.reverse()
  271. node = parent if parent else self
  272. for h_folder in hierarchy:
  273. node = node.add_child_node(h_folder)
  274. self.node_map[str(h_folder)] = node
  275. logger.debug("Added node [%s] to [%s]" % (
  276. node.relative_path, self.source_folder))
  277. return node
  278. def add_resource(self, a_file):
  279. """
  280. Adds a file to the parent node. Also adds to to the
  281. hashtable of path to resource associations for quick lookup.
  282. """
  283. afile = File(a_file)
  284. resource = self.resource_from_path(afile)
  285. if resource:
  286. logger.debug("Resource exists at [%s]" % resource.relative_path)
  287. return resource
  288. if not afile.is_descendant_of(self.source_folder):
  289. raise HydeException("The given file [%s] does not reside"
  290. " in this hierarchy [%s]" %
  291. (afile, self.source_folder))
  292. node = self.node_from_path(afile.parent)
  293. if not node:
  294. node = self.add_node(afile.parent)
  295. resource = node.add_child_resource(afile)
  296. self.resource_map[str(afile)] = resource
  297. relative_path = resource.relative_path
  298. resource.simple_copy = any(fnmatch.fnmatch(relative_path, pattern)
  299. for pattern in self.site.config.simple_copy)
  300. logger.debug("Added resource [%s] to [%s]" %
  301. (resource.relative_path, self.source_folder))
  302. return resource
  303. def load(self):
  304. """
  305. Walks the `source_folder` and loads the sitemap.
  306. Creates nodes and resources, reads metadata and injects attributes.
  307. This is the model for hyde.
  308. """
  309. if not self.source_folder.exists:
  310. raise HydeException("The given source folder [%s]"
  311. " does not exist" % self.source_folder)
  312. with self.source_folder.walker as walker:
  313. def dont_ignore(name):
  314. for pattern in self.site.config.ignore:
  315. if fnmatch.fnmatch(name, pattern):
  316. return False
  317. return True
  318. @walker.folder_visitor
  319. def visit_folder(folder):
  320. if dont_ignore(folder.name):
  321. self.add_node(folder)
  322. else:
  323. logger.debug("Ignoring node: %s" % folder.name)
  324. return False
  325. @walker.file_visitor
  326. def visit_file(afile):
  327. if dont_ignore(afile.name):
  328. self.add_resource(afile)
  329. def _encode_path(base, path, safe):
  330. base = base.strip().replace(os.sep, '/')
  331. path = path.strip().replace(os.sep, '/')
  332. path = quote(path, safe) if safe is not None else quote(path)
  333. full_path = base.rstrip('/') + '/' + path.lstrip('/')
  334. return full_path
  335. class Site(object):
  336. """
  337. Represents the site to be generated.
  338. """
  339. def __init__(self, sitepath=None, config=None):
  340. super(Site, self).__init__()
  341. self.sitepath = Folder(Folder(sitepath).fully_expanded_path)
  342. # Add sitepath to the list of module search paths so that
  343. # local plugins can be included.
  344. sys.path.insert(0, self.sitepath.fully_expanded_path)
  345. self.config = config if config else Config(self.sitepath)
  346. self.content = RootNode(self.config.content_root_path, self)
  347. self.plugins = []
  348. self.context = {}
  349. def refresh_config(self):
  350. """
  351. Refreshes config data if one or more config files have
  352. changed. Note that this does not refresh the meta data.
  353. """
  354. if self.config.needs_refresh():
  355. logger.debug("Refreshing config data")
  356. self.config = Config(self.sitepath, self.config.config_file,
  357. self.config.config_dict)
  358. def reload_if_needed(self):
  359. """
  360. Reloads if the site has not been loaded before or if the
  361. configuration has changed since the last load.
  362. """
  363. if not len(self.content.child_nodes):
  364. self.load()
  365. def load(self):
  366. """
  367. Walks the content and media folders to load up the sitemap.
  368. """
  369. self.content.load()
  370. def _safe_chars(self, safe=None):
  371. if safe is not None:
  372. return safe
  373. elif self.config.encode_safe is not None:
  374. return self.config.encode_safe
  375. else:
  376. return None
  377. def content_url(self, path, safe=None):
  378. """
  379. Returns the content url by appending the base url from the config
  380. with the given path. The return value is url encoded.
  381. """
  382. return _encode_path(self.config.base_url, path, self._safe_chars(safe))
  383. def media_url(self, path, safe=None):
  384. """
  385. Returns the media url by appending the media base url from the config
  386. with the given path. The return value is url encoded.
  387. """
  388. return _encode_path(self.config.media_url, path,
  389. self._safe_chars(safe))
  390. def full_url(self, path, safe=None):
  391. """
  392. Determines if the given path is media or content based on the
  393. configuration and returns the appropriate url. The return value
  394. is url encoded.
  395. """
  396. if parse.urlparse(path)[:2] != ("", ""):
  397. return path
  398. if self.is_media(path):
  399. relative_path = File(path).get_relative_path(
  400. Folder(self.config.media_root))
  401. return self.media_url(relative_path, safe)
  402. else:
  403. return self.content_url(path, safe)
  404. def is_media(self, path):
  405. """
  406. Given the relative path, determines if it is content or media.
  407. """
  408. folder = self.content.source.child_folder(path)
  409. return folder.is_descendant_of(self.config.media_root_path)