PageRenderTime 34ms CodeModel.GetById 9ms app.highlight 21ms RepoModel.GetById 1ms app.codeStats 0ms

/hyde/site.py

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