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