@@ -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] |