| @@ -14,3 +14,4 @@ build | |||||
| .idea | .idea | ||||
| PYSMELLTAGS | PYSMELLTAGS | ||||
| .noseids | .noseids | ||||
| *.tar.gz | |||||
| @@ -0,0 +1,6 @@ | |||||
| exclude *.pyc .DS_Store .gitignore .noseids MANIFEST.in | |||||
| include setup.py | |||||
| include distribute_setup.py | |||||
| recursive-include hyde *.py | |||||
| recursive-include hyde/tests *.py | |||||
| recursive-include hyde/layouts *.* | |||||
| @@ -1,13 +1,14 @@ | |||||
| Version 0.7b1 | |||||
| # A brand new **hyde** | # A brand new **hyde** | ||||
| This is the new version of hyde under active development. | |||||
| Incomplete documentation can be found [here][hydedocs]. | |||||
| [This][hyde1-0] should give a good understanding of the motivation behind this | |||||
| version. You can also take a look at the [cloudpanic source][cp] for a | |||||
| reference implementation. | |||||
| This is the new version of hyde under active development. Incomplete documentation | |||||
| can be found [here][hydedocs]. [This][hyde1-0] should give a good understanding of | |||||
| the motivation behind this version. You can also take a look at the | |||||
| [documentation source][docs] for a reference implementation. | |||||
| [hyde1-0]: http://groups.google.com/group/hyde-dev/web/hyde-1-0 | [hyde1-0]: http://groups.google.com/group/hyde-dev/web/hyde-1-0 | ||||
| [cp]: http://github.com/tipiirai/cloudpanic/tree/refactor | |||||
| [docs]: https://github.com/hyde/hyde/tree/master/hyde/layouts/doc | |||||
| [hydedocs]: http://hyde.github.com/overview | [hydedocs]: http://hyde.github.com/overview | ||||
| [Here](http://groups.google.com/group/hyde-dev/browse_thread/thread/2a143bd2081b3322) is | [Here](http://groups.google.com/group/hyde-dev/browse_thread/thread/2a143bd2081b3322) is | ||||
| @@ -15,31 +16,22 @@ the initial announcement of the project. | |||||
| ## Installation | ## Installation | ||||
| Hyde supports both python 2.7 and 2.6. | |||||
| pip install -r req-2.6.txt | |||||
| or | |||||
| pip install -r req-2.7.txt | |||||
| To get the latest released version: | |||||
| will install all the dependencies of hyde. | |||||
| pip install hyde | |||||
| You can choose to install hyde by running | |||||
| For the current trunk: | |||||
| python setup.py install | |||||
| pip install -e git://github.com/hyde/hyde.git#egg=hyde | |||||
| ## Creating a new hyde site | ## Creating a new hyde site | ||||
| The new version of Hyde uses the `argparse` module and hence support subcommands. | |||||
| The following command: | |||||
| hyde -s ~/test_site create -l test | |||||
| hyde -s ~/test_site create -l doc | |||||
| will create a new hyde site using the test layout. | will create a new hyde site using the test layout. | ||||
| ## Generating the hyde site | ## Generating the hyde site | ||||
| cd ~/test_site | cd ~/test_site | ||||
| @@ -56,6 +48,11 @@ The server also regenerates on demand. As long as the server is running, | |||||
| you can make changes to your source and refresh the browser to view the changes. | you can make changes to your source and refresh the browser to view the changes. | ||||
| ## Examples | |||||
| 1. [Cloudpanic](https://github.com/tipiirai/cloudpanic) | |||||
| 2. [Ringce](https://github.com/lakshmivyas/ringce/tree/v3.0) | |||||
| ## A brief list of features | ## A brief list of features | ||||
| @@ -0,0 +1,485 @@ | |||||
| #!python | |||||
| """Bootstrap distribute installation | |||||
| If you want to use setuptools in your package's setup.py, just include this | |||||
| file in the same directory with it, and add this to the top of your setup.py:: | |||||
| from distribute_setup import use_setuptools | |||||
| use_setuptools() | |||||
| If you want to require a specific version of setuptools, set a download | |||||
| mirror, or use an alternate download directory, you can do so by supplying | |||||
| the appropriate options to ``use_setuptools()``. | |||||
| This file can also be run as a script to install or upgrade setuptools. | |||||
| """ | |||||
| import os | |||||
| import sys | |||||
| import time | |||||
| import fnmatch | |||||
| import tempfile | |||||
| import tarfile | |||||
| from distutils import log | |||||
| try: | |||||
| from site import USER_SITE | |||||
| except ImportError: | |||||
| USER_SITE = None | |||||
| try: | |||||
| import subprocess | |||||
| def _python_cmd(*args): | |||||
| args = (sys.executable,) + args | |||||
| return subprocess.call(args) == 0 | |||||
| except ImportError: | |||||
| # will be used for python 2.3 | |||||
| def _python_cmd(*args): | |||||
| args = (sys.executable,) + args | |||||
| # quoting arguments if windows | |||||
| if sys.platform == 'win32': | |||||
| def quote(arg): | |||||
| if ' ' in arg: | |||||
| return '"%s"' % arg | |||||
| return arg | |||||
| args = [quote(arg) for arg in args] | |||||
| return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 | |||||
| DEFAULT_VERSION = "0.6.14" | |||||
| DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" | |||||
| SETUPTOOLS_FAKED_VERSION = "0.6c11" | |||||
| SETUPTOOLS_PKG_INFO = """\ | |||||
| Metadata-Version: 1.0 | |||||
| Name: setuptools | |||||
| Version: %s | |||||
| Summary: xxxx | |||||
| Home-page: xxx | |||||
| Author: xxx | |||||
| Author-email: xxx | |||||
| License: xxx | |||||
| Description: xxx | |||||
| """ % SETUPTOOLS_FAKED_VERSION | |||||
| def _install(tarball): | |||||
| # extracting the tarball | |||||
| tmpdir = tempfile.mkdtemp() | |||||
| log.warn('Extracting in %s', tmpdir) | |||||
| old_wd = os.getcwd() | |||||
| try: | |||||
| os.chdir(tmpdir) | |||||
| tar = tarfile.open(tarball) | |||||
| _extractall(tar) | |||||
| tar.close() | |||||
| # going in the directory | |||||
| subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) | |||||
| os.chdir(subdir) | |||||
| log.warn('Now working in %s', subdir) | |||||
| # installing | |||||
| log.warn('Installing Distribute') | |||||
| if not _python_cmd('setup.py', 'install'): | |||||
| log.warn('Something went wrong during the installation.') | |||||
| log.warn('See the error message above.') | |||||
| finally: | |||||
| os.chdir(old_wd) | |||||
| def _build_egg(egg, tarball, to_dir): | |||||
| # extracting the tarball | |||||
| tmpdir = tempfile.mkdtemp() | |||||
| log.warn('Extracting in %s', tmpdir) | |||||
| old_wd = os.getcwd() | |||||
| try: | |||||
| os.chdir(tmpdir) | |||||
| tar = tarfile.open(tarball) | |||||
| _extractall(tar) | |||||
| tar.close() | |||||
| # going in the directory | |||||
| subdir = os.path.join(tmpdir, os.listdir(tmpdir)[0]) | |||||
| os.chdir(subdir) | |||||
| log.warn('Now working in %s', subdir) | |||||
| # building an egg | |||||
| log.warn('Building a Distribute egg in %s', to_dir) | |||||
| _python_cmd('setup.py', '-q', 'bdist_egg', '--dist-dir', to_dir) | |||||
| finally: | |||||
| os.chdir(old_wd) | |||||
| # returning the result | |||||
| log.warn(egg) | |||||
| if not os.path.exists(egg): | |||||
| raise IOError('Could not build the egg.') | |||||
| def _do_download(version, download_base, to_dir, download_delay): | |||||
| egg = os.path.join(to_dir, 'distribute-%s-py%d.%d.egg' | |||||
| % (version, sys.version_info[0], sys.version_info[1])) | |||||
| if not os.path.exists(egg): | |||||
| tarball = download_setuptools(version, download_base, | |||||
| to_dir, download_delay) | |||||
| _build_egg(egg, tarball, to_dir) | |||||
| sys.path.insert(0, egg) | |||||
| import setuptools | |||||
| setuptools.bootstrap_install_from = egg | |||||
| def use_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, | |||||
| to_dir=os.curdir, download_delay=15, no_fake=True): | |||||
| # making sure we use the absolute path | |||||
| to_dir = os.path.abspath(to_dir) | |||||
| was_imported = 'pkg_resources' in sys.modules or \ | |||||
| 'setuptools' in sys.modules | |||||
| try: | |||||
| try: | |||||
| import pkg_resources | |||||
| if not hasattr(pkg_resources, '_distribute'): | |||||
| if not no_fake: | |||||
| _fake_setuptools() | |||||
| raise ImportError | |||||
| except ImportError: | |||||
| return _do_download(version, download_base, to_dir, download_delay) | |||||
| try: | |||||
| pkg_resources.require("distribute>="+version) | |||||
| return | |||||
| except pkg_resources.VersionConflict: | |||||
| e = sys.exc_info()[1] | |||||
| if was_imported: | |||||
| sys.stderr.write( | |||||
| "The required version of distribute (>=%s) is not available,\n" | |||||
| "and can't be installed while this script is running. Please\n" | |||||
| "install a more recent version first, using\n" | |||||
| "'easy_install -U distribute'." | |||||
| "\n\n(Currently using %r)\n" % (version, e.args[0])) | |||||
| sys.exit(2) | |||||
| else: | |||||
| del pkg_resources, sys.modules['pkg_resources'] # reload ok | |||||
| return _do_download(version, download_base, to_dir, | |||||
| download_delay) | |||||
| except pkg_resources.DistributionNotFound: | |||||
| return _do_download(version, download_base, to_dir, | |||||
| download_delay) | |||||
| finally: | |||||
| if not no_fake: | |||||
| _create_fake_setuptools_pkg_info(to_dir) | |||||
| def download_setuptools(version=DEFAULT_VERSION, download_base=DEFAULT_URL, | |||||
| to_dir=os.curdir, delay=15): | |||||
| """Download distribute from a specified location and return its filename | |||||
| `version` should be a valid distribute version number that is available | |||||
| as an egg for download under the `download_base` URL (which should end | |||||
| with a '/'). `to_dir` is the directory where the egg will be downloaded. | |||||
| `delay` is the number of seconds to pause before an actual download | |||||
| attempt. | |||||
| """ | |||||
| # making sure we use the absolute path | |||||
| to_dir = os.path.abspath(to_dir) | |||||
| try: | |||||
| from urllib.request import urlopen | |||||
| except ImportError: | |||||
| from urllib2 import urlopen | |||||
| tgz_name = "distribute-%s.tar.gz" % version | |||||
| url = download_base + tgz_name | |||||
| saveto = os.path.join(to_dir, tgz_name) | |||||
| src = dst = None | |||||
| if not os.path.exists(saveto): # Avoid repeated downloads | |||||
| try: | |||||
| log.warn("Downloading %s", url) | |||||
| src = urlopen(url) | |||||
| # Read/write all in one block, so we don't create a corrupt file | |||||
| # if the download is interrupted. | |||||
| data = src.read() | |||||
| dst = open(saveto, "wb") | |||||
| dst.write(data) | |||||
| finally: | |||||
| if src: | |||||
| src.close() | |||||
| if dst: | |||||
| dst.close() | |||||
| return os.path.realpath(saveto) | |||||
| def _no_sandbox(function): | |||||
| def __no_sandbox(*args, **kw): | |||||
| try: | |||||
| from setuptools.sandbox import DirectorySandbox | |||||
| if not hasattr(DirectorySandbox, '_old'): | |||||
| def violation(*args): | |||||
| pass | |||||
| DirectorySandbox._old = DirectorySandbox._violation | |||||
| DirectorySandbox._violation = violation | |||||
| patched = True | |||||
| else: | |||||
| patched = False | |||||
| except ImportError: | |||||
| patched = False | |||||
| try: | |||||
| return function(*args, **kw) | |||||
| finally: | |||||
| if patched: | |||||
| DirectorySandbox._violation = DirectorySandbox._old | |||||
| del DirectorySandbox._old | |||||
| return __no_sandbox | |||||
| def _patch_file(path, content): | |||||
| """Will backup the file then patch it""" | |||||
| existing_content = open(path).read() | |||||
| if existing_content == content: | |||||
| # already patched | |||||
| log.warn('Already patched.') | |||||
| return False | |||||
| log.warn('Patching...') | |||||
| _rename_path(path) | |||||
| f = open(path, 'w') | |||||
| try: | |||||
| f.write(content) | |||||
| finally: | |||||
| f.close() | |||||
| return True | |||||
| _patch_file = _no_sandbox(_patch_file) | |||||
| def _same_content(path, content): | |||||
| return open(path).read() == content | |||||
| def _rename_path(path): | |||||
| new_name = path + '.OLD.%s' % time.time() | |||||
| log.warn('Renaming %s into %s', path, new_name) | |||||
| os.rename(path, new_name) | |||||
| return new_name | |||||
| def _remove_flat_installation(placeholder): | |||||
| if not os.path.isdir(placeholder): | |||||
| log.warn('Unkown installation at %s', placeholder) | |||||
| return False | |||||
| found = False | |||||
| for file in os.listdir(placeholder): | |||||
| if fnmatch.fnmatch(file, 'setuptools*.egg-info'): | |||||
| found = True | |||||
| break | |||||
| if not found: | |||||
| log.warn('Could not locate setuptools*.egg-info') | |||||
| return | |||||
| log.warn('Removing elements out of the way...') | |||||
| pkg_info = os.path.join(placeholder, file) | |||||
| if os.path.isdir(pkg_info): | |||||
| patched = _patch_egg_dir(pkg_info) | |||||
| else: | |||||
| patched = _patch_file(pkg_info, SETUPTOOLS_PKG_INFO) | |||||
| if not patched: | |||||
| log.warn('%s already patched.', pkg_info) | |||||
| return False | |||||
| # now let's move the files out of the way | |||||
| for element in ('setuptools', 'pkg_resources.py', 'site.py'): | |||||
| element = os.path.join(placeholder, element) | |||||
| if os.path.exists(element): | |||||
| _rename_path(element) | |||||
| else: | |||||
| log.warn('Could not find the %s element of the ' | |||||
| 'Setuptools distribution', element) | |||||
| return True | |||||
| _remove_flat_installation = _no_sandbox(_remove_flat_installation) | |||||
| def _after_install(dist): | |||||
| log.warn('After install bootstrap.') | |||||
| placeholder = dist.get_command_obj('install').install_purelib | |||||
| _create_fake_setuptools_pkg_info(placeholder) | |||||
| def _create_fake_setuptools_pkg_info(placeholder): | |||||
| if not placeholder or not os.path.exists(placeholder): | |||||
| log.warn('Could not find the install location') | |||||
| return | |||||
| pyver = '%s.%s' % (sys.version_info[0], sys.version_info[1]) | |||||
| setuptools_file = 'setuptools-%s-py%s.egg-info' % \ | |||||
| (SETUPTOOLS_FAKED_VERSION, pyver) | |||||
| pkg_info = os.path.join(placeholder, setuptools_file) | |||||
| if os.path.exists(pkg_info): | |||||
| log.warn('%s already exists', pkg_info) | |||||
| return | |||||
| log.warn('Creating %s', pkg_info) | |||||
| f = open(pkg_info, 'w') | |||||
| try: | |||||
| f.write(SETUPTOOLS_PKG_INFO) | |||||
| finally: | |||||
| f.close() | |||||
| pth_file = os.path.join(placeholder, 'setuptools.pth') | |||||
| log.warn('Creating %s', pth_file) | |||||
| f = open(pth_file, 'w') | |||||
| try: | |||||
| f.write(os.path.join(os.curdir, setuptools_file)) | |||||
| finally: | |||||
| f.close() | |||||
| _create_fake_setuptools_pkg_info = _no_sandbox(_create_fake_setuptools_pkg_info) | |||||
| def _patch_egg_dir(path): | |||||
| # let's check if it's already patched | |||||
| pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') | |||||
| if os.path.exists(pkg_info): | |||||
| if _same_content(pkg_info, SETUPTOOLS_PKG_INFO): | |||||
| log.warn('%s already patched.', pkg_info) | |||||
| return False | |||||
| _rename_path(path) | |||||
| os.mkdir(path) | |||||
| os.mkdir(os.path.join(path, 'EGG-INFO')) | |||||
| pkg_info = os.path.join(path, 'EGG-INFO', 'PKG-INFO') | |||||
| f = open(pkg_info, 'w') | |||||
| try: | |||||
| f.write(SETUPTOOLS_PKG_INFO) | |||||
| finally: | |||||
| f.close() | |||||
| return True | |||||
| _patch_egg_dir = _no_sandbox(_patch_egg_dir) | |||||
| def _before_install(): | |||||
| log.warn('Before install bootstrap.') | |||||
| _fake_setuptools() | |||||
| def _under_prefix(location): | |||||
| if 'install' not in sys.argv: | |||||
| return True | |||||
| args = sys.argv[sys.argv.index('install')+1:] | |||||
| for index, arg in enumerate(args): | |||||
| for option in ('--root', '--prefix'): | |||||
| if arg.startswith('%s=' % option): | |||||
| top_dir = arg.split('root=')[-1] | |||||
| return location.startswith(top_dir) | |||||
| elif arg == option: | |||||
| if len(args) > index: | |||||
| top_dir = args[index+1] | |||||
| return location.startswith(top_dir) | |||||
| if arg == '--user' and USER_SITE is not None: | |||||
| return location.startswith(USER_SITE) | |||||
| return True | |||||
| def _fake_setuptools(): | |||||
| log.warn('Scanning installed packages') | |||||
| try: | |||||
| import pkg_resources | |||||
| except ImportError: | |||||
| # we're cool | |||||
| log.warn('Setuptools or Distribute does not seem to be installed.') | |||||
| return | |||||
| ws = pkg_resources.working_set | |||||
| try: | |||||
| setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools', | |||||
| replacement=False)) | |||||
| except TypeError: | |||||
| # old distribute API | |||||
| setuptools_dist = ws.find(pkg_resources.Requirement.parse('setuptools')) | |||||
| if setuptools_dist is None: | |||||
| log.warn('No setuptools distribution found') | |||||
| return | |||||
| # detecting if it was already faked | |||||
| setuptools_location = setuptools_dist.location | |||||
| log.warn('Setuptools installation detected at %s', setuptools_location) | |||||
| # if --root or --preix was provided, and if | |||||
| # setuptools is not located in them, we don't patch it | |||||
| if not _under_prefix(setuptools_location): | |||||
| log.warn('Not patching, --root or --prefix is installing Distribute' | |||||
| ' in another location') | |||||
| return | |||||
| # let's see if its an egg | |||||
| if not setuptools_location.endswith('.egg'): | |||||
| log.warn('Non-egg installation') | |||||
| res = _remove_flat_installation(setuptools_location) | |||||
| if not res: | |||||
| return | |||||
| else: | |||||
| log.warn('Egg installation') | |||||
| pkg_info = os.path.join(setuptools_location, 'EGG-INFO', 'PKG-INFO') | |||||
| if (os.path.exists(pkg_info) and | |||||
| _same_content(pkg_info, SETUPTOOLS_PKG_INFO)): | |||||
| log.warn('Already patched.') | |||||
| return | |||||
| log.warn('Patching...') | |||||
| # let's create a fake egg replacing setuptools one | |||||
| res = _patch_egg_dir(setuptools_location) | |||||
| if not res: | |||||
| return | |||||
| log.warn('Patched done.') | |||||
| _relaunch() | |||||
| def _relaunch(): | |||||
| log.warn('Relaunching...') | |||||
| # we have to relaunch the process | |||||
| # pip marker to avoid a relaunch bug | |||||
| if sys.argv[:3] == ['-c', 'install', '--single-version-externally-managed']: | |||||
| sys.argv[0] = 'setup.py' | |||||
| args = [sys.executable] + sys.argv | |||||
| sys.exit(subprocess.call(args)) | |||||
| def _extractall(self, path=".", members=None): | |||||
| """Extract all members from the archive to the current working | |||||
| directory and set owner, modification time and permissions on | |||||
| directories afterwards. `path' specifies a different directory | |||||
| to extract to. `members' is optional and must be a subset of the | |||||
| list returned by getmembers(). | |||||
| """ | |||||
| import copy | |||||
| import operator | |||||
| from tarfile import ExtractError | |||||
| directories = [] | |||||
| if members is None: | |||||
| members = self | |||||
| for tarinfo in members: | |||||
| if tarinfo.isdir(): | |||||
| # Extract directories with a safe mode. | |||||
| directories.append(tarinfo) | |||||
| tarinfo = copy.copy(tarinfo) | |||||
| tarinfo.mode = 448 # decimal for oct 0700 | |||||
| self.extract(tarinfo, path) | |||||
| # Reverse sort directories. | |||||
| if sys.version_info < (2, 4): | |||||
| def sorter(dir1, dir2): | |||||
| return cmp(dir1.name, dir2.name) | |||||
| directories.sort(sorter) | |||||
| directories.reverse() | |||||
| else: | |||||
| directories.sort(key=operator.attrgetter('name'), reverse=True) | |||||
| # Set correct owner, mtime and filemode on directories. | |||||
| for tarinfo in directories: | |||||
| dirpath = os.path.join(path, tarinfo.name) | |||||
| try: | |||||
| self.chown(tarinfo, dirpath) | |||||
| self.utime(tarinfo, dirpath) | |||||
| self.chmod(tarinfo, dirpath) | |||||
| except ExtractError: | |||||
| e = sys.exc_info()[1] | |||||
| if self.errorlevel > 1: | |||||
| raise | |||||
| else: | |||||
| self._dbg(1, "tarfile: %s" % e) | |||||
| def main(argv, version=DEFAULT_VERSION): | |||||
| """Install or upgrade setuptools and EasyInstall""" | |||||
| tarball = download_setuptools() | |||||
| _install(tarball) | |||||
| if __name__ == '__main__': | |||||
| main(sys.argv[1:]) | |||||
| @@ -20,6 +20,9 @@ class AutoExtendPlugin(Plugin): | |||||
| and there is no extends statement, this plugin automatically adds | and there is no extends statement, this plugin automatically adds | ||||
| an extends statement to the top of the file. | an extends statement to the top of the file. | ||||
| """ | """ | ||||
| if not resource.uses_template: | |||||
| return text | |||||
| layout = None | layout = None | ||||
| block = None | block = None | ||||
| try: | try: | ||||
| @@ -7,7 +7,7 @@ import re | |||||
| from hyde.model import Expando | from hyde.model import Expando | ||||
| from hyde.plugin import Plugin | from hyde.plugin import Plugin | ||||
| from hyde.site import Node, Resource | from hyde.site import Node, Resource | ||||
| from hyde.util import add_method, pairwalk | |||||
| from hyde.util import add_method, add_property, pairwalk | |||||
| from collections import namedtuple | from collections import namedtuple | ||||
| from functools import partial | from functools import partial | ||||
| @@ -42,6 +42,10 @@ class Group(Expando): | |||||
| 'walk_resources_grouped_by_%s' % self.name, | 'walk_resources_grouped_by_%s' % self.name, | ||||
| Group.walk_resources, | Group.walk_resources, | ||||
| group=self) | group=self) | ||||
| add_property(Resource, | |||||
| '%s_group' % self.name, | |||||
| Group.get_resource_group, | |||||
| group=self) | |||||
| add_method(Resource, | add_method(Resource, | ||||
| 'walk_%s_groups' % self.name, | 'walk_%s_groups' % self.name, | ||||
| Group.walk_resource_groups, | Group.walk_resource_groups, | ||||
| @@ -57,6 +61,23 @@ class Group(Expando): | |||||
| else: | else: | ||||
| return super(Group, self).set_expando(key, value) | return super(Group, self).set_expando(key, value) | ||||
| @staticmethod | |||||
| def get_resource_group(resource, group): | |||||
| """ | |||||
| This method gets attached to the resource object. | |||||
| Returns group and its ancestors that the resource | |||||
| belongs to, in that order. | |||||
| """ | |||||
| try: | |||||
| group_name = getattr(resource.meta, group.root.name) | |||||
| except AttributeError: | |||||
| group_name = None | |||||
| return next((g for g in group.walk_groups() | |||||
| if g.name == group_name), None) \ | |||||
| if group_name \ | |||||
| else None | |||||
| @staticmethod | @staticmethod | ||||
| def walk_resource_groups(resource, group): | def walk_resource_groups(resource, group): | ||||
| """ | """ | ||||
| @@ -4,6 +4,7 @@ Jinja template utilties | |||||
| """ | """ | ||||
| from hyde.fs import File, Folder | from hyde.fs import File, Folder | ||||
| from hyde.model import Expando | |||||
| from hyde.template import HtmlWrap, Template | from hyde.template import HtmlWrap, Template | ||||
| from hyde.site import Resource | from hyde.site import Resource | ||||
| from hyde.util import getLoggerWithNullHandler, getLoggerWithConsoleHandler | from hyde.util import getLoggerWithNullHandler, getLoggerWithConsoleHandler | ||||
| @@ -58,11 +59,12 @@ def markdown(env, value): | |||||
| d = {} | d = {} | ||||
| if hasattr(env.config, 'markdown'): | if hasattr(env.config, 'markdown'): | ||||
| d['extensions'] = getattr(env.config.markdown, 'extensions', []) | d['extensions'] = getattr(env.config.markdown, 'extensions', []) | ||||
| d['extension_configs'] = getattr(env.config.markdown, 'extension_configs', {}) | |||||
| d['extension_configs'] = getattr(env.config.markdown, | |||||
| 'extension_configs', | |||||
| Expando({})).to_dict() | |||||
| md = markdown.Markdown(**d) | md = markdown.Markdown(**d) | ||||
| return md.convert(output) | return md.convert(output) | ||||
| @environmentfilter | @environmentfilter | ||||
| def syntax(env, value, lexer=None, filename=None): | def syntax(env, value, lexer=None, filename=None): | ||||
| """ | """ | ||||
| @@ -81,7 +83,9 @@ def syntax(env, value, lexer=None, filename=None): | |||||
| lexers.guess_lexer(value)) | lexers.guess_lexer(value)) | ||||
| settings = {} | settings = {} | ||||
| if hasattr(env.config, 'syntax'): | if hasattr(env.config, 'syntax'): | ||||
| settings = getattr(env.config.syntax, 'options', {}) | |||||
| settings = getattr(env.config.syntax, | |||||
| 'options', | |||||
| Expando({})).to_dict() | |||||
| formatter = formatters.HtmlFormatter(**settings) | formatter = formatters.HtmlFormatter(**settings) | ||||
| code = pygments.highlight(value, pyg, formatter) | code = pygments.highlight(value, pyg, formatter) | ||||
| @@ -117,6 +121,49 @@ class Markdown(Extension): | |||||
| output = caller().strip() | output = caller().strip() | ||||
| return markdown(self.environment, output) | return markdown(self.environment, output) | ||||
| class YamlVar(Extension): | |||||
| """ | |||||
| An extension that converts the content between the tags | |||||
| into an yaml object and sets the value in the given | |||||
| variable. | |||||
| """ | |||||
| tags = set(['yaml']) | |||||
| def parse(self, parser): | |||||
| """ | |||||
| Parses the contained data and defers to the callback to load it as | |||||
| yaml. | |||||
| """ | |||||
| lineno = parser.stream.next().lineno | |||||
| var = parser.stream.expect('name').value | |||||
| body = parser.parse_statements(['name:endyaml'], drop_needle=True) | |||||
| return [ | |||||
| nodes.Assign( | |||||
| nodes.Name(var, 'store'), | |||||
| nodes.Const({}) | |||||
| ).set_lineno(lineno), | |||||
| nodes.CallBlock( | |||||
| self.call_method('_set_yaml', args=[nodes.Name(var, 'load')]), | |||||
| [], [], body).set_lineno(lineno) | |||||
| ] | |||||
| def _set_yaml(self, var, caller=None): | |||||
| """ | |||||
| Loads the yaml data into the specified variable. | |||||
| """ | |||||
| if not caller: | |||||
| return '' | |||||
| try: | |||||
| import yaml | |||||
| except ImportError: | |||||
| return '' | |||||
| out = caller().strip() | |||||
| var.update(yaml.load(out)) | |||||
| return '' | |||||
| def parse_kwargs(parser): | def parse_kwargs(parser): | ||||
| name = parser.stream.expect('name').value | name = parser.stream.expect('name').value | ||||
| parser.stream.expect('assign') | parser.stream.expect('assign') | ||||
| @@ -265,7 +312,13 @@ class Refer(Extension): | |||||
| includeNode.ignore_missing = False | includeNode.ignore_missing = False | ||||
| includeNode.template = template | includeNode.template = template | ||||
| temp = parser.free_identifier(lineno) | |||||
| return [ | return [ | ||||
| nodes.Assign( | |||||
| nodes.Name(temp.name, 'store'), | |||||
| nodes.Name(MARKINGS, 'load') | |||||
| ).set_lineno(lineno), | |||||
| nodes.Assign( | nodes.Assign( | ||||
| nodes.Name(MARKINGS, 'store'), | nodes.Name(MARKINGS, 'store'), | ||||
| nodes.Const({})).set_lineno(lineno), | nodes.Const({})).set_lineno(lineno), | ||||
| @@ -295,6 +348,10 @@ class Refer(Extension): | |||||
| nodes.Getitem(nodes.Name(namespace, 'load'), | nodes.Getitem(nodes.Name(namespace, 'load'), | ||||
| nodes.Const('parent_resource'), 'load') | nodes.Const('parent_resource'), 'load') | ||||
| ).set_lineno(lineno), | ).set_lineno(lineno), | ||||
| nodes.Assign( | |||||
| nodes.Name(MARKINGS, 'store'), | |||||
| nodes.Name(temp.name, 'load') | |||||
| ).set_lineno(lineno), | |||||
| ] | ] | ||||
| def _push_resource(self, namespace, site, resource, template, caller): | def _push_resource(self, namespace, site, resource, template, caller): | ||||
| @@ -378,6 +435,7 @@ class Jinja2Template(Template): | |||||
| Syntax, | Syntax, | ||||
| Reference, | Reference, | ||||
| Refer, | Refer, | ||||
| YamlVar, | |||||
| 'jinja2.ext.do', | 'jinja2.ext.do', | ||||
| 'jinja2.ext.loopcontrols', | 'jinja2.ext.loopcontrols', | ||||
| 'jinja2.ext.with_']) | 'jinja2.ext.with_']) | ||||
| @@ -13,7 +13,7 @@ | |||||
| <!-- Always force latest IE rendering engine (even in intranet) & Chrome Frame | <!-- Always force latest IE rendering engine (even in intranet) & Chrome Frame | ||||
| Remove this if you use the .htaccess --> | Remove this if you use the .htaccess --> | ||||
| <meta http-equiv="X-UA-Compatible" content="{{page.meta.compatibility|default('IE=edge,chrome=1')"> | |||||
| <meta http-equiv="X-UA-Compatible" content="{{page.meta.compatibility|default('IE=edge,chrome=1')}}"> | |||||
| <!-- encoding must be specified within the first 512 bytes www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#charset --> | <!-- encoding must be specified within the first 512 bytes www.whatwg.org/specs/web-apps/current-work/multipage/semantics.html#charset --> | ||||
| @@ -25,7 +25,7 @@ | |||||
| <meta name="author" content="{{resource.meta.author}}"> | <meta name="author" content="{{resource.meta.author}}"> | ||||
| <!-- Mobile viewport optimized: j.mp/bplateviewport --> | <!-- Mobile viewport optimized: j.mp/bplateviewport --> | ||||
| <meta name="viewport" content="{{page.meta.viewport|default('width=device-width, initial-scale=1.0')"> | |||||
| <meta name="viewport" content="{{page.meta.viewport|default('width=device-width, initial-scale=1.0')}}"> | |||||
| {% block favicons %} | {% block favicons %} | ||||
| <!-- Place favicon.ico & apple-touch-icon.png in the root of your domain and delete these references --> | <!-- Place favicon.ico & apple-touch-icon.png in the root of your domain and delete these references --> | ||||
| @@ -99,4 +99,4 @@ | |||||
| {% endblock js %} | {% endblock js %} | ||||
| </body> | </body> | ||||
| </html> | </html> | ||||
| {% endblock all %} | |||||
| {% endblock all %} | |||||
| @@ -57,6 +57,16 @@ class Expando(object): | |||||
| else: | else: | ||||
| return primitive | return primitive | ||||
| def to_dict(self): | |||||
| """ | |||||
| Reverse transform an expando to dict | |||||
| """ | |||||
| d = self.__dict__ | |||||
| for k, v in d.iteritems(): | |||||
| if isinstance(v, Expando): | |||||
| d[k] = v.to_dict() | |||||
| return d | |||||
| class Context(object): | class Context(object): | ||||
| """ | """ | ||||
| @@ -58,9 +58,13 @@ class HydeRequestHandler(SimpleHTTPRequestHandler): | |||||
| logger.debug("Trying to load file based on request:[%s]" % result.path) | logger.debug("Trying to load file based on request:[%s]" % result.path) | ||||
| path = result.path.lstrip('/') | path = result.path.lstrip('/') | ||||
| if path.strip() == "" or File(path).kind.strip() == "": | if path.strip() == "" or File(path).kind.strip() == "": | ||||
| return site.config.deploy_root_path.child(path) | |||||
| res = site.content.resource_from_relative_deploy_path(path) | |||||
| deployed = site.config.deploy_root_path.child(path) | |||||
| deployed = Folder.file_or_folder(deployed) | |||||
| if isinstance(deployed, Folder): | |||||
| node = site.content.node_from_relative_path(path) | |||||
| res = node.get_resource('index.html') | |||||
| else: | |||||
| res = site.content.resource_from_relative_deploy_path(path) | |||||
| if not res: | if not res: | ||||
| @@ -130,6 +130,19 @@ class TestGrouperSingleLevel(object): | |||||
| plugin_resources = [resource.name for resource in self.s.content.walk_resources_grouped_by_plugins()] | plugin_resources = [resource.name for resource in self.s.content.walk_resources_grouped_by_plugins()] | ||||
| assert plugin_resources == self.plugins | assert plugin_resources == self.plugins | ||||
| def test_resource_group(self): | |||||
| groups = dict([(g.name, g) for g in self.s.grouper['section'].groups]) | |||||
| for name, group in groups.items(): | |||||
| pages = getattr(self, name) | |||||
| for page in pages: | |||||
| res = self.s.content.resource_from_relative_path('blog/' + page) | |||||
| assert hasattr(res, 'section_group') | |||||
| res_group = getattr(res, 'section_group') | |||||
| print "%s, %s=%s" % (page, group.name, res_group.name) | |||||
| assert res_group == group | |||||
| def test_resource_belongs_to(self): | def test_resource_belongs_to(self): | ||||
| groups = dict([(g.name, g) for g in self.s.grouper['section'].groups]) | groups = dict([(g.name, g) for g in self.s.grouper['section'].groups]) | ||||
| @@ -128,6 +128,7 @@ def assert_markdown_typogrify_processed_well(include_text, includer_text): | |||||
| gen.load_template_if_needed() | gen.load_template_if_needed() | ||||
| template = gen.template | template = gen.template | ||||
| html = template.render(includer_text, {}).strip() | html = template.render(includer_text, {}).strip() | ||||
| print html | |||||
| assert html | assert html | ||||
| q = PyQuery(html) | q = PyQuery(html) | ||||
| assert "is_processable" not in html | assert "is_processable" not in html | ||||
| @@ -200,7 +201,7 @@ class TestJinjaTemplate(object): | |||||
| deps = list(t.get_dependencies('inc.md')) | deps = list(t.get_dependencies('inc.md')) | ||||
| assert len(deps) == 1 | assert len(deps) == 1 | ||||
| assert not deps[0] | assert not deps[0] | ||||
| @@ -314,6 +315,52 @@ Hyde & Jinja. | |||||
| assert "mark" not in html | assert "mark" not in html | ||||
| assert "reference" not in html | assert "reference" not in html | ||||
| def test_two_level_refer_with_var(self): | |||||
| text = """ | |||||
| === | |||||
| is_processable: False | |||||
| === | |||||
| <div class="fulltext"> | |||||
| {% filter markdown|typogrify %} | |||||
| {% mark heading %} | |||||
| This is a heading | |||||
| ================= | |||||
| {% endmark %} | |||||
| {% reference content %} | |||||
| Hyde & Jinja. | |||||
| {% endreference %} | |||||
| {% endfilter %} | |||||
| </div> | |||||
| """ | |||||
| text2 = """ | |||||
| {% set super = 'super.md' %} | |||||
| {% refer to super as sup %} | |||||
| <div class="justhead"> | |||||
| {% mark child %} | |||||
| {{ sup.heading }} | |||||
| {% endmark %} | |||||
| {% mark cont %} | |||||
| {{ sup.content }} | |||||
| {% endmark %} | |||||
| </div> | |||||
| """ | |||||
| text3 = """ | |||||
| {% set incu = 'inc.md' %} | |||||
| {% refer to incu as inc %} | |||||
| {% filter markdown|typogrify %} | |||||
| {{ inc.child }} | |||||
| {{ inc.cont }} | |||||
| {% endfilter %} | |||||
| """ | |||||
| superinc = File(TEST_SITE.child('content/super.md')) | |||||
| superinc.write(text) | |||||
| html = assert_markdown_typogrify_processed_well(text2, text3) | |||||
| assert "mark" not in html | |||||
| assert "reference" not in html | |||||
| def test_refer_with_var(self): | def test_refer_with_var(self): | ||||
| text = """ | text = """ | ||||
| @@ -340,4 +387,45 @@ Hyde & Jinja. | |||||
| """ | """ | ||||
| html = assert_markdown_typogrify_processed_well(text, text2) | html = assert_markdown_typogrify_processed_well(text, text2) | ||||
| assert "mark" not in html | assert "mark" not in html | ||||
| assert "reference" not in html | |||||
| assert "reference" not in html | |||||
| def test_yaml_tag(salf): | |||||
| text = """ | |||||
| {% yaml test %} | |||||
| one: | |||||
| - A | |||||
| - B | |||||
| - C | |||||
| two: | |||||
| - D | |||||
| - E | |||||
| - F | |||||
| {% endyaml %} | |||||
| {% for section, values in test.items() %} | |||||
| <ul class="{{ section }}"> | |||||
| {% for value in values %} | |||||
| <li>{{ value }}</li> | |||||
| {% endfor %} | |||||
| </ul> | |||||
| {% endfor %} | |||||
| """ | |||||
| t = Jinja2Template(JINJA2.path) | |||||
| t.configure(None) | |||||
| html = t.render(text, {}).strip() | |||||
| actual = PyQuery(html) | |||||
| assert actual("ul").length == 2 | |||||
| assert actual("ul.one").length == 1 | |||||
| assert actual("ul.two").length == 1 | |||||
| assert actual("li").length == 6 | |||||
| assert actual("ul.one li").length == 3 | |||||
| assert actual("ul.two li").length == 3 | |||||
| ones = [item.text for item in actual("ul.one li")] | |||||
| assert ones == ["A", "B", "C"] | |||||
| twos = [item.text for item in actual("ul.two li")] | |||||
| assert twos == ["D", "E", "F"] | |||||
| @@ -43,6 +43,26 @@ def test_expando_update(): | |||||
| assert x.a == 789 | assert x.a == 789 | ||||
| assert x.f == "opq" | assert x.f == "opq" | ||||
| def test_expando_to_dict(): | |||||
| d = {"a": 123, "b": {"c": 456, "d": {"e": "abc"}}} | |||||
| x = Expando(d) | |||||
| assert d == x.to_dict() | |||||
| def test_expando_to_dict_with_update(): | |||||
| d1 = {"a": 123, "b": "abc"} | |||||
| x = Expando(d1) | |||||
| d = {"b": {"c": 456, "d": {"e": "abc"}}, "f": "lmn"} | |||||
| x.update(d) | |||||
| expected = {} | |||||
| expected.update(d1) | |||||
| expected.update(d) | |||||
| assert expected == x.to_dict() | |||||
| d2 = {"a": 789, "f": "opq"} | |||||
| y = Expando(d2) | |||||
| x.update(y) | |||||
| expected.update(d2) | |||||
| assert expected == x.to_dict() | |||||
| TEST_SITE = File(__file__).parent.child_folder('_test') | TEST_SITE = File(__file__).parent.child_folder('_test') | ||||
| import yaml | import yaml | ||||
| @@ -92,11 +92,16 @@ def make_method(method_name, method_): | |||||
| method__.__name__ = method_name | method__.__name__ = method_name | ||||
| return method__ | return method__ | ||||
| def add_property(obj, method_name, method_, *args, **kwargs): | |||||
| from functools import partial | |||||
| m = make_method(method_name, partial(method_, *args, **kwargs)) | |||||
| setattr(obj, method_name, property(m)) | |||||
| def add_method(obj, method_name, method_, *args, **kwargs): | def add_method(obj, method_name, method_, *args, **kwargs): | ||||
| from functools import partial | from functools import partial | ||||
| m = make_method(method_name, partial(method_, *args, **kwargs)) | m = make_method(method_name, partial(method_, *args, **kwargs)) | ||||
| setattr(obj, method_name, m) | setattr(obj, method_name, m) | ||||
| def pairwalk(iterable): | def pairwalk(iterable): | ||||
| a, b = tee(iterable) | a, b = tee(iterable) | ||||
| next(b, None) | next(b, None) | ||||
| @@ -3,4 +3,4 @@ | |||||
| Handles hyde version | Handles hyde version | ||||
| TODO: Use fabric like versioning scheme | TODO: Use fabric like versioning scheme | ||||
| """ | """ | ||||
| __version__ = '0.6.0' | |||||
| __version__ = '0.7b1' | |||||
| @@ -1,2 +0,0 @@ | |||||
| argparse | |||||
| -r req-2.7.txt | |||||
| @@ -1,7 +0,0 @@ | |||||
| commando==0.1.1a | |||||
| PyYAML==3.09 | |||||
| Markdown==2.0.3 | |||||
| MarkupSafe==0.11 | |||||
| smartypants==1.6.0.3 | |||||
| -e git://github.com/hyde/typogrify.git#egg=typogrify | |||||
| Jinja2==2.5.5 | |||||
| @@ -1 +1,8 @@ | |||||
| -r req-2.6.txt | |||||
| argparse | |||||
| commando | |||||
| PyYAML | |||||
| Markdown | |||||
| MarkupSafe | |||||
| smartypants | |||||
| -e git://github.com/hyde/typogrify.git#egg=typogrify | |||||
| Jinja2 | |||||
| @@ -1,13 +1,123 @@ | |||||
| # Bootstrap installation of Distribute | |||||
| import distribute_setup | |||||
| distribute_setup.use_setuptools() | |||||
| from setuptools import setup, find_packages | from setuptools import setup, find_packages | ||||
| from hyde.version import __version__ | from hyde.version import __version__ | ||||
| setup(name='hyde', | |||||
| from distutils.util import convert_path | |||||
| from fnmatch import fnmatchcase | |||||
| import os | |||||
| import sys | |||||
| PROJECT = 'hyde' | |||||
| try: | |||||
| long_description = open('README.markdown', 'rt').read() | |||||
| except IOError: | |||||
| long_description = '' | |||||
| ################################################################################ | |||||
| # find_package_data is an Ian Bicking creation. | |||||
| # Provided as an attribute, so you can append to these instead | |||||
| # of replicating them: | |||||
| standard_exclude = ('*.py', '*.pyc', '*~', '.*', '*.bak', '*.swp*') | |||||
| standard_exclude_directories = ('.*', 'CVS', '_darcs', './build', | |||||
| './dist', 'EGG-INFO', '*.egg-info') | |||||
| def find_package_data( | |||||
| where='.', package='', | |||||
| exclude=standard_exclude, | |||||
| exclude_directories=standard_exclude_directories, | |||||
| only_in_packages=True, | |||||
| show_ignored=False): | |||||
| """ | |||||
| Return a dictionary suitable for use in ``package_data`` | |||||
| in a distutils ``setup.py`` file. | |||||
| The dictionary looks like:: | |||||
| {'package': [files]} | |||||
| Where ``files`` is a list of all the files in that package that | |||||
| don't match anything in ``exclude``. | |||||
| If ``only_in_packages`` is true, then top-level directories that | |||||
| are not packages won't be included (but directories under packages | |||||
| will). | |||||
| Directories matching any pattern in ``exclude_directories`` will | |||||
| be ignored; by default directories with leading ``.``, ``CVS``, | |||||
| and ``_darcs`` will be ignored. | |||||
| If ``show_ignored`` is true, then all the files that aren't | |||||
| included in package data are shown on stderr (for debugging | |||||
| purposes). | |||||
| Note patterns use wildcards, or can be exact paths (including | |||||
| leading ``./``), and all searching is case-insensitive. | |||||
| This function is by Ian Bicking. | |||||
| """ | |||||
| out = {} | |||||
| stack = [(convert_path(where), '', package, only_in_packages)] | |||||
| while stack: | |||||
| where, prefix, package, only_in_packages = stack.pop(0) | |||||
| for name in os.listdir(where): | |||||
| fn = os.path.join(where, name) | |||||
| if os.path.isdir(fn): | |||||
| bad_name = False | |||||
| for pattern in exclude_directories: | |||||
| if (fnmatchcase(name, pattern) | |||||
| or fn.lower() == pattern.lower()): | |||||
| bad_name = True | |||||
| if show_ignored: | |||||
| print >> sys.stderr, ( | |||||
| "Directory %s ignored by pattern %s" | |||||
| % (fn, pattern)) | |||||
| break | |||||
| if bad_name: | |||||
| continue | |||||
| if os.path.isfile(os.path.join(fn, '__init__.py')): | |||||
| if not package: | |||||
| new_package = name | |||||
| else: | |||||
| new_package = package + '.' + name | |||||
| stack.append((fn, '', new_package, False)) | |||||
| else: | |||||
| stack.append((fn, prefix + name + '/', package, only_in_packages)) | |||||
| elif package or not only_in_packages: | |||||
| # is a file | |||||
| bad_name = False | |||||
| for pattern in exclude: | |||||
| if (fnmatchcase(name, pattern) | |||||
| or fn.lower() == pattern.lower()): | |||||
| bad_name = True | |||||
| if show_ignored: | |||||
| print >> sys.stderr, ( | |||||
| "File %s ignored by pattern %s" | |||||
| % (fn, pattern)) | |||||
| break | |||||
| if bad_name: | |||||
| continue | |||||
| out.setdefault(package, []).append(prefix+name) | |||||
| return out | |||||
| ################################################################################ | |||||
| setup(name=PROJECT, | |||||
| version=__version__, | version=__version__, | ||||
| description='hyde is a pythonic static website generator', | |||||
| description='hyde is a static website generator', | |||||
| long_description = long_description, | |||||
| author='Lakshmi Vyas', | author='Lakshmi Vyas', | ||||
| author_email='lakshmi.vyas@gmail.com', | author_email='lakshmi.vyas@gmail.com', | ||||
| url='http://ringce.com/hyde', | url='http://ringce.com/hyde', | ||||
| packages=find_packages(), | packages=find_packages(), | ||||
| dependency_links=[ | |||||
| "https://github.com/hyde/typogrify/tarball/hyde-setup#egg=typogrify-hyde" | |||||
| ], | |||||
| install_requires=( | install_requires=( | ||||
| 'argparse', | 'argparse', | ||||
| 'commando', | 'commando', | ||||
| @@ -15,8 +125,21 @@ setup(name='hyde', | |||||
| 'pyYAML', | 'pyYAML', | ||||
| 'markdown', | 'markdown', | ||||
| 'smartypants', | 'smartypants', | ||||
| 'pygments' | |||||
| 'pygments', | |||||
| 'typogrify-hyde' | |||||
| ), | |||||
| tests_require=( | |||||
| 'nose', | |||||
| ), | ), | ||||
| test_suite='nose.collector', | |||||
| include_package_data = True, | |||||
| # Scan the input for package information | |||||
| # to grab any data files (text, images, etc.) | |||||
| # associated with sub-packages. | |||||
| package_data = find_package_data(PROJECT, | |||||
| package=PROJECT, | |||||
| only_in_packages=False, | |||||
| ), | |||||
| scripts=['main.py'], | scripts=['main.py'], | ||||
| entry_points={ | entry_points={ | ||||
| 'console_scripts': [ | 'console_scripts': [ | ||||
| @@ -25,7 +148,7 @@ setup(name='hyde', | |||||
| }, | }, | ||||
| license='MIT', | license='MIT', | ||||
| classifiers=[ | classifiers=[ | ||||
| 'Development Status :: 3 - Alpha', | |||||
| 'Development Status :: 4 - Beta', | |||||
| 'Environment :: Console', | 'Environment :: Console', | ||||
| 'Intended Audience :: End Users/Desktop', | 'Intended Audience :: End Users/Desktop', | ||||
| 'Intended Audience :: Developers', | 'Intended Audience :: Developers', | ||||
| @@ -34,11 +157,13 @@ setup(name='hyde', | |||||
| 'Operating System :: MacOS :: MacOS X', | 'Operating System :: MacOS :: MacOS X', | ||||
| 'Operating System :: Unix', | 'Operating System :: Unix', | ||||
| 'Operating System :: POSIX', | 'Operating System :: POSIX', | ||||
| 'Operating System :: Microsoft :: Windows', | |||||
| 'Programming Language :: Python', | 'Programming Language :: Python', | ||||
| 'Programming Language :: Python :: 2.7', | |||||
| 'Topic :: Software Development', | 'Topic :: Software Development', | ||||
| 'Topic :: Software Development :: Build Tools', | 'Topic :: Software Development :: Build Tools', | ||||
| 'Topic :: Software Development :: Websites', | |||||
| 'Topic :: Software Development :: Static Websites', | |||||
| 'Topic :: Software Development :: Code Generators', | |||||
| 'Topic :: Internet', | |||||
| 'Topic :: Internet :: WWW/HTTP :: Site Management', | |||||
| ], | ], | ||||
| zip_safe=False, | |||||
| ) | ) | ||||