| @@ -18,5 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||
| THE SOFTWARE. | |||
| THE SOFTWARE. | |||
| @@ -2,10 +2,15 @@ | |||
| """ | |||
| Implements the hyde entry point commands | |||
| """ | |||
| import sys | |||
| from commando import Application, command, subcommand, param | |||
| from version import __version__ | |||
| from commando import * | |||
| from hyde.exceptions import HydeException | |||
| from hyde.fs import File, Folder | |||
| from hyde.layout import Layout | |||
| from hyde.version import __version__ | |||
| import os | |||
| HYDE_LAYOUTS = "HYDE_LAYOUTS" | |||
| class Engine(Application): | |||
| """ | |||
| @@ -14,9 +19,9 @@ class Engine(Application): | |||
| @command(description='hyde - a python static website generator', | |||
| epilog='Use %(prog)s {command} -h to get help on individual commands') | |||
| @param('-v', '--version', action='version', version='%(prog)s ' + __version__) | |||
| @param('-s', '--sitepath', action='store', default='.', help="Location of the hyde site") | |||
| def main(self, params): | |||
| @version('-v', '--version', version='%(prog)s ' + __version__) | |||
| @store('-s', '--sitepath', default='.', help="Location of the hyde site") | |||
| def main(self, args): | |||
| """ | |||
| Will not be executed. A sub command is required. This function exists to provide | |||
| common parameters for the subcommands and some generic stuff like version and | |||
| @@ -25,24 +30,21 @@ class Engine(Application): | |||
| pass | |||
| @subcommand('init', help='Create a new hyde site') | |||
| @param('-t', '--template', action='store', default='basic', dest='template', | |||
| help='Overwrite the current site if it exists') | |||
| @param('-f', '--force', action='store_true', default=False, dest='overwrite', | |||
| help='Overwrite the current site if it exists') | |||
| def init(self, params): | |||
| @store('-l', '--layout', default='basic', help='Layout for the new site') | |||
| @true('-f', '--force', default=False, dest='overwrite', | |||
| help='Overwrite the current site if it exists') | |||
| def init(self, args): | |||
| """ | |||
| The initialize command. Creates a new site from the template at the given | |||
| sitepath. | |||
| """ | |||
| print params.sitepath | |||
| print params.template | |||
| print params.overwrite | |||
| def start(self): | |||
| """ | |||
| main() | |||
| """ | |||
| args = self.parse(sys.argv[1:]) | |||
| self.run(args) | |||
| sitepath = File(args.sitepath) | |||
| if sitepath.exists and not args.overwrite: | |||
| raise HydeException("The given site path[%s] is not empty" % sitepath) | |||
| layout = Layout.find_layout(args.layout) | |||
| if not layout.exists: | |||
| raise HydeException( | |||
| "The given layout is invalid. Please check if you have the `layout` " | |||
| "is in the right place and the environment variable has been setup" | |||
| "properly") | |||
| layout.copy_to(args.sitepath) | |||
| @@ -0,0 +1,5 @@ | |||
| class HydeException(Exception): | |||
| """ | |||
| Base class for exceptions from hyde | |||
| """ | |||
| pass | |||
| @@ -1,14 +1,15 @@ | |||
| # -*- coding: utf-8 -*- | |||
| """ | |||
| Unified object oriented interface for interacting with file system objects. File system operations in | |||
| python are distributed across modules: os, os.path, fnamtch, shutil and distutils. This module attempts | |||
| to make the right choices for common operations to provide a single interface. | |||
| Unified object oriented interface for interacting with file system objects. | |||
| File system operations in python are distributed across modules: os, os.path, | |||
| fnamtch, shutil and distutils. This module attempts to make the right choices | |||
| for common operations to provide a single interface. | |||
| """ | |||
| import codecs | |||
| # import fnmatch | |||
| import os | |||
| # import shutil | |||
| import shutil | |||
| # from datetime import datetime | |||
| # pylint: disable-msg=E0611 | |||
| @@ -28,6 +29,19 @@ class FS(object): | |||
| def __repr__(self): | |||
| return self.path | |||
| def __eq__(self, other): | |||
| return str(self) == str(other) | |||
| def __ne__(self, other): | |||
| return str(self) != str(other) | |||
| @property | |||
| def exists(self): | |||
| """ | |||
| Does the file system object exist? | |||
| """ | |||
| return os.path.exists(self.path) | |||
| @property | |||
| def name(self): | |||
| """ | |||
| @@ -42,6 +56,27 @@ class FS(object): | |||
| """ | |||
| return Folder(os.path.dirname(self.path)) | |||
| @staticmethod | |||
| def file_or_folder(path): | |||
| """ | |||
| Returns a File or Folder object that would represent the given path. | |||
| """ | |||
| target = str(path) | |||
| return Folder(target) if os.path.isdir(target) else File(target) | |||
| 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)): | |||
| return FS.file_or_folder(Folder(destination).child(self.name)) | |||
| else: | |||
| return destination | |||
| class File(FS): | |||
| """ | |||
| The File object. | |||
| @@ -85,6 +120,16 @@ class File(FS): | |||
| with codecs.open(self.path, 'w', encoding) as fout: | |||
| fout.write(text) | |||
| 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) | |||
| class Folder(FS): | |||
| """ | |||
| Represents a directory. | |||
| @@ -102,4 +147,32 @@ class Folder(FS): | |||
| """ | |||
| Returns a path of a child item represented by `name`. | |||
| """ | |||
| return os.path.join(self.path, name) | |||
| return os.path.join(self.path, name) | |||
| 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 delete(self): | |||
| """ | |||
| Deletes the directory if it exists. | |||
| """ | |||
| if self.exists: | |||
| shutil.rmtree(self.path) | |||
| def copy_to(self, destination): | |||
| """ | |||
| Copies this directory to the given destination. Returns a Folder object | |||
| that represents the moved directory. | |||
| """ | |||
| target = self.__get_destination__(destination) | |||
| shutil.copytree(self.path, str(target)) | |||
| return target | |||
| @@ -1 +1,40 @@ | |||
| # -*- coding: utf-8 -*- | |||
| # -*- coding: utf-8 -*- | |||
| """ | |||
| Classes, functions and utilties related to hyde layouts | |||
| """ | |||
| import os | |||
| from hyde.fs import File, Folder | |||
| HYDE_DATA = "HYDE_DATA" | |||
| LAYOUTS = "layouts" | |||
| class Layout(object): | |||
| """ | |||
| Represents a layout package | |||
| """ | |||
| @staticmethod | |||
| def find_layout(layout_name="basic"): | |||
| """ | |||
| Find the layout with a given name. | |||
| Search order: | |||
| 1. env(HYDE_DATA) | |||
| 2. <hyde script path>/layouts/ | |||
| """ | |||
| layout_folder = None | |||
| if HYDE_DATA in os.environ: | |||
| layout_folder = Layout._get_layout_folder(os.environ[HYDE_DATA], layout_name) | |||
| if not layout_folder: | |||
| layout_folder = Layout._get_layout_folder(File(__file__).parent, layout_name) | |||
| return layout_folder | |||
| @staticmethod | |||
| def _get_layout_folder(root, layout_name="basic"): | |||
| """ | |||
| Finds the layout folder from the given root folder. | |||
| If it does not exist, return None | |||
| """ | |||
| layout_folder = Folder(str(root)).child_folder(LAYOUTS).child_folder(layout_name) | |||
| return layout_folder if layout_folder.exists else None | |||
| @@ -3,7 +3,7 @@ | |||
| """ | |||
| The hyde executable | |||
| """ | |||
| from engine import Engine | |||
| from hyde.engine import Engine | |||
| if __name__ == "__main__": | |||
| Engine().start() | |||
| Engine().run() | |||
| @@ -1 +0,0 @@ | |||
| åßcdeƒ | |||
| @@ -8,7 +8,9 @@ Use nose | |||
| from hyde.fs import FS, File, Folder | |||
| import codecs | |||
| import os | |||
| import shutil | |||
| from nose.tools import raises, with_setup, nottest | |||
| def test_representation(): | |||
| f = FS(__file__) | |||
| @@ -49,9 +51,76 @@ def test_child_folder(): | |||
| assert hasattr(c, 'child_folder') | |||
| assert str(c) == os.path.join(os.path.dirname(__file__), 'data') | |||
| def test_exists(): | |||
| p = FS(__file__) | |||
| assert p.exists | |||
| p = FS(__file__ + "_some_junk") | |||
| assert not p.exists | |||
| f = FS(__file__).parent.parent | |||
| assert f.exists | |||
| f = FS(__file__).parent.child_folder('templates') | |||
| assert f.exists | |||
| def test_create_folder(): | |||
| f = FS(__file__).parent | |||
| assert f.exists | |||
| f.make() | |||
| assert True # No Exceptions | |||
| c = f.child_folder('__test__') | |||
| assert not c.exists | |||
| c.make() | |||
| assert c.exists | |||
| shutil.rmtree(str(c)) | |||
| assert not c.exists | |||
| def test_remove_folder(): | |||
| f = FS(__file__).parent | |||
| c = f.child_folder('__test__') | |||
| assert not c.exists | |||
| c.make() | |||
| assert c.exists | |||
| c.delete() | |||
| assert not c.exists | |||
| def test_file_or_folder(): | |||
| f = FS.file_or_folder(__file__) | |||
| assert isinstance(f, File) | |||
| f = FS.file_or_folder(File(__file__).parent) | |||
| assert isinstance(f, Folder) | |||
| DATA_ROOT = File(__file__).parent.child_folder('data') | |||
| TEMPLATE_ROOT = File(__file__).parent.child_folder('templates') | |||
| JINJA2 = TEMPLATE_ROOT.child_folder('jinja2') | |||
| HELPERS = File(JINJA2.child('helpers.html')) | |||
| INDEX = File(JINJA2.child('index.html')) | |||
| LAYOUT = File(JINJA2.child('layout.html')) | |||
| @nottest | |||
| def setup_data(): | |||
| DATA_ROOT.make() | |||
| @nottest | |||
| def cleanup_data(): | |||
| DATA_ROOT.delete() | |||
| @with_setup(setup_data, cleanup_data) | |||
| def test_copy_file(): | |||
| DATA_HELPERS = File(DATA_ROOT.child(HELPERS.name)) | |||
| assert not DATA_HELPERS.exists | |||
| HELPERS.copy_to(DATA_ROOT) | |||
| assert DATA_HELPERS.exists | |||
| @with_setup(setup_data, cleanup_data) | |||
| def test_copy_folder(): | |||
| assert DATA_ROOT.exists | |||
| DATA_JINJA2 = DATA_ROOT.child_folder(JINJA2.name) | |||
| assert not DATA_JINJA2.exists | |||
| JINJA2.copy_to(DATA_ROOT) | |||
| assert DATA_JINJA2.exists | |||
| for f in [HELPERS, INDEX, LAYOUT]: | |||
| assert File(DATA_JINJA2.child(f.name)).exists | |||
| @with_setup(setup_data, cleanup_data) | |||
| def test_read_all(): | |||
| utxt = u'åßcdeƒ' | |||
| path = DATA_ROOT.child('unicode.txt') | |||
| @@ -60,7 +129,8 @@ def test_read_all(): | |||
| txt = File(path).read_all() | |||
| assert txt == utxt | |||
| @with_setup(setup_data, cleanup_data) | |||
| def test_write(): | |||
| utxt = u'åßcdeƒ' | |||
| path = DATA_ROOT.child('unicode.txt') | |||
| @@ -0,0 +1,45 @@ | |||
| # -*- coding: utf-8 -*- | |||
| """ | |||
| Use nose | |||
| `$ pip install nose` | |||
| `$ nosetests` | |||
| """ | |||
| from hyde.engine import Engine | |||
| from hyde.exceptions import HydeException | |||
| from hyde.fs import FS, File, Folder | |||
| from nose.tools import raises, with_setup, nottest | |||
| TEST_SITE = File(__file__).parent.child_folder('_test') | |||
| @nottest | |||
| def create_test_site(): | |||
| TEST_SITE.make() | |||
| @nottest | |||
| def delete_test_site(): | |||
| TEST_SITE.delete() | |||
| @raises(HydeException) | |||
| @with_setup(create_test_site, delete_test_site) | |||
| def test_ensure_exception_when_sitepath_exists(): | |||
| e = Engine() | |||
| e.run(e.parse(['-s', str(TEST_SITE), 'init'])) | |||
| @with_setup(create_test_site, delete_test_site) | |||
| def test_ensure_no_exception_when_sitepath_exists_when_forced(): | |||
| e = Engine() | |||
| e.run(e.parse(['-s', str(TEST_SITE), 'init', '-f'])) | |||
| assert True #No Exception | |||
| @with_setup(create_test_site, delete_test_site) | |||
| def test_ensure_no_exception_when_sitepath_does_not_exist(): | |||
| e = Engine() | |||
| TEST_SITE.delete() | |||
| e.run(e.parse(['-s', str(TEST_SITE), 'init', '-f'])) | |||
| assert TEST_SITE.exists | |||
| assert TEST_SITE.child_folder('layout').exists | |||
| assert File(TEST_SITE.child('info.yaml')).exists | |||
| @@ -0,0 +1,39 @@ | |||
| # -*- coding: utf-8 -*- | |||
| """ | |||
| Use nose | |||
| `$ pip install nose` | |||
| `$ nosetests` | |||
| """ | |||
| from hyde.fs import File, Folder | |||
| from hyde.layout import Layout, HYDE_DATA, LAYOUTS | |||
| from nose.tools import raises, with_setup, nottest | |||
| import os | |||
| DATA_ROOT = File(__file__).parent.child_folder('data') | |||
| LAYOUT_ROOT = DATA_ROOT.child_folder(LAYOUTS) | |||
| @nottest | |||
| def setup_data(): | |||
| DATA_ROOT.make() | |||
| @nottest | |||
| def cleanup_data(): | |||
| DATA_ROOT.delete() | |||
| def test_find_layout_from_package_dir(): | |||
| f = Layout.find_layout() | |||
| assert f.name == 'basic' | |||
| assert f.child_folder('layout').exists | |||
| @with_setup(setup_data, cleanup_data) | |||
| def test_find_layout_from_env_var(): | |||
| f = Layout.find_layout() | |||
| LAYOUT_ROOT.make() | |||
| f.copy_to(LAYOUT_ROOT) | |||
| os.environ[HYDE_DATA] = str(DATA_ROOT) | |||
| f = Layout.find_layout() | |||
| assert f.parent == LAYOUT_ROOT | |||
| assert f.name == 'basic' | |||
| assert f.child_folder('layout').exists | |||
| del os.environ[HYDE_DATA] | |||