| @@ -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 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | 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 | 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 | 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): | class Engine(Application): | ||||
| """ | """ | ||||
| @@ -14,9 +19,9 @@ class Engine(Application): | |||||
| @command(description='hyde - a python static website generator', | @command(description='hyde - a python static website generator', | ||||
| epilog='Use %(prog)s {command} -h to get help on individual commands') | 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 | 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 | common parameters for the subcommands and some generic stuff like version and | ||||
| @@ -25,24 +30,21 @@ class Engine(Application): | |||||
| pass | pass | ||||
| @subcommand('init', help='Create a new hyde site') | @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 | The initialize command. Creates a new site from the template at the given | ||||
| sitepath. | 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 -*- | # -*- 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 codecs | ||||
| # import fnmatch | # import fnmatch | ||||
| import os | import os | ||||
| # import shutil | |||||
| import shutil | |||||
| # from datetime import datetime | # from datetime import datetime | ||||
| # pylint: disable-msg=E0611 | # pylint: disable-msg=E0611 | ||||
| @@ -28,6 +29,19 @@ class FS(object): | |||||
| def __repr__(self): | def __repr__(self): | ||||
| return self.path | 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 | @property | ||||
| def name(self): | def name(self): | ||||
| """ | """ | ||||
| @@ -42,6 +56,27 @@ class FS(object): | |||||
| """ | """ | ||||
| return Folder(os.path.dirname(self.path)) | 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): | class File(FS): | ||||
| """ | """ | ||||
| The File object. | The File object. | ||||
| @@ -85,6 +120,16 @@ class File(FS): | |||||
| with codecs.open(self.path, 'w', encoding) as fout: | with codecs.open(self.path, 'w', encoding) as fout: | ||||
| fout.write(text) | 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): | class Folder(FS): | ||||
| """ | """ | ||||
| Represents a directory. | Represents a directory. | ||||
| @@ -102,4 +147,32 @@ class Folder(FS): | |||||
| """ | """ | ||||
| Returns a path of a child item represented by `name`. | 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 | The hyde executable | ||||
| """ | """ | ||||
| from engine import Engine | |||||
| from hyde.engine import Engine | |||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||
| Engine().start() | |||||
| Engine().run() | |||||
| @@ -1 +0,0 @@ | |||||
| ĂĄĂźcdeĆ’ | |||||
| @@ -8,7 +8,9 @@ Use nose | |||||
| from hyde.fs import FS, File, Folder | from hyde.fs import FS, File, Folder | ||||
| import codecs | import codecs | ||||
| import os | import os | ||||
| import shutil | |||||
| from nose.tools import raises, with_setup, nottest | |||||
| def test_representation(): | def test_representation(): | ||||
| f = FS(__file__) | f = FS(__file__) | ||||
| @@ -49,9 +51,76 @@ def test_child_folder(): | |||||
| assert hasattr(c, 'child_folder') | assert hasattr(c, 'child_folder') | ||||
| assert str(c) == os.path.join(os.path.dirname(__file__), 'data') | 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') | 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(): | def test_read_all(): | ||||
| utxt = u'ĂĄĂźcdeĆ’' | utxt = u'ĂĄĂźcdeĆ’' | ||||
| path = DATA_ROOT.child('unicode.txt') | path = DATA_ROOT.child('unicode.txt') | ||||
| @@ -60,7 +129,8 @@ def test_read_all(): | |||||
| txt = File(path).read_all() | txt = File(path).read_all() | ||||
| assert txt == utxt | assert txt == utxt | ||||
| @with_setup(setup_data, cleanup_data) | |||||
| def test_write(): | def test_write(): | ||||
| utxt = u'ĂĄĂźcdeĆ’' | utxt = u'ĂĄĂźcdeĆ’' | ||||
| path = DATA_ROOT.child('unicode.txt') | 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] | |||||