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.
 
 
 

261 lines
7.1 KiB

  1. # -*- coding: utf-8 -*-
  2. """
  3. Contains data structures and utilities for hyde.
  4. """
  5. import codecs
  6. import yaml
  7. from datetime import datetime
  8. from UserDict import IterableUserDict
  9. from commando.util import getLoggerWithNullHandler
  10. from fswrap import File, Folder
  11. logger = getLoggerWithNullHandler('hyde.engine')
  12. SEQS = (tuple, list, set, frozenset)
  13. def make_expando(primitive):
  14. """
  15. Creates an expando object, a sequence of expando objects or just
  16. returns the primitive based on the primitive's type.
  17. """
  18. if isinstance(primitive, dict):
  19. return Expando(primitive)
  20. elif isinstance(primitive, SEQS):
  21. seq = type(primitive)
  22. return seq(make_expando(attr) for attr in primitive)
  23. else:
  24. return primitive
  25. class Expando(object):
  26. """
  27. A generic expando class that creates attributes from
  28. the passed in dictionary.
  29. """
  30. def __init__(self, d):
  31. super(Expando, self).__init__()
  32. self.update(d)
  33. def __iter__(self):
  34. """
  35. Returns an iterator for all the items in the
  36. dictionary as key value pairs.
  37. """
  38. return self.__dict__.iteritems()
  39. def update(self, d):
  40. """
  41. Updates the expando with a new dictionary
  42. """
  43. d = d or {}
  44. if isinstance(d, dict):
  45. for key, value in d.items():
  46. self.set_expando(key, value)
  47. elif isinstance(d, Expando):
  48. self.update(d.to_dict())
  49. def set_expando(self, key, value):
  50. """
  51. Sets the expando attribute after
  52. transforming the value.
  53. """
  54. setattr(self, unicode(key).encode('utf-8'), make_expando(value))
  55. def __repr__(self):
  56. return unicode(self.to_dict())
  57. def to_dict(self):
  58. """
  59. Reverse transform an expando to dict
  60. """
  61. result = {}
  62. d = self.__dict__
  63. for k, v in d.items():
  64. if isinstance(v, Expando):
  65. result[k] = v.to_dict()
  66. elif isinstance(v, SEQS):
  67. seq = type(v)
  68. result[k] = seq(item.to_dict()
  69. if isinstance(item, Expando)
  70. else item for item in v
  71. )
  72. else:
  73. result[k] = v
  74. return result
  75. def get(self, key, default=None):
  76. """
  77. Dict like get helper method
  78. """
  79. return self.__dict__.get(key, default)
  80. class Context(object):
  81. """
  82. Wraps the context related functions and utilities.
  83. """
  84. @staticmethod
  85. def load(sitepath, ctx):
  86. """
  87. Load context from config data and providers.
  88. """
  89. context = {}
  90. try:
  91. context.update(ctx.data.__dict__)
  92. except AttributeError:
  93. # No context data found
  94. pass
  95. providers = {}
  96. try:
  97. providers.update(ctx.providers.__dict__)
  98. except AttributeError:
  99. # No providers found
  100. pass
  101. for provider_name, resource_name in providers.items():
  102. res = File(Folder(sitepath).child(resource_name))
  103. if res.exists:
  104. data = make_expando(yaml.load(res.read_all()))
  105. context[provider_name] = data
  106. return context
  107. class Dependents(IterableUserDict):
  108. """
  109. Represents the dependency graph for hyde.
  110. """
  111. def __init__(self, sitepath, depends_file_name='.hyde_deps'):
  112. self.sitepath = Folder(sitepath)
  113. self.deps_file = File(self.sitepath.child(depends_file_name))
  114. self.data = {}
  115. if self.deps_file.exists:
  116. self.data = yaml.load(self.deps_file.read_all())
  117. import atexit
  118. atexit.register(self.save)
  119. def save(self):
  120. """
  121. Saves the dependency graph (just a dict for now).
  122. """
  123. if self.deps_file.parent.exists:
  124. self.deps_file.write(yaml.dump(self.data))
  125. def _expand_path(sitepath, path):
  126. child = sitepath.child_folder(path)
  127. return Folder(child.fully_expanded_path)
  128. class Config(Expando):
  129. """
  130. Represents the hyde configuration file
  131. """
  132. def __init__(self, sitepath, config_file=None, config_dict=None):
  133. self.default_config = dict(
  134. mode='production',
  135. simple_copy=[],
  136. content_root='content',
  137. deploy_root='deploy',
  138. media_root='media',
  139. layout_root='layout',
  140. media_url='/media',
  141. base_url="/",
  142. encode_safe=None,
  143. not_found='404.html',
  144. plugins=[],
  145. ignore=["*~", "*.bak", ".hg", ".git", ".svn"],
  146. meta={
  147. "nodemeta": 'meta.yaml'
  148. }
  149. )
  150. self.config_file = config_file
  151. self.config_dict = config_dict
  152. self.load_time = datetime.min
  153. self.config_files = []
  154. self.sitepath = Folder(sitepath)
  155. super(Config, self).__init__(self.load())
  156. @property
  157. def last_modified(self):
  158. return max((conf.last_modified for conf in self.config_files))
  159. def needs_refresh(self):
  160. if not self.config_files:
  161. return True
  162. return any((conf.has_changed_since(self.load_time)
  163. for conf in self.config_files))
  164. def load(self):
  165. conf = dict(**self.default_config)
  166. conf.update(self.read_config(self.config_file))
  167. if self.config_dict:
  168. conf.update(self.config_dict)
  169. return conf
  170. def reload(self):
  171. if not self.config_file:
  172. return
  173. self.update(self.load())
  174. def read_config(self, config_file):
  175. """
  176. Reads the configuration file and updates this
  177. object while allowing for inherited configurations.
  178. """
  179. conf_file = self.sitepath.child(
  180. config_file if
  181. config_file else 'site.yaml')
  182. conf = {}
  183. if File(conf_file).exists:
  184. self.config_files.append(File(conf_file))
  185. logger.info("Reading site configuration from [%s]", conf_file)
  186. with codecs.open(conf_file, 'r', 'utf-8') as stream:
  187. conf = yaml.load(stream)
  188. if 'extends' in conf:
  189. parent = self.read_config(conf['extends'])
  190. parent.update(conf)
  191. conf = parent
  192. self.load_time = datetime.now()
  193. return conf
  194. @property
  195. def deploy_root_path(self):
  196. """
  197. Derives the deploy root path from the site path
  198. """
  199. return _expand_path(self.sitepath, self.deploy_root)
  200. @property
  201. def content_root_path(self):
  202. """
  203. Derives the content root path from the site path
  204. """
  205. return _expand_path(self.sitepath, self.content_root)
  206. @property
  207. def media_root_path(self):
  208. """
  209. Derives the media root path from the content path
  210. """
  211. path = Folder(self.content_root).child(self.media_root)
  212. return _expand_path(self.sitepath, path)
  213. @property
  214. def layout_root_path(self):
  215. """
  216. Derives the layout root path from the site path
  217. """
  218. return _expand_path(self.sitepath, self.layout_root)