| @@ -11,4 +11,4 @@ test_site | |||||
| dist | dist | ||||
| build | build | ||||
| *egg* | *egg* | ||||
| .idea | |||||
| @@ -50,6 +50,7 @@ class Engine(Application): | |||||
| layout.copy_contents_to(args.sitepath) | layout.copy_contents_to(args.sitepath) | ||||
| @subcommand('gen', help='Generate the site') | @subcommand('gen', help='Generate the site') | ||||
| @store('-c', '--config-path', default='site.yaml', help='The configuration used to generate the site') | |||||
| @store('-d', '--deploy-path', default='deploy', help='Where should the site be generated?') | @store('-d', '--deploy-path', default='deploy', help='Where should the site be generated?') | ||||
| def gen(self, args): | def gen(self, args): | ||||
| """ | """ | ||||
| @@ -1,678 +0,0 @@ | |||||
| """ | |||||
| Unified interface for performing file system tasks. Uses os, os.path. shutil | |||||
| and distutil to perform the tasks. The behavior of some functions is slightly | |||||
| contaminated with requirements from Hyde: For example, the backup function | |||||
| deletes the directory that is being backed up. | |||||
| """ | |||||
| import os | |||||
| import shutil | |||||
| import codecs | |||||
| import fnmatch | |||||
| from datetime import datetime | |||||
| # pylint: disable-msg=E0611 | |||||
| from distutils import dir_util, file_util | |||||
| @staticmethod | |||||
| def filter_hidden_inplace(item_list): | |||||
| """ | |||||
| Given a list of filenames, removes filenames for invisible files (whose | |||||
| names begin with dots) or files whose names end in tildes '~'. | |||||
| Does not remove files with the filname '.htaccess'. | |||||
| The list is modified in-place; this function has no return value. | |||||
| """ | |||||
| if not item_list: | |||||
| return | |||||
| wanted = filter( | |||||
| lambda item: | |||||
| not ((item.startswith('.') and item != ".htaccess") | |||||
| or item.endswith('~')), | |||||
| item_list) | |||||
| count = len(item_list) | |||||
| good_item_count = len(wanted) | |||||
| if count == good_item_count: | |||||
| return | |||||
| item_list[:good_item_count] = wanted | |||||
| for _ in range(good_item_count, count): | |||||
| item_list.pop() | |||||
| @staticmethod | |||||
| def get_path_fragment(root_dir, a_dir): | |||||
| """ | |||||
| Gets the path fragment starting at root_dir to a_dir | |||||
| """ | |||||
| current_dir = a_dir | |||||
| current_fragment = '' | |||||
| while not current_dir == root_dir: | |||||
| (current_dir, current_fragment_part) = os.path.split(current_dir) | |||||
| current_fragment = os.path.join( | |||||
| current_fragment_part, current_fragment) | |||||
| return current_fragment | |||||
| @staticmethod | |||||
| def get_mirror_dir(directory, source_root, mirror_root, ignore_root = False): | |||||
| """ | |||||
| Returns the mirror directory from source_root to mirror_root | |||||
| """ | |||||
| current_fragment = get_path_fragment(source_root, directory) | |||||
| if not current_fragment: | |||||
| return mirror_root | |||||
| mirror_directory = mirror_root | |||||
| if not ignore_root: | |||||
| mirror_directory = os.path.join( | |||||
| mirror_root, | |||||
| os.path.basename(source_root)) | |||||
| mirror_directory = os.path.join( | |||||
| mirror_directory, current_fragment) | |||||
| return mirror_directory | |||||
| @staticmethod | |||||
| def mirror_dir_tree(directory, source_root, mirror_root, ignore_root = False): | |||||
| """ | |||||
| Create the mirror directory tree | |||||
| """ | |||||
| mirror_directory = get_mirror_dir( | |||||
| directory, source_root, | |||||
| mirror_root, ignore_root) | |||||
| try: | |||||
| os.makedirs(mirror_directory) | |||||
| except os.error: | |||||
| pass | |||||
| return mirror_directory | |||||
| class FileSystemEntity(object): | |||||
| """ | |||||
| Base class for files and folders. | |||||
| """ | |||||
| def __init__(self, path): | |||||
| super(FileSystemEntity, self).__init__() | |||||
| if path is FileSystemEntity: | |||||
| self.path = path.path | |||||
| else: | |||||
| self.path = path | |||||
| def __str__(self): | |||||
| return self.path | |||||
| def __repr__(self): | |||||
| return self.path | |||||
| def allow(self, include=None, exclude=None): | |||||
| """ | |||||
| Given a set of wilcard patterns in the include and exclude arguments, | |||||
| tests if the patterns allow this item for processing. | |||||
| The exclude parameter is processed first as a broader filter and then | |||||
| include is used as a narrower filter to override the results for more | |||||
| specific files. | |||||
| Example: | |||||
| exclude = (".*", "*~") | |||||
| include = (".htaccess") | |||||
| """ | |||||
| if not include: | |||||
| include = () | |||||
| if not exclude: | |||||
| exclude = () | |||||
| if reduce(lambda result, | |||||
| pattern: result or | |||||
| fnmatch.fnmatch(self.name, pattern), include, False): | |||||
| return True | |||||
| if reduce(lambda result, pattern: | |||||
| result and not fnmatch.fnmatch(self.name, pattern), | |||||
| exclude, True): | |||||
| return True | |||||
| return False | |||||
| @property | |||||
| def humblepath(self): | |||||
| """ | |||||
| Expands variables, user, normalizes path and case and coverts | |||||
| to absolute. | |||||
| """ | |||||
| return os.path.abspath( | |||||
| os.path.normpath( | |||||
| os.path.normcase( | |||||
| os.path.expandvars( | |||||
| os.path.expanduser(self.path))))) | |||||
| def same_as(self, other): | |||||
| """ | |||||
| Checks if the path of this object is same as `other`. `other` must | |||||
| be a FileSystemEntity. | |||||
| """ | |||||
| return (self.humblepath.rstrip(os.sep) == | |||||
| other.humblepath.rstrip(os.sep)) | |||||
| @property | |||||
| def exists(self): | |||||
| """ | |||||
| Checks if the entity exists in the file system. | |||||
| """ | |||||
| return os.path.exists(self.path) | |||||
| @property | |||||
| def isdir(self): | |||||
| """ | |||||
| Is this a folder. | |||||
| """ | |||||
| return os.path.isdir(self.path) | |||||
| @property | |||||
| def stats(self): | |||||
| """ | |||||
| Shortcut for os.stat. | |||||
| """ | |||||
| return os.stat(self.path) | |||||
| @property | |||||
| def name(self): | |||||
| """ | |||||
| Name of the entity. Calls os.path.basename. | |||||
| """ | |||||
| return os.path.basename(self.path) | |||||
| @property | |||||
| def parent(self): | |||||
| """ | |||||
| The parent folder. Returns a `Folder` object. | |||||
| """ | |||||
| return Folder(os.path.dirname(self.path)) | |||||
| def __get_destination__(self, destination): | |||||
| """ | |||||
| Returns a File or Folder object that would represent this entity | |||||
| if it were copied or moved to `destination`. `destination` must be | |||||
| an instance of File or Folder. | |||||
| """ | |||||
| if os.path.isdir(str(destination)): | |||||
| target = destination.child(self.name) | |||||
| if os.path.isdir(self.path): | |||||
| return Folder(target) | |||||
| else: return File(target) | |||||
| else: | |||||
| return destination | |||||
| # pylint: disable-msg=R0904,W0142 | |||||
| class File(FileSystemEntity): | |||||
| """ | |||||
| Encapsulates commonly used functions related to files. | |||||
| """ | |||||
| def __init__(self, path): | |||||
| super(File, self).__init__(path) | |||||
| @property | |||||
| def size(self): | |||||
| """ | |||||
| Gets the file size | |||||
| """ | |||||
| return os.path.getsize(self.path) | |||||
| #return 1 | |||||
| def has_extension(self, extension): | |||||
| """ | |||||
| Checks if this file has the given extension. | |||||
| """ | |||||
| return self.extension == extension | |||||
| def delete(self): | |||||
| """ | |||||
| Deletes if the file exists. | |||||
| """ | |||||
| if self.exists: | |||||
| os.remove(self.path) | |||||
| @property | |||||
| def last_modified(self): | |||||
| """ | |||||
| Returns a datetime object representing the last modified time. | |||||
| Calls os.path.getmtime. | |||||
| """ | |||||
| return datetime.fromtimestamp(os.path.getmtime(self.path)) | |||||
| def changed_since(self, basetime): | |||||
| """ | |||||
| Returns True if the file has been changed since the given time. | |||||
| """ | |||||
| return self.last_modified > basetime | |||||
| def older_than(self, another_file): | |||||
| """ | |||||
| Checks if this file is older than the given file. Uses last_modified to | |||||
| determine age. | |||||
| """ | |||||
| return another_file.last_modified > self.last_modified | |||||
| @property | |||||
| def path_without_extension(self): | |||||
| """ | |||||
| The full path of the file without its extension. | |||||
| """ | |||||
| return os.path.splitext(self.path)[0] | |||||
| @property | |||||
| def name_without_extension(self): | |||||
| """ | |||||
| Name of the file without its extension. | |||||
| """ | |||||
| return os.path.splitext(self.name)[0] | |||||
| @property | |||||
| def extension(self): | |||||
| """ | |||||
| File's extension prefixed with a dot. | |||||
| """ | |||||
| return os.path.splitext(self.path)[1] | |||||
| @property | |||||
| def kind(self): | |||||
| """ | |||||
| File's extension without a dot prefix. | |||||
| """ | |||||
| return self.extension.lstrip(".") | |||||
| def move_to(self, destination): | |||||
| """ | |||||
| Moves the file to the given destination. Returns a File | |||||
| object that represents the target file. `destination` must | |||||
| be a File or Folder object. | |||||
| """ | |||||
| shutil.move(self.path, str(destination)) | |||||
| return self.__get_destination__(destination) | |||||
| def copy_to(self, destination): | |||||
| """ | |||||
| Copies the file to the given destination. Returns a File | |||||
| object that represents the target file. `destination` must | |||||
| be a File or Folder object. | |||||
| """ | |||||
| shutil.copy(self.path, str(destination)) | |||||
| return self.__get_destination__(destination) | |||||
| def write(self, text, encoding="utf-8"): | |||||
| """ | |||||
| Writes the given text to the file using the given encoding. | |||||
| """ | |||||
| fout = codecs.open(self.path, 'w', encoding) | |||||
| fout.write(text) | |||||
| fout.close() | |||||
| def read_all(self): | |||||
| """ | |||||
| Reads from the file and returns the content as a string. | |||||
| """ | |||||
| fin = codecs.open(self.path, 'r') | |||||
| read_text = fin.read() | |||||
| fin.close() | |||||
| return read_text | |||||
| # pylint: disable-msg=R0904,W0142 | |||||
| class Folder(FileSystemEntity): | |||||
| """ | |||||
| Encapsulates commonly used directory functions. | |||||
| """ | |||||
| def __init__(self, path): | |||||
| super(Folder, self).__init__(path) | |||||
| def __str__(self): | |||||
| return self.path | |||||
| def __repr__(self): | |||||
| return self.path | |||||
| def delete(self): | |||||
| """ | |||||
| Deletes the directory if it exists. | |||||
| """ | |||||
| if self.exists: | |||||
| shutil.rmtree(self.path) | |||||
| def depth(self): | |||||
| """ | |||||
| Returns the number of ancestors of this directory. | |||||
| """ | |||||
| return len(self.path.split(os.sep)) | |||||
| def make(self): | |||||
| """ | |||||
| Creates this directory and any of the missing directories in the path. | |||||
| Any errors that may occur are eaten. | |||||
| """ | |||||
| try: | |||||
| if not self.exists: | |||||
| os.makedirs(self.path) | |||||
| except os.error: | |||||
| pass | |||||
| return self | |||||
| def is_parent_of(self, other_entity): | |||||
| """ | |||||
| Returns True if this directory is a direct parent of the the given | |||||
| directory. | |||||
| """ | |||||
| return self.same_as(other_entity.parent) | |||||
| def is_ancestor_of(self, other_entity): | |||||
| """ | |||||
| Returns True if this directory is in the path of the given directory. | |||||
| Note that this will return True if the given directory is same as this. | |||||
| """ | |||||
| folder = other_entity | |||||
| while not folder.parent.same_as(folder): | |||||
| folder = folder.parent | |||||
| if self.same_as(folder): | |||||
| return True | |||||
| return False | |||||
| def child(self, name): | |||||
| """ | |||||
| Returns a path of a child item represented by `name`. | |||||
| """ | |||||
| return os.path.join(self.path, name) | |||||
| def child_folder(self, *args): | |||||
| """ | |||||
| Returns a Folder object by joining the path component in args | |||||
| to this directory's path. | |||||
| """ | |||||
| return Folder(os.path.join(self.path, *args)) | |||||
| def child_folder_with_fragment(self, fragment): | |||||
| """ | |||||
| Returns a Folder object by joining the fragment to | |||||
| this directory's path. | |||||
| """ | |||||
| return Folder(os.path.join(self.path, fragment.lstrip(os.sep))) | |||||
| def get_fragment(self, root): | |||||
| """ | |||||
| Returns the path fragment of this directory starting with the given | |||||
| directory. | |||||
| """ | |||||
| return get_path_fragment(str(root), self.path) | |||||
| def get_mirror_folder(self, root, mirror_root, ignore_root=False): | |||||
| """ | |||||
| Returns a Folder object that reperesents if the entire fragment of this | |||||
| directory starting with `root` were copied to `mirror_root`. If ignore_root | |||||
| is True, the mirror does not include `root` directory itself. | |||||
| Example: | |||||
| Current Directory: /usr/local/hyde/stuff | |||||
| root: /usr/local/hyde | |||||
| mirror_root: /usr/tmp | |||||
| Result: | |||||
| if ignore_root == False: | |||||
| Folder(/usr/tmp/hyde/stuff) | |||||
| if ignore_root == True: | |||||
| Folder(/usr/tmp/stuff) | |||||
| """ | |||||
| path = get_mirror_dir(self.path, | |||||
| str(root), str(mirror_root), ignore_root) | |||||
| return Folder(path) | |||||
| def create_mirror_folder(self, root, mirror_root, ignore_root=False): | |||||
| """ | |||||
| Creates the mirror directory returned by `get_mirror_folder` | |||||
| """ | |||||
| mirror_folder = self.get_mirror_folder( | |||||
| root, mirror_root, ignore_root) | |||||
| mirror_folder.make() | |||||
| return mirror_folder | |||||
| def backup(self, destination): | |||||
| """ | |||||
| Creates a backup of this directory in the given destination. The backup is | |||||
| suffixed with a number for uniqueness. Deletes this directory after backup | |||||
| is complete. | |||||
| """ | |||||
| new_name = self.name | |||||
| count = 0 | |||||
| dest = Folder(destination.child(new_name)) | |||||
| while(True): | |||||
| dest = Folder(destination.child(new_name)) | |||||
| if not dest.exists: | |||||
| break | |||||
| else: | |||||
| count = count + 1 | |||||
| new_name = self.name + str(count) | |||||
| dest.make() | |||||
| dest.move_contents_of(self) | |||||
| self.delete() | |||||
| return dest | |||||
| def move_to(self, destination): | |||||
| """ | |||||
| Moves this directory to the given destination. Returns a Folder object | |||||
| that represents the moved directory. | |||||
| """ | |||||
| shutil.copytree(self.path, str(destination)) | |||||
| shutil.rmtree(self.path) | |||||
| return self.__get_destination__(destination) | |||||
| def copy_to(self, destination): | |||||
| """ | |||||
| Copies this directory to the given destination. Returns a Folder object | |||||
| that represents the moved directory. | |||||
| """ | |||||
| shutil.copytree(self.path, str(destination)) | |||||
| return self.__get_destination__(destination) | |||||
| def move_folder_from(self, source, incremental=False): | |||||
| """ | |||||
| Moves the given source directory to this directory. If incremental is True | |||||
| only newer objects are overwritten. | |||||
| """ | |||||
| self.copy_folder_from(source, incremental) | |||||
| shutil.rmtree(str(source)) | |||||
| def copy_folder_from(self, source, incremental=False): | |||||
| """ | |||||
| Copies the given source directory to this directory. If incremental is True | |||||
| only newer objects are overwritten. | |||||
| """ | |||||
| # There is a bug in dir_util that makes copy_tree crash if a folder in | |||||
| # the tree has been deleted before and readded now. To workaround the | |||||
| # bug, we first walk the tree and create directories that are needed. | |||||
| # | |||||
| # pylint: disable-msg=C0111,W0232 | |||||
| target_root = self | |||||
| # pylint: disable-msg=R0903 | |||||
| class _DirCreator: | |||||
| @staticmethod | |||||
| def visit_folder(folder): | |||||
| target = folder.get_mirror_folder( | |||||
| source.parent, target_root, ignore_root=True) | |||||
| target.make() | |||||
| source.walk(_DirCreator) | |||||
| dir_util.copy_tree(str(source), | |||||
| self.child(source.name), | |||||
| update=incremental) | |||||
| def move_contents_of(self, source, move_empty_folders=True, | |||||
| incremental=False): | |||||
| """ | |||||
| Moves the contents of the given source directory to this directory. If | |||||
| incremental is True only newer objects are overwritten. | |||||
| """ | |||||
| # pylint: disable-msg=C0111,W0232 | |||||
| class _Mover: | |||||
| @staticmethod | |||||
| def visit_folder(folder): | |||||
| self.move_folder_from(folder, incremental) | |||||
| @staticmethod | |||||
| def visit_file(a_file): | |||||
| self.move_file_from(a_file, incremental) | |||||
| source.list(_Mover, move_empty_folders) | |||||
| def copy_contents_of(self, source, copy_empty_folders=True, | |||||
| incremental=False): | |||||
| """ | |||||
| Copies the contents of the given source directory to this directory. If | |||||
| incremental is True only newer objects are overwritten. | |||||
| """ | |||||
| # pylint: disable-msg=C0111,W0232 | |||||
| class _Copier: | |||||
| @staticmethod | |||||
| def visit_folder(folder): | |||||
| self.copy_folder_from(folder, incremental) | |||||
| @staticmethod | |||||
| def visit_file(a_file): | |||||
| self.copy_file_from(a_file, incremental) | |||||
| source.list(_Copier, copy_empty_folders) | |||||
| def move_file_from(self, source, incremental=False): | |||||
| """ | |||||
| Moves the given source file to this directory. If incremental is True the | |||||
| move is performed only if the source file is newer. | |||||
| """ | |||||
| self.copy_file_from(source, incremental) | |||||
| source.delete() | |||||
| def copy_file_from(self, source, incremental=False): | |||||
| """ | |||||
| Copies the given source file to this directory. If incremental is True the | |||||
| move is performed only if the source file is newer. | |||||
| """ | |||||
| file_util.copy_file(str(source), self.path, update=incremental) | |||||
| def list(self, visitor, list_empty_folders=True): | |||||
| """ | |||||
| Calls the visitor.visit_file or visitor.visit_folder for each file or folder | |||||
| in this directory. If list_empty_folders is False folders that are empty are | |||||
| skipped. | |||||
| """ | |||||
| a_files = os.listdir(self.path) | |||||
| for a_file in a_files: | |||||
| path = os.path.join(self.path, str(a_file)) | |||||
| if os.path.isdir(path): | |||||
| if not list_empty_folders: | |||||
| if Folder(path).empty(): | |||||
| continue | |||||
| visitor.visit_folder(Folder(path)) | |||||
| else: | |||||
| visitor.visit_file(File(path)) | |||||
| def empty(self): | |||||
| """ | |||||
| Checks if this directory or any of its subdirectories contain files. | |||||
| """ | |||||
| paths = os.listdir(self.path) | |||||
| for path in paths: | |||||
| if os.path.isdir(path): | |||||
| if not Folder(path).empty(): | |||||
| return False | |||||
| else: | |||||
| return False | |||||
| return True | |||||
| def walk(self, visitor = None, pattern = None): | |||||
| """ | |||||
| Walks the entire hirearchy of this directory starting with itself. | |||||
| Calls visitor.visit_folder first and then calls visitor.visit_file for | |||||
| any files found. After all files and folders have been exhausted | |||||
| visitor.visit_complete is called. | |||||
| If a pattern is provided, only the files that match the pattern are | |||||
| processed. | |||||
| If visitor.visit_folder returns False, the files in the folder are not | |||||
| processed. | |||||
| """ | |||||
| def __visit_folder__(visitor, folder): | |||||
| process_folder = True | |||||
| if visitor and hasattr(visitor,'visit_folder'): | |||||
| process_folder = visitor.visit_folder(folder) | |||||
| # If there is no return value assume true | |||||
| # | |||||
| if process_folder is None: | |||||
| process_folder = True | |||||
| return process_folder | |||||
| def __visit_file__(visitor, a_file): | |||||
| if visitor and hasattr(visitor,'visit_file'): | |||||
| visitor.visit_file(a_file) | |||||
| def __visit_complete__(visitor): | |||||
| if visitor and hasattr(visitor,'visit_complete'): | |||||
| visitor.visit_complete() | |||||
| for root, dirs, a_files in os.walk(self.path): | |||||
| folder = Folder(root) | |||||
| if not __visit_folder__(visitor, folder): | |||||
| dirs[:] = [] | |||||
| continue | |||||
| for a_file in a_files: | |||||
| if not pattern or fnmatch.fnmatch(a_file, pattern): | |||||
| __visit_file__(visitor, File(folder.child(a_file))) | |||||
| __visit_complete__(visitor) | |||||
| @@ -14,6 +14,7 @@ import os | |||||
| import shutil | import shutil | ||||
| from distutils import dir_util | from distutils import dir_util | ||||
| import functools | import functools | ||||
| import itertools | |||||
| # pylint: disable-msg=E0611 | # pylint: disable-msg=E0611 | ||||
| logger = logging.getLogger('fs') | logger = logging.getLogger('fs') | ||||
| @@ -28,7 +29,7 @@ class FS(object): | |||||
| """ | """ | ||||
| def __init__(self, path): | def __init__(self, path): | ||||
| super(FS, self).__init__() | super(FS, self).__init__() | ||||
| self.path = str(path).strip() | |||||
| self.path = str(path).strip().rstrip(os.sep) | |||||
| def __str__(self): | def __str__(self): | ||||
| return self.path | return self.path | ||||
| @@ -63,6 +64,14 @@ class FS(object): | |||||
| """ | """ | ||||
| return Folder(os.path.dirname(self.path)) | return Folder(os.path.dirname(self.path)) | ||||
| @property | |||||
| def depth(self): | |||||
| """ | |||||
| Returns the number of ancestors of this directory. | |||||
| """ | |||||
| return len(self.path.rstrip(os.sep).split(os.sep)) | |||||
| def ancestors(self, stop=None): | def ancestors(self, stop=None): | ||||
| """ | """ | ||||
| Generates the parents until stop or the absolute | Generates the parents until stop or the absolute | ||||
| @@ -75,7 +84,16 @@ class FS(object): | |||||
| yield f.parent | yield f.parent | ||||
| f = f.parent | f = f.parent | ||||
| def get_fragment(self, root): | |||||
| def is_descendant_of(self, ancestor): | |||||
| stop = Folder(ancestor) | |||||
| for folder in self.ancestors(): | |||||
| if folder == stop: | |||||
| return True | |||||
| if stop.depth > folder.depth: | |||||
| return False | |||||
| return False | |||||
| def get_relative_path(self, root): | |||||
| """ | """ | ||||
| Gets the fragment of the current path starting at root. | Gets the fragment of the current path starting at root. | ||||
| """ | """ | ||||
| @@ -89,7 +107,7 @@ class FS(object): | |||||
| >>> Folder('/usr/local/hyde/stuff').get_mirror('/usr/tmp', source_root='/usr/local/hyde') | >>> Folder('/usr/local/hyde/stuff').get_mirror('/usr/tmp', source_root='/usr/local/hyde') | ||||
| Folder('/usr/tmp/stuff') | Folder('/usr/tmp/stuff') | ||||
| """ | """ | ||||
| fragment = self.get_fragment(source_root if source_root else self.parent) | |||||
| fragment = self.get_relative_path(source_root if source_root else self.parent) | |||||
| return Folder(target_root).child(fragment) | return Folder(target_root).child(fragment) | ||||
| @staticmethod | @staticmethod | ||||
| @@ -0,0 +1,9 @@ | |||||
| site: | |||||
| mode: development | |||||
| media: | |||||
| root: | |||||
| path: media # Relative path from site root (the directory where this file exists) | |||||
| url: /media | |||||
| widgets: | |||||
| plugins: | |||||
| aggregators: | |||||
| @@ -1 +1,5 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| # -*- coding: utf-8 -*- | |||||
| # 1. start_node | |||||
| # 2. start_resource | |||||
| # 3. end_resource | |||||
| # 4. end_node | |||||
| @@ -0,0 +1,245 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| """ | |||||
| Parses & holds information about the site to be generated. | |||||
| """ | |||||
| from hyde.fs import File, Folder | |||||
| from hyde.exceptions import HydeException | |||||
| import logging | |||||
| import os | |||||
| from logging import NullHandler | |||||
| logger = logging.getLogger('hyde.site') | |||||
| logger.addHandler(NullHandler()) | |||||
| class Resource(object): | |||||
| """ | |||||
| Represents any file that is processed by hyde | |||||
| """ | |||||
| def __init__(self, source_file, node): | |||||
| super(Resource, self).__init__() | |||||
| self.source_file = source_file | |||||
| if not node: | |||||
| raise HydeException("Resource cannot exist without a node") | |||||
| if not source_file: | |||||
| raise HydeException("Source file is required to instantiate a resource") | |||||
| self.node = node | |||||
| def __repr__(self): | |||||
| return self.path | |||||
| @property | |||||
| def path(self): | |||||
| """ | |||||
| Gets the source path of this node. | |||||
| """ | |||||
| return self.source_file.path | |||||
| @property | |||||
| def relative_path(self): | |||||
| """ | |||||
| Gets the path relative to the root folder (Content, Media, Layout) | |||||
| """ | |||||
| return self.source_file.get_relative_path(self.node.root.source_folder) | |||||
| class Node(object): | |||||
| """ | |||||
| Represents any folder that is processed by hyde | |||||
| """ | |||||
| def __init__(self, source_folder, parent=None): | |||||
| super(Node, self).__init__() | |||||
| if not source_folder: | |||||
| raise HydeException("Source folder is required to instantiate a node.") | |||||
| self.root = self | |||||
| self.module = None | |||||
| self.site = None | |||||
| self.source_folder = Folder(str(source_folder)) | |||||
| self.parent = parent | |||||
| if parent: | |||||
| self.root = self.parent.root | |||||
| self.module = self.parent.module if self.parent.module else self | |||||
| self.site = parent.site | |||||
| self.child_nodes = [] | |||||
| self.resources = [] | |||||
| def __repr__(self): | |||||
| return self.path | |||||
| def add_child_node(self, folder): | |||||
| """ | |||||
| Creates a new child node and adds it to the list of child nodes. | |||||
| """ | |||||
| if folder.parent != self.source_folder: | |||||
| raise HydeException("The given folder [%s] is not a direct descendant of [%s]" % | |||||
| (folder, self.source_folder)) | |||||
| node = Node(folder, self) | |||||
| self.child_nodes.append(node) | |||||
| return node | |||||
| def add_child_resource(self, afile): | |||||
| """ | |||||
| Creates a new resource and adds it to the list of child resources. | |||||
| """ | |||||
| if afile.parent != self.source_folder: | |||||
| raise HydeException("The given file [%s] is not a direct descendant of [%s]" % | |||||
| (afile, self.source_folder)) | |||||
| resource = Resource(afile, self) | |||||
| self.resources.append(resource) | |||||
| return resource | |||||
| @property | |||||
| def path(self): | |||||
| """ | |||||
| Gets the source path of this node. | |||||
| """ | |||||
| return self.source_folder.path | |||||
| @property | |||||
| def relative_path(self): | |||||
| """ | |||||
| Gets the path relative to the root folder (Content, Media, Layout) | |||||
| """ | |||||
| return self.source_folder.get_relative_path(self.root.source_folder) | |||||
| class RootNode(Node): | |||||
| """ | |||||
| Represents one of the roots of site: Content, Media or Layout | |||||
| """ | |||||
| def __init__(self, source_folder, site): | |||||
| super(RootNode, self).__init__(source_folder) | |||||
| self.site = site | |||||
| self.node_map = {} | |||||
| self.resource_map = {} | |||||
| def node_from_path(self, path): | |||||
| """ | |||||
| Gets the node that maps to the given path. If no match is found it returns None. | |||||
| """ | |||||
| if Folder(path) == self.source_folder: | |||||
| return self | |||||
| return self.node_map.get(str(Folder(path)), None) | |||||
| def node_from_relative_path(self, relative_path): | |||||
| """ | |||||
| Gets the content node that maps to the given relative path. If no match is found it returns None. | |||||
| """ | |||||
| return self.node_from_path(self.source_folder.child(str(relative_path))) | |||||
| def resource_from_path(self, path): | |||||
| """ | |||||
| Gets the resource that maps to the given path. If no match is found it returns None. | |||||
| """ | |||||
| return self.resource_map.get(str(File(path)), None) | |||||
| def resource_from_relative_path(self, relative_path): | |||||
| """ | |||||
| Gets the content resource that maps to the given relative path. If no match is found it returns None. | |||||
| """ | |||||
| return self.resource_from_path(self.source_folder.child(str(relative_path))) | |||||
| def add_node(self, a_folder): | |||||
| """ | |||||
| Adds a new node to this folder's hierarchy. Also adds to to the hashtable of path to | |||||
| node associations for quick lookup. | |||||
| """ | |||||
| folder = Folder(a_folder) | |||||
| node = self.node_from_path(folder) | |||||
| if node: | |||||
| logger.info("Node exists at [%s]" % node.relative_path) | |||||
| return node | |||||
| if not folder.is_descendant_of(self.source_folder): | |||||
| raise HydeException("The given folder [%s] does not belong to this hierarchy [%s]" % | |||||
| (folder, self.source_folder)) | |||||
| p_folder = folder | |||||
| parent = None | |||||
| hierarchy = [] | |||||
| while not parent: | |||||
| hierarchy.append(p_folder) | |||||
| p_folder = p_folder.parent | |||||
| parent = self.node_from_path(p_folder) | |||||
| hierarchy.reverse() | |||||
| node = parent if parent else self | |||||
| for h_folder in hierarchy: | |||||
| node = node.add_child_node(h_folder) | |||||
| self.node_map[str(h_folder)] = node | |||||
| logger.info("Added node [%s] to [%s]" % (node.relative_path, self.source_folder)) | |||||
| return node | |||||
| def add_resource(self, a_file): | |||||
| """ | |||||
| Adds a file to the parent node. Also adds to to the hashtable of path to | |||||
| resource associations for quick lookup. | |||||
| """ | |||||
| afile = File(a_file) | |||||
| resource = self.resource_from_path(afile) | |||||
| if resource: | |||||
| logger.info("Resource exists at [%s]" % resource.relative_path) | |||||
| if not afile.is_descendant_of(self.source_folder): | |||||
| raise HydeException("The given file [%s] does not reside in this hierarchy [%s]" % | |||||
| (afile, self.content_folder)) | |||||
| node = self.node_from_path(afile.parent) | |||||
| if not node: | |||||
| node = self.add_node(afile.parent) | |||||
| resource = node.add_child_resource(afile) | |||||
| self.resource_map[str(afile)] = resource | |||||
| logger.info("Added resource [%s] to [%s]" % (resource.relative_path, self.source_folder)) | |||||
| return resource | |||||
| def build(self): | |||||
| """ | |||||
| Walks the `source_folder` and builds the sitemap. Creates nodes and resources, | |||||
| reads metadata and injects attributes. This is the model for hyde. | |||||
| """ | |||||
| if not self.source_folder.exists: | |||||
| raise HydeException("The given source folder[%s] does not exist" % self.source_folder) | |||||
| with self.source_folder.walk() as walker: | |||||
| @walker.folder_visitor | |||||
| def visit_folder(folder): | |||||
| self.add_node(folder) | |||||
| @walker.file_visitor | |||||
| def visit_file(afile): | |||||
| self.add_resource(afile) | |||||
| class Site(object): | |||||
| """ | |||||
| Represents the site to be generated | |||||
| """ | |||||
| def __init__(self, site_path): | |||||
| super(Site, self).__init__() | |||||
| self.site_path = Folder(str(site_path)) | |||||
| # TODO: Get the value from config | |||||
| content_folder = self.site_path.child_folder('content') | |||||
| self.content = RootNode(content_folder, self) | |||||
| self.node_map = {} | |||||
| self.resource_map = {} | |||||
| def build(self): | |||||
| """ | |||||
| Walks the content and media folders to build up the sitemap. | |||||
| """ | |||||
| self.content.build() | |||||
| @@ -0,0 +1,7 @@ | |||||
| {% extends "base.html" %} | |||||
| {% block main %} | |||||
| Hi! | |||||
| I am a test template to make sure jinja2 generation works well with hyde. | |||||
| {% endblock %} | |||||
| @@ -0,0 +1,9 @@ | |||||
| {% extends "blog/post.html" %} | |||||
| {% block article %} | |||||
| {% lipsum n=10 %} | |||||
| {% endblock %} | |||||
| {% block aside %} | |||||
| {% lipsum n=2 %} | |||||
| {% endblock %} | |||||
| @@ -0,0 +1,4 @@ | |||||
| author: Lakshmi Vyasarajan | |||||
| description: A test layout for hyde. | |||||
| template: jinja2 (2.6) | |||||
| version: 0.1 | |||||
| @@ -0,0 +1,56 @@ | |||||
| {% extends "root.html" %} | |||||
| {% block all %} | |||||
| <!doctype html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| {% block starthead %}{% endblock starthead %} | |||||
| <meta charset="{{page.meta.charset|default('utf-8')}}"> | |||||
| <meta http-equiv="X-UA-Compatible" content="{{page.meta.compatibility|default('IE=edge,chrome=1')"> | |||||
| <title>{% block title %}{{page.meta.title}}{% endblock %}</title> | |||||
| <meta name="description" content="{{page.meta.description}}"> | |||||
| <meta name="author" content="{{page.meta.author}}"> | |||||
| <!-- Mobile viewport optimized: j.mp/bplateviewport --> | |||||
| <meta name="viewport" content="{{page.meta.viewport|default('width=device-width, initial-scale=1.0')"> | |||||
| {% block favicons %} | |||||
| <!-- Place favicon.ico & apple-touch-icon.png in the root of your domain and delete these references --> | |||||
| <link rel="shortcut icon" href="{% media '/favicon.ico' %}"> | |||||
| <link rel="apple-touch-icon" href="{% media '/apple-touch-icon.png' %}"> | |||||
| {% endblock favicons %} | |||||
| {% block css %} | |||||
| <link rel="stylesheet" href="{% media 'css/site.css' %}"> | |||||
| {% endblock css %} | |||||
| {% block endhead %}{% endblock endhead %} | |||||
| </head> | |||||
| <body id="{{page.id if page.id else page.name_without_extension}}"> | |||||
| {% block content %} | |||||
| <div id="container"> | |||||
| {% block container %} | |||||
| <header> | |||||
| {% block header %}{% endblock header %} | |||||
| </header> | |||||
| <div id="main" role="main"> | |||||
| {% block main %}{% endblock main %} | |||||
| </div> | |||||
| <footer> | |||||
| {% block footer %}{% endblock %} | |||||
| </footer> | |||||
| {% endblock container%} | |||||
| </div> <!--! end of #container --> | |||||
| {% endblock content%} | |||||
| {% block js %} | |||||
| <!-- Javascript at the bottom for fast page loading --> | |||||
| {% block jquery %} | |||||
| <!-- Grab Google CDN's jQuery. fall back to local if necessary --> | |||||
| <script src="//ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.js"></script> | |||||
| {% endblock jquery %} | |||||
| {% block scripts %} | |||||
| </body> | |||||
| </html> | |||||
| {% endblock all %} | |||||
| @@ -0,0 +1,10 @@ | |||||
| {% extends "base.html" %} | |||||
| {% block main %} | |||||
| <article> | |||||
| {% block article %}{% endblock %} | |||||
| </article> | |||||
| <aside> | |||||
| {% block aside %}{% endblock %} | |||||
| </aside> | |||||
| {% endblock %} | |||||
| @@ -0,0 +1 @@ | |||||
| {% block all %}{% endblock %} | |||||
| @@ -0,0 +1,4 @@ | |||||
| body{ | |||||
| margin: 0 auto; | |||||
| width: 960px; | |||||
| } | |||||
| @@ -0,0 +1,9 @@ | |||||
| site: | |||||
| mode: development | |||||
| media: | |||||
| root: | |||||
| path: media # Relative path from site root (the directory where this file exists) | |||||
| url: /media | |||||
| widgets: | |||||
| plugins: | |||||
| aggregators: | |||||
| @@ -22,6 +22,16 @@ def test_name(): | |||||
| f = FS(__file__) | f = FS(__file__) | ||||
| assert f.name == os.path.basename(__file__) | assert f.name == os.path.basename(__file__) | ||||
| def test_equals(): | |||||
| f = FS('/blog/2010/december') | |||||
| g = FS('/blog/2010/december') | |||||
| h = FS('/blog/2010/december/') | |||||
| i = FS('/blog/2010/november') | |||||
| assert f == f.path | |||||
| assert f == g | |||||
| assert f == h | |||||
| assert f != i | |||||
| def test_name_without_extension(): | def test_name_without_extension(): | ||||
| f = File(__file__) | f = File(__file__) | ||||
| assert f.name_without_extension == "test_fs" | assert f.name_without_extension == "test_fs" | ||||
| @@ -108,16 +118,23 @@ def test_ancestors_stop(): | |||||
| depth = 0 | depth = 0 | ||||
| next = JINJA2 | next = JINJA2 | ||||
| for folder in INDEX.ancestors(stop=TEMPLATE_ROOT.parent): | for folder in INDEX.ancestors(stop=TEMPLATE_ROOT.parent): | ||||
| print folder | |||||
| assert folder == next | assert folder == next | ||||
| depth += 1 | depth += 1 | ||||
| next = folder.parent | next = folder.parent | ||||
| assert depth == 2 | assert depth == 2 | ||||
| def test_is_descendant_of(): | |||||
| assert INDEX.is_descendant_of(JINJA2) | |||||
| print "*" | |||||
| assert JINJA2.is_descendant_of(TEMPLATE_ROOT) | |||||
| print "*" | |||||
| assert INDEX.is_descendant_of(TEMPLATE_ROOT) | |||||
| print "*" | |||||
| assert not INDEX.is_descendant_of(DATA_ROOT) | |||||
| def test_fragment(): | def test_fragment(): | ||||
| print INDEX.get_fragment(TEMPLATE_ROOT) | |||||
| assert INDEX.get_fragment(TEMPLATE_ROOT) == Folder(JINJA2.name).child(INDEX.name) | |||||
| assert INDEX.get_fragment(TEMPLATE_ROOT.parent) == Folder( | |||||
| assert INDEX.get_relative_path(TEMPLATE_ROOT) == Folder(JINJA2.name).child(INDEX.name) | |||||
| assert INDEX.get_relative_path(TEMPLATE_ROOT.parent) == Folder( | |||||
| TEMPLATE_ROOT.name).child_folder(JINJA2.name).child(INDEX.name) | TEMPLATE_ROOT.name).child_folder(JINJA2.name).child(INDEX.name) | ||||
| def test_get_mirror(): | def test_get_mirror(): | ||||
| @@ -0,0 +1,62 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| """ | |||||
| Use nose | |||||
| `$ pip install nose` | |||||
| `$ nosetests` | |||||
| """ | |||||
| from hyde.fs import File, Folder | |||||
| from hyde.site import Node, RootNode, Site | |||||
| TEST_SITE_ROOT = File(__file__).parent.child_folder('sites/test_jinja') | |||||
| def test_node_site(): | |||||
| s = Site(TEST_SITE_ROOT) | |||||
| r = RootNode(TEST_SITE_ROOT.child_folder('content'), s) | |||||
| assert r.site == s | |||||
| n = Node(r.source_folder.child_folder('blog'), r) | |||||
| assert n.site == s | |||||
| def test_node_root(): | |||||
| s = Site(TEST_SITE_ROOT) | |||||
| r = RootNode(TEST_SITE_ROOT.child_folder('content'), s) | |||||
| assert r.root == r | |||||
| n = Node(r.source_folder.child_folder('blog'), r) | |||||
| assert n.root == r | |||||
| def test_node_parent(): | |||||
| s = Site(TEST_SITE_ROOT) | |||||
| r = RootNode(TEST_SITE_ROOT.child_folder('content'), s) | |||||
| c = r.add_node(TEST_SITE_ROOT.child_folder('content/blog/2010/december')) | |||||
| assert c.parent == r.node_from_relative_path('blog/2010') | |||||
| def test_node_module(): | |||||
| s = Site(TEST_SITE_ROOT) | |||||
| r = RootNode(TEST_SITE_ROOT.child_folder('content'), s) | |||||
| assert not r.module | |||||
| n = r.add_node(TEST_SITE_ROOT.child_folder('content/blog')) | |||||
| assert n.module == n | |||||
| c = r.add_node(TEST_SITE_ROOT.child_folder('content/blog/2010/december')) | |||||
| assert c.module == n | |||||
| def test_node_relative_path(): | |||||
| s = Site(TEST_SITE_ROOT) | |||||
| r = RootNode(TEST_SITE_ROOT.child_folder('content'), s) | |||||
| assert not r.module | |||||
| n = r.add_node(TEST_SITE_ROOT.child_folder('content/blog')) | |||||
| assert n.relative_path == 'blog' | |||||
| c = r.add_node(TEST_SITE_ROOT.child_folder('content/blog/2010/december')) | |||||
| assert c.relative_path == 'blog/2010/december' | |||||
| def test_build(): | |||||
| s = Site(TEST_SITE_ROOT) | |||||
| s.build() | |||||
| path = 'blog/2010/december' | |||||
| node = s.content.node_from_relative_path(path) | |||||
| assert node | |||||
| assert Folder(node.relative_path) == Folder(path) | |||||
| path += '/merry-christmas.html' | |||||
| resource = s.content.resource_from_relative_path(path) | |||||
| assert resource | |||||
| assert resource.relative_path == path | |||||
| assert not s.content.resource_from_relative_path('/happy-festivus.html') | |||||
| @@ -1,4 +1,9 @@ | |||||
| from django.utils.html import strip_spaces_between_tags | |||||
| def strip_spaces_between_tags(value): | |||||
| """ | |||||
| Stolen from `django.util.html` | |||||
| Returns the given HTML with spaces between tags removed. | |||||
| """ | |||||
| return re.sub(r'>\s+<', '><', force_unicode(value)) | |||||
| def assert_html_equals(expected, actual, sanitize=None): | def assert_html_equals(expected, actual, sanitize=None): | ||||
| expected = strip_spaces_between_tags(expected.strip()) | expected = strip_spaces_between_tags(expected.strip()) | ||||