diff --git a/Dockerfile b/Dockerfile
index 9102fd81..82166e8c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -4,3 +4,5 @@ WORKDIR /app
COPY . /app
RUN python setup.py install \
&& rm -rf /app
+WORKDIR /
+ENTRYPOINT ["/usr/local/bin/cement"]
diff --git a/MANIFEST.in b/MANIFEST.in
index 78bf8b03..af13e088 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -2,3 +2,4 @@ recursive-include *.py
include setup.cfg
include README.md CHANGELOG.md LICENSE.md CONTRIBUTORS.md
include *.txt
+recursive-include cement/cli/templates/generate *
diff --git a/Makefile b/Makefile
index d5e57446..0968d703 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,4 @@
-.PHONY: all dev test comply docs clean dist dist-upload
-
-all: test comply comply-test api-docs clean
+.PHONY: dev test test-core comply-fix docs clean dist dist-upload
dev:
docker-compose up -d
diff --git a/cement/cli/contrib/markupsafe/__init__.py b/cement/cli/contrib/markupsafe/__init__.py
new file mode 100644
index 00000000..68dc85f6
--- /dev/null
+++ b/cement/cli/contrib/markupsafe/__init__.py
@@ -0,0 +1,305 @@
+# -*- coding: utf-8 -*-
+"""
+ markupsafe
+ ~~~~~~~~~~
+
+ Implements a Markup string.
+
+ :copyright: (c) 2010 by Armin Ronacher.
+ :license: BSD, see LICENSE for more details.
+"""
+import re
+import string
+from collections import Mapping
+from markupsafe._compat import text_type, string_types, int_types, \
+ unichr, iteritems, PY2
+
+__version__ = "1.0"
+
+__all__ = ['Markup', 'soft_unicode', 'escape', 'escape_silent']
+
+
+_striptags_re = re.compile(r'(|<[^>]*>)')
+_entity_re = re.compile(r'&([^& ;]+);')
+
+
+class Markup(text_type):
+ r"""Marks a string as being safe for inclusion in HTML/XML output without
+ needing to be escaped. This implements the `__html__` interface a couple
+ of frameworks and web applications use. :class:`Markup` is a direct
+ subclass of `unicode` and provides all the methods of `unicode` just that
+ it escapes arguments passed and always returns `Markup`.
+
+ The `escape` function returns markup objects so that double escaping can't
+ happen.
+
+ The constructor of the :class:`Markup` class can be used for three
+ different things: When passed an unicode object it's assumed to be safe,
+ when passed an object with an HTML representation (has an `__html__`
+ method) that representation is used, otherwise the object passed is
+ converted into a unicode string and then assumed to be safe:
+
+ >>> Markup("Hello World!")
+ Markup(u'Hello World!')
+ >>> class Foo(object):
+ ... def __html__(self):
+ ... return 'foo'
+ ...
+ >>> Markup(Foo())
+ Markup(u'foo')
+
+ If you want object passed being always treated as unsafe you can use the
+ :meth:`escape` classmethod to create a :class:`Markup` object:
+
+ >>> Markup.escape("Hello World!")
+ Markup(u'Hello <em>World</em>!')
+
+ Operations on a markup string are markup aware which means that all
+ arguments are passed through the :func:`escape` function:
+
+ >>> em = Markup("%s")
+ >>> em % "foo & bar"
+ Markup(u'foo & bar')
+ >>> strong = Markup("%(text)s")
+ >>> strong % {'text': ''}
+ Markup(u'<blink>hacker here</blink>')
+ >>> Markup("Hello ") + ""
+ Markup(u'Hello <foo>')
+ """
+ __slots__ = ()
+
+ def __new__(cls, base=u'', encoding=None, errors='strict'):
+ if hasattr(base, '__html__'):
+ base = base.__html__()
+ if encoding is None:
+ return text_type.__new__(cls, base)
+ return text_type.__new__(cls, base, encoding, errors)
+
+ def __html__(self):
+ return self
+
+ def __add__(self, other):
+ if isinstance(other, string_types) or hasattr(other, '__html__'):
+ return self.__class__(super(Markup, self).__add__(self.escape(other)))
+ return NotImplemented
+
+ def __radd__(self, other):
+ if hasattr(other, '__html__') or isinstance(other, string_types):
+ return self.escape(other).__add__(self)
+ return NotImplemented
+
+ def __mul__(self, num):
+ if isinstance(num, int_types):
+ return self.__class__(text_type.__mul__(self, num))
+ return NotImplemented
+ __rmul__ = __mul__
+
+ def __mod__(self, arg):
+ if isinstance(arg, tuple):
+ arg = tuple(_MarkupEscapeHelper(x, self.escape) for x in arg)
+ else:
+ arg = _MarkupEscapeHelper(arg, self.escape)
+ return self.__class__(text_type.__mod__(self, arg))
+
+ def __repr__(self):
+ return '%s(%s)' % (
+ self.__class__.__name__,
+ text_type.__repr__(self)
+ )
+
+ def join(self, seq):
+ return self.__class__(text_type.join(self, map(self.escape, seq)))
+ join.__doc__ = text_type.join.__doc__
+
+ def split(self, *args, **kwargs):
+ return list(map(self.__class__, text_type.split(self, *args, **kwargs)))
+ split.__doc__ = text_type.split.__doc__
+
+ def rsplit(self, *args, **kwargs):
+ return list(map(self.__class__, text_type.rsplit(self, *args, **kwargs)))
+ rsplit.__doc__ = text_type.rsplit.__doc__
+
+ def splitlines(self, *args, **kwargs):
+ return list(map(self.__class__, text_type.splitlines(
+ self, *args, **kwargs)))
+ splitlines.__doc__ = text_type.splitlines.__doc__
+
+ def unescape(self):
+ r"""Unescape markup again into an text_type string. This also resolves
+ known HTML4 and XHTML entities:
+
+ >>> Markup("Main » About").unescape()
+ u'Main \xbb About'
+ """
+ from markupsafe._constants import HTML_ENTITIES
+ def handle_match(m):
+ name = m.group(1)
+ if name in HTML_ENTITIES:
+ return unichr(HTML_ENTITIES[name])
+ try:
+ if name[:2] in ('#x', '#X'):
+ return unichr(int(name[2:], 16))
+ elif name.startswith('#'):
+ return unichr(int(name[1:]))
+ except ValueError:
+ pass
+ # Don't modify unexpected input.
+ return m.group()
+ return _entity_re.sub(handle_match, text_type(self))
+
+ def striptags(self):
+ r"""Unescape markup into an text_type string and strip all tags. This
+ also resolves known HTML4 and XHTML entities. Whitespace is
+ normalized to one:
+
+ >>> Markup("Main » About").striptags()
+ u'Main \xbb About'
+ """
+ stripped = u' '.join(_striptags_re.sub('', self).split())
+ return Markup(stripped).unescape()
+
+ @classmethod
+ def escape(cls, s):
+ """Escape the string. Works like :func:`escape` with the difference
+ that for subclasses of :class:`Markup` this function would return the
+ correct subclass.
+ """
+ rv = escape(s)
+ if rv.__class__ is not cls:
+ return cls(rv)
+ return rv
+
+ def make_simple_escaping_wrapper(name):
+ orig = getattr(text_type, name)
+ def func(self, *args, **kwargs):
+ args = _escape_argspec(list(args), enumerate(args), self.escape)
+ _escape_argspec(kwargs, iteritems(kwargs), self.escape)
+ return self.__class__(orig(self, *args, **kwargs))
+ func.__name__ = orig.__name__
+ func.__doc__ = orig.__doc__
+ return func
+
+ for method in '__getitem__', 'capitalize', \
+ 'title', 'lower', 'upper', 'replace', 'ljust', \
+ 'rjust', 'lstrip', 'rstrip', 'center', 'strip', \
+ 'translate', 'expandtabs', 'swapcase', 'zfill':
+ locals()[method] = make_simple_escaping_wrapper(method)
+
+ # new in python 2.5
+ if hasattr(text_type, 'partition'):
+ def partition(self, sep):
+ return tuple(map(self.__class__,
+ text_type.partition(self, self.escape(sep))))
+ def rpartition(self, sep):
+ return tuple(map(self.__class__,
+ text_type.rpartition(self, self.escape(sep))))
+
+ # new in python 2.6
+ if hasattr(text_type, 'format'):
+ def format(*args, **kwargs):
+ self, args = args[0], args[1:]
+ formatter = EscapeFormatter(self.escape)
+ kwargs = _MagicFormatMapping(args, kwargs)
+ return self.__class__(formatter.vformat(self, args, kwargs))
+
+ def __html_format__(self, format_spec):
+ if format_spec:
+ raise ValueError('Unsupported format specification '
+ 'for Markup.')
+ return self
+
+ # not in python 3
+ if hasattr(text_type, '__getslice__'):
+ __getslice__ = make_simple_escaping_wrapper('__getslice__')
+
+ del method, make_simple_escaping_wrapper
+
+
+class _MagicFormatMapping(Mapping):
+ """This class implements a dummy wrapper to fix a bug in the Python
+ standard library for string formatting.
+
+ See http://bugs.python.org/issue13598 for information about why
+ this is necessary.
+ """
+
+ def __init__(self, args, kwargs):
+ self._args = args
+ self._kwargs = kwargs
+ self._last_index = 0
+
+ def __getitem__(self, key):
+ if key == '':
+ idx = self._last_index
+ self._last_index += 1
+ try:
+ return self._args[idx]
+ except LookupError:
+ pass
+ key = str(idx)
+ return self._kwargs[key]
+
+ def __iter__(self):
+ return iter(self._kwargs)
+
+ def __len__(self):
+ return len(self._kwargs)
+
+
+if hasattr(text_type, 'format'):
+ class EscapeFormatter(string.Formatter):
+
+ def __init__(self, escape):
+ self.escape = escape
+
+ def format_field(self, value, format_spec):
+ if hasattr(value, '__html_format__'):
+ rv = value.__html_format__(format_spec)
+ elif hasattr(value, '__html__'):
+ if format_spec:
+ raise ValueError('No format specification allowed '
+ 'when formatting an object with '
+ 'its __html__ method.')
+ rv = value.__html__()
+ else:
+ # We need to make sure the format spec is unicode here as
+ # otherwise the wrong callback methods are invoked. For
+ # instance a byte string there would invoke __str__ and
+ # not __unicode__.
+ rv = string.Formatter.format_field(
+ self, value, text_type(format_spec))
+ return text_type(self.escape(rv))
+
+
+def _escape_argspec(obj, iterable, escape):
+ """Helper for various string-wrapped functions."""
+ for key, value in iterable:
+ if hasattr(value, '__html__') or isinstance(value, string_types):
+ obj[key] = escape(value)
+ return obj
+
+
+class _MarkupEscapeHelper(object):
+ """Helper for Markup.__mod__"""
+
+ def __init__(self, obj, escape):
+ self.obj = obj
+ self.escape = escape
+
+ __getitem__ = lambda s, x: _MarkupEscapeHelper(s.obj[x], s.escape)
+ __unicode__ = __str__ = lambda s: text_type(s.escape(s.obj))
+ __repr__ = lambda s: str(s.escape(repr(s.obj)))
+ __int__ = lambda s: int(s.obj)
+ __float__ = lambda s: float(s.obj)
+
+
+# we have to import it down here as the speedups and native
+# modules imports the markup type which is define above.
+try:
+ from markupsafe._speedups import escape, escape_silent, soft_unicode
+except ImportError:
+ from markupsafe._native import escape, escape_silent, soft_unicode
+
+if not PY2:
+ soft_str = soft_unicode
+ __all__.append('soft_str')
diff --git a/cement/cli/contrib/markupsafe/_compat.py b/cement/cli/contrib/markupsafe/_compat.py
new file mode 100644
index 00000000..62e5632a
--- /dev/null
+++ b/cement/cli/contrib/markupsafe/_compat.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+"""
+ markupsafe._compat
+ ~~~~~~~~~~~~~~~~~~
+
+ Compatibility module for different Python versions.
+
+ :copyright: (c) 2013 by Armin Ronacher.
+ :license: BSD, see LICENSE for more details.
+"""
+import sys
+
+PY2 = sys.version_info[0] == 2
+
+if not PY2:
+ text_type = str
+ string_types = (str,)
+ unichr = chr
+ int_types = (int,)
+ iteritems = lambda x: iter(x.items())
+else:
+ text_type = unicode
+ string_types = (str, unicode)
+ unichr = unichr
+ int_types = (int, long)
+ iteritems = lambda x: x.iteritems()
diff --git a/cement/cli/contrib/markupsafe/_constants.py b/cement/cli/contrib/markupsafe/_constants.py
new file mode 100644
index 00000000..919bf03c
--- /dev/null
+++ b/cement/cli/contrib/markupsafe/_constants.py
@@ -0,0 +1,267 @@
+# -*- coding: utf-8 -*-
+"""
+ markupsafe._constants
+ ~~~~~~~~~~~~~~~~~~~~~
+
+ Highlevel implementation of the Markup string.
+
+ :copyright: (c) 2010 by Armin Ronacher.
+ :license: BSD, see LICENSE for more details.
+"""
+
+
+HTML_ENTITIES = {
+ 'AElig': 198,
+ 'Aacute': 193,
+ 'Acirc': 194,
+ 'Agrave': 192,
+ 'Alpha': 913,
+ 'Aring': 197,
+ 'Atilde': 195,
+ 'Auml': 196,
+ 'Beta': 914,
+ 'Ccedil': 199,
+ 'Chi': 935,
+ 'Dagger': 8225,
+ 'Delta': 916,
+ 'ETH': 208,
+ 'Eacute': 201,
+ 'Ecirc': 202,
+ 'Egrave': 200,
+ 'Epsilon': 917,
+ 'Eta': 919,
+ 'Euml': 203,
+ 'Gamma': 915,
+ 'Iacute': 205,
+ 'Icirc': 206,
+ 'Igrave': 204,
+ 'Iota': 921,
+ 'Iuml': 207,
+ 'Kappa': 922,
+ 'Lambda': 923,
+ 'Mu': 924,
+ 'Ntilde': 209,
+ 'Nu': 925,
+ 'OElig': 338,
+ 'Oacute': 211,
+ 'Ocirc': 212,
+ 'Ograve': 210,
+ 'Omega': 937,
+ 'Omicron': 927,
+ 'Oslash': 216,
+ 'Otilde': 213,
+ 'Ouml': 214,
+ 'Phi': 934,
+ 'Pi': 928,
+ 'Prime': 8243,
+ 'Psi': 936,
+ 'Rho': 929,
+ 'Scaron': 352,
+ 'Sigma': 931,
+ 'THORN': 222,
+ 'Tau': 932,
+ 'Theta': 920,
+ 'Uacute': 218,
+ 'Ucirc': 219,
+ 'Ugrave': 217,
+ 'Upsilon': 933,
+ 'Uuml': 220,
+ 'Xi': 926,
+ 'Yacute': 221,
+ 'Yuml': 376,
+ 'Zeta': 918,
+ 'aacute': 225,
+ 'acirc': 226,
+ 'acute': 180,
+ 'aelig': 230,
+ 'agrave': 224,
+ 'alefsym': 8501,
+ 'alpha': 945,
+ 'amp': 38,
+ 'and': 8743,
+ 'ang': 8736,
+ 'apos': 39,
+ 'aring': 229,
+ 'asymp': 8776,
+ 'atilde': 227,
+ 'auml': 228,
+ 'bdquo': 8222,
+ 'beta': 946,
+ 'brvbar': 166,
+ 'bull': 8226,
+ 'cap': 8745,
+ 'ccedil': 231,
+ 'cedil': 184,
+ 'cent': 162,
+ 'chi': 967,
+ 'circ': 710,
+ 'clubs': 9827,
+ 'cong': 8773,
+ 'copy': 169,
+ 'crarr': 8629,
+ 'cup': 8746,
+ 'curren': 164,
+ 'dArr': 8659,
+ 'dagger': 8224,
+ 'darr': 8595,
+ 'deg': 176,
+ 'delta': 948,
+ 'diams': 9830,
+ 'divide': 247,
+ 'eacute': 233,
+ 'ecirc': 234,
+ 'egrave': 232,
+ 'empty': 8709,
+ 'emsp': 8195,
+ 'ensp': 8194,
+ 'epsilon': 949,
+ 'equiv': 8801,
+ 'eta': 951,
+ 'eth': 240,
+ 'euml': 235,
+ 'euro': 8364,
+ 'exist': 8707,
+ 'fnof': 402,
+ 'forall': 8704,
+ 'frac12': 189,
+ 'frac14': 188,
+ 'frac34': 190,
+ 'frasl': 8260,
+ 'gamma': 947,
+ 'ge': 8805,
+ 'gt': 62,
+ 'hArr': 8660,
+ 'harr': 8596,
+ 'hearts': 9829,
+ 'hellip': 8230,
+ 'iacute': 237,
+ 'icirc': 238,
+ 'iexcl': 161,
+ 'igrave': 236,
+ 'image': 8465,
+ 'infin': 8734,
+ 'int': 8747,
+ 'iota': 953,
+ 'iquest': 191,
+ 'isin': 8712,
+ 'iuml': 239,
+ 'kappa': 954,
+ 'lArr': 8656,
+ 'lambda': 955,
+ 'lang': 9001,
+ 'laquo': 171,
+ 'larr': 8592,
+ 'lceil': 8968,
+ 'ldquo': 8220,
+ 'le': 8804,
+ 'lfloor': 8970,
+ 'lowast': 8727,
+ 'loz': 9674,
+ 'lrm': 8206,
+ 'lsaquo': 8249,
+ 'lsquo': 8216,
+ 'lt': 60,
+ 'macr': 175,
+ 'mdash': 8212,
+ 'micro': 181,
+ 'middot': 183,
+ 'minus': 8722,
+ 'mu': 956,
+ 'nabla': 8711,
+ 'nbsp': 160,
+ 'ndash': 8211,
+ 'ne': 8800,
+ 'ni': 8715,
+ 'not': 172,
+ 'notin': 8713,
+ 'nsub': 8836,
+ 'ntilde': 241,
+ 'nu': 957,
+ 'oacute': 243,
+ 'ocirc': 244,
+ 'oelig': 339,
+ 'ograve': 242,
+ 'oline': 8254,
+ 'omega': 969,
+ 'omicron': 959,
+ 'oplus': 8853,
+ 'or': 8744,
+ 'ordf': 170,
+ 'ordm': 186,
+ 'oslash': 248,
+ 'otilde': 245,
+ 'otimes': 8855,
+ 'ouml': 246,
+ 'para': 182,
+ 'part': 8706,
+ 'permil': 8240,
+ 'perp': 8869,
+ 'phi': 966,
+ 'pi': 960,
+ 'piv': 982,
+ 'plusmn': 177,
+ 'pound': 163,
+ 'prime': 8242,
+ 'prod': 8719,
+ 'prop': 8733,
+ 'psi': 968,
+ 'quot': 34,
+ 'rArr': 8658,
+ 'radic': 8730,
+ 'rang': 9002,
+ 'raquo': 187,
+ 'rarr': 8594,
+ 'rceil': 8969,
+ 'rdquo': 8221,
+ 'real': 8476,
+ 'reg': 174,
+ 'rfloor': 8971,
+ 'rho': 961,
+ 'rlm': 8207,
+ 'rsaquo': 8250,
+ 'rsquo': 8217,
+ 'sbquo': 8218,
+ 'scaron': 353,
+ 'sdot': 8901,
+ 'sect': 167,
+ 'shy': 173,
+ 'sigma': 963,
+ 'sigmaf': 962,
+ 'sim': 8764,
+ 'spades': 9824,
+ 'sub': 8834,
+ 'sube': 8838,
+ 'sum': 8721,
+ 'sup': 8835,
+ 'sup1': 185,
+ 'sup2': 178,
+ 'sup3': 179,
+ 'supe': 8839,
+ 'szlig': 223,
+ 'tau': 964,
+ 'there4': 8756,
+ 'theta': 952,
+ 'thetasym': 977,
+ 'thinsp': 8201,
+ 'thorn': 254,
+ 'tilde': 732,
+ 'times': 215,
+ 'trade': 8482,
+ 'uArr': 8657,
+ 'uacute': 250,
+ 'uarr': 8593,
+ 'ucirc': 251,
+ 'ugrave': 249,
+ 'uml': 168,
+ 'upsih': 978,
+ 'upsilon': 965,
+ 'uuml': 252,
+ 'weierp': 8472,
+ 'xi': 958,
+ 'yacute': 253,
+ 'yen': 165,
+ 'yuml': 255,
+ 'zeta': 950,
+ 'zwj': 8205,
+ 'zwnj': 8204
+}
diff --git a/cement/cli/contrib/markupsafe/_native.py b/cement/cli/contrib/markupsafe/_native.py
new file mode 100644
index 00000000..5e83f10a
--- /dev/null
+++ b/cement/cli/contrib/markupsafe/_native.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+"""
+ markupsafe._native
+ ~~~~~~~~~~~~~~~~~~
+
+ Native Python implementation the C module is not compiled.
+
+ :copyright: (c) 2010 by Armin Ronacher.
+ :license: BSD, see LICENSE for more details.
+"""
+from markupsafe import Markup
+from markupsafe._compat import text_type
+
+
+def escape(s):
+ """Convert the characters &, <, >, ' and " in string s to HTML-safe
+ sequences. Use this if you need to display text that might contain
+ such characters in HTML. Marks return value as markup string.
+ """
+ if hasattr(s, '__html__'):
+ return s.__html__()
+ return Markup(text_type(s)
+ .replace('&', '&')
+ .replace('>', '>')
+ .replace('<', '<')
+ .replace("'", ''')
+ .replace('"', '"')
+ )
+
+
+def escape_silent(s):
+ """Like :func:`escape` but converts `None` into an empty
+ markup string.
+ """
+ if s is None:
+ return Markup()
+ return escape(s)
+
+
+def soft_unicode(s):
+ """Make a string unicode if it isn't already. That way a markup
+ string is not converted back to unicode.
+ """
+ if not isinstance(s, text_type):
+ s = text_type(s)
+ return s
diff --git a/cement/cli/contrib/markupsafe/_speedups.c b/cement/cli/contrib/markupsafe/_speedups.c
new file mode 100644
index 00000000..d779a68c
--- /dev/null
+++ b/cement/cli/contrib/markupsafe/_speedups.c
@@ -0,0 +1,239 @@
+/**
+ * markupsafe._speedups
+ * ~~~~~~~~~~~~~~~~~~~~
+ *
+ * This module implements functions for automatic escaping in C for better
+ * performance.
+ *
+ * :copyright: (c) 2010 by Armin Ronacher.
+ * :license: BSD.
+ */
+
+#include
+
+#define ESCAPED_CHARS_TABLE_SIZE 63
+#define UNICHR(x) (PyUnicode_AS_UNICODE((PyUnicodeObject*)PyUnicode_DecodeASCII(x, strlen(x), NULL)));
+
+#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN)
+typedef int Py_ssize_t;
+#define PY_SSIZE_T_MAX INT_MAX
+#define PY_SSIZE_T_MIN INT_MIN
+#endif
+
+
+static PyObject* markup;
+static Py_ssize_t escaped_chars_delta_len[ESCAPED_CHARS_TABLE_SIZE];
+static Py_UNICODE *escaped_chars_repl[ESCAPED_CHARS_TABLE_SIZE];
+
+static int
+init_constants(void)
+{
+ PyObject *module;
+ /* mapping of characters to replace */
+ escaped_chars_repl['"'] = UNICHR(""");
+ escaped_chars_repl['\''] = UNICHR("'");
+ escaped_chars_repl['&'] = UNICHR("&");
+ escaped_chars_repl['<'] = UNICHR("<");
+ escaped_chars_repl['>'] = UNICHR(">");
+
+ /* lengths of those characters when replaced - 1 */
+ memset(escaped_chars_delta_len, 0, sizeof (escaped_chars_delta_len));
+ escaped_chars_delta_len['"'] = escaped_chars_delta_len['\''] = \
+ escaped_chars_delta_len['&'] = 4;
+ escaped_chars_delta_len['<'] = escaped_chars_delta_len['>'] = 3;
+
+ /* import markup type so that we can mark the return value */
+ module = PyImport_ImportModule("markupsafe");
+ if (!module)
+ return 0;
+ markup = PyObject_GetAttrString(module, "Markup");
+ Py_DECREF(module);
+
+ return 1;
+}
+
+static PyObject*
+escape_unicode(PyUnicodeObject *in)
+{
+ PyUnicodeObject *out;
+ Py_UNICODE *inp = PyUnicode_AS_UNICODE(in);
+ const Py_UNICODE *inp_end = PyUnicode_AS_UNICODE(in) + PyUnicode_GET_SIZE(in);
+ Py_UNICODE *next_escp;
+ Py_UNICODE *outp;
+ Py_ssize_t delta=0, erepl=0, delta_len=0;
+
+ /* First we need to figure out how long the escaped string will be */
+ while (*(inp) || inp < inp_end) {
+ if (*inp < ESCAPED_CHARS_TABLE_SIZE) {
+ delta += escaped_chars_delta_len[*inp];
+ erepl += !!escaped_chars_delta_len[*inp];
+ }
+ ++inp;
+ }
+
+ /* Do we need to escape anything at all? */
+ if (!erepl) {
+ Py_INCREF(in);
+ return (PyObject*)in;
+ }
+
+ out = (PyUnicodeObject*)PyUnicode_FromUnicode(NULL, PyUnicode_GET_SIZE(in) + delta);
+ if (!out)
+ return NULL;
+
+ outp = PyUnicode_AS_UNICODE(out);
+ inp = PyUnicode_AS_UNICODE(in);
+ while (erepl-- > 0) {
+ /* look for the next substitution */
+ next_escp = inp;
+ while (next_escp < inp_end) {
+ if (*next_escp < ESCAPED_CHARS_TABLE_SIZE &&
+ (delta_len = escaped_chars_delta_len[*next_escp])) {
+ ++delta_len;
+ break;
+ }
+ ++next_escp;
+ }
+
+ if (next_escp > inp) {
+ /* copy unescaped chars between inp and next_escp */
+ Py_UNICODE_COPY(outp, inp, next_escp-inp);
+ outp += next_escp - inp;
+ }
+
+ /* escape 'next_escp' */
+ Py_UNICODE_COPY(outp, escaped_chars_repl[*next_escp], delta_len);
+ outp += delta_len;
+
+ inp = next_escp + 1;
+ }
+ if (inp < inp_end)
+ Py_UNICODE_COPY(outp, inp, PyUnicode_GET_SIZE(in) - (inp - PyUnicode_AS_UNICODE(in)));
+
+ return (PyObject*)out;
+}
+
+
+static PyObject*
+escape(PyObject *self, PyObject *text)
+{
+ PyObject *s = NULL, *rv = NULL, *html;
+
+ /* we don't have to escape integers, bools or floats */
+ if (PyLong_CheckExact(text) ||
+#if PY_MAJOR_VERSION < 3
+ PyInt_CheckExact(text) ||
+#endif
+ PyFloat_CheckExact(text) || PyBool_Check(text) ||
+ text == Py_None)
+ return PyObject_CallFunctionObjArgs(markup, text, NULL);
+
+ /* if the object has an __html__ method that performs the escaping */
+ html = PyObject_GetAttrString(text, "__html__");
+ if (html) {
+ rv = PyObject_CallObject(html, NULL);
+ Py_DECREF(html);
+ return rv;
+ }
+
+ /* otherwise make the object unicode if it isn't, then escape */
+ PyErr_Clear();
+ if (!PyUnicode_Check(text)) {
+#if PY_MAJOR_VERSION < 3
+ PyObject *unicode = PyObject_Unicode(text);
+#else
+ PyObject *unicode = PyObject_Str(text);
+#endif
+ if (!unicode)
+ return NULL;
+ s = escape_unicode((PyUnicodeObject*)unicode);
+ Py_DECREF(unicode);
+ }
+ else
+ s = escape_unicode((PyUnicodeObject*)text);
+
+ /* convert the unicode string into a markup object. */
+ rv = PyObject_CallFunctionObjArgs(markup, (PyObject*)s, NULL);
+ Py_DECREF(s);
+ return rv;
+}
+
+
+static PyObject*
+escape_silent(PyObject *self, PyObject *text)
+{
+ if (text != Py_None)
+ return escape(self, text);
+ return PyObject_CallFunctionObjArgs(markup, NULL);
+}
+
+
+static PyObject*
+soft_unicode(PyObject *self, PyObject *s)
+{
+ if (!PyUnicode_Check(s))
+#if PY_MAJOR_VERSION < 3
+ return PyObject_Unicode(s);
+#else
+ return PyObject_Str(s);
+#endif
+ Py_INCREF(s);
+ return s;
+}
+
+
+static PyMethodDef module_methods[] = {
+ {"escape", (PyCFunction)escape, METH_O,
+ "escape(s) -> markup\n\n"
+ "Convert the characters &, <, >, ', and \" in string s to HTML-safe\n"
+ "sequences. Use this if you need to display text that might contain\n"
+ "such characters in HTML. Marks return value as markup string."},
+ {"escape_silent", (PyCFunction)escape_silent, METH_O,
+ "escape_silent(s) -> markup\n\n"
+ "Like escape but converts None to an empty string."},
+ {"soft_unicode", (PyCFunction)soft_unicode, METH_O,
+ "soft_unicode(object) -> string\n\n"
+ "Make a string unicode if it isn't already. That way a markup\n"
+ "string is not converted back to unicode."},
+ {NULL, NULL, 0, NULL} /* Sentinel */
+};
+
+
+#if PY_MAJOR_VERSION < 3
+
+#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */
+#define PyMODINIT_FUNC void
+#endif
+PyMODINIT_FUNC
+init_speedups(void)
+{
+ if (!init_constants())
+ return;
+
+ Py_InitModule3("markupsafe._speedups", module_methods, "");
+}
+
+#else /* Python 3.x module initialization */
+
+static struct PyModuleDef module_definition = {
+ PyModuleDef_HEAD_INIT,
+ "markupsafe._speedups",
+ NULL,
+ -1,
+ module_methods,
+ NULL,
+ NULL,
+ NULL,
+ NULL
+};
+
+PyMODINIT_FUNC
+PyInit__speedups(void)
+{
+ if (!init_constants())
+ return NULL;
+
+ return PyModule_Create(&module_definition);
+}
+
+#endif
diff --git a/cement/cli/controllers/base.py b/cement/cli/controllers/base.py
index 27b26a6a..772b3c1e 100644
--- a/cement/cli/controllers/base.py
+++ b/cement/cli/controllers/base.py
@@ -1,18 +1,8 @@
-import sys
-import platform
from cement import Controller
-from cement.utils.version import get_version
+from cement.utils.version import get_version_banner
-VERSION = get_version()
-PYTHON_VERSION = '.'.join([str(x) for x in sys.version_info[0:3]])
-PLATFORM = platform.platform()
-
-BANNER = """
-Cement Framework %s
-Python %s
-Platform %s
-""" % (VERSION, PYTHON_VERSION, PLATFORM)
+BANNER = get_version_banner()
class Base(Controller):
diff --git a/cement/cli/templates/generate/extension/.generate.yml b/cement/cli/templates/generate/extension/.generate.yml
index b5aedae5..03de1207 100644
--- a/cement/cli/templates/generate/extension/.generate.yml
+++ b/cement/cli/templates/generate/extension/.generate.yml
@@ -1,5 +1,13 @@
---
+exclude:
+ - '^(.*)[\/\\\\]extension[\/\\\\]{{ label }}[\/\\\\]templates[\/\\\\](.*)$'
+
+ignore:
+ - '^(.*)pyc(.*)$'
+ - '^(.*)pyo(.*)$'
+ - '^(.*)__pycache__(.*)$'
+
variables:
- name: label
prompt: "Extension Label"
diff --git a/cement/cli/templates/generate/plugin/.generate.yml b/cement/cli/templates/generate/plugin/.generate.yml
index 6ffed86c..abd6a61e 100644
--- a/cement/cli/templates/generate/plugin/.generate.yml
+++ b/cement/cli/templates/generate/plugin/.generate.yml
@@ -3,6 +3,11 @@
exclude:
- '^(.*)[\/\\\\]plugin[\/\\\\]{{ label }}[\/\\\\]templates[\/\\\\](.*)$'
+ignore:
+ - '^(.*)pyc(.*)$'
+ - '^(.*)pyo(.*)$'
+ - '^(.*)__pycache__(.*)$'
+
variables:
- name: label
prompt: "Plugin Label"
diff --git a/cement/cli/templates/generate/project-loaded/.generate.yml b/cement/cli/templates/generate/project-loaded/.generate.yml
new file mode 100644
index 00000000..ceac5be0
--- /dev/null
+++ b/cement/cli/templates/generate/project-loaded/.generate.yml
@@ -0,0 +1,39 @@
+---
+
+exclude:
+ - '^(.*)[\/\\\\]project[\/\\\\]{{ label }}[\/\\\\]templates[\/\\\\](.*)$'
+
+variables:
+ - name: label
+ prompt: "App Label"
+ case: "lower"
+ default: "myapp"
+
+ - name: name
+ prompt: "App Name"
+ default: "My Application"
+
+ - name: class_name
+ prompt: "App Class Name"
+ validate: "^[a-zA-Z0-9]+$"
+ default: "MyApp"
+
+ - name: description
+ prompt: "App Description"
+ default: "MyApp Does Amazing Things!"
+
+ - name: creator
+ prompt: "Creator Name"
+ default: "John Doe"
+
+ - name: creator_email
+ prompt: "Creator Email"
+ default: "john.doe@example.com"
+
+ - name: url
+ prompt: "Project URL"
+ default: "https://github.com/johndoe/myapp/"
+
+ - name: license
+ prompt: "License"
+ default: "unlicensed"
diff --git a/cement/cli/templates/generate/project-loaded/.gitignore b/cement/cli/templates/generate/project-loaded/.gitignore
new file mode 100644
index 00000000..a74b246a
--- /dev/null
+++ b/cement/cli/templates/generate/project-loaded/.gitignore
@@ -0,0 +1,105 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+coverage-report/
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# pyenv
+.python-version
+
+# celery beat schedule file
+celerybeat-schedule
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
diff --git a/cement/cli/templates/generate/project-loaded/CHANGELOG.md b/cement/cli/templates/generate/project-loaded/CHANGELOG.md
new file mode 100644
index 00000000..e69de29b
diff --git a/cement/cli/templates/generate/project-loaded/Dockerfile b/cement/cli/templates/generate/project-loaded/Dockerfile
new file mode 100644
index 00000000..91dd3343
--- /dev/null
+++ b/cement/cli/templates/generate/project-loaded/Dockerfile
@@ -0,0 +1,15 @@
+FROM python:3.6-alpine
+MAINTAINER {{ creator }} <{{ creator_email }}>
+WORKDIR /app
+COPY . /app
+RUN apk update \
+ && apk add git \
+ && pip install --no-cache-dir -r requirements.txt \
+ && rm -f /usr/local/lib/python3.6/site-packages/cement.egg-link \
+ && cd src/cement \
+ && python setup.py install \
+ && cd /app \
+ && python setup.py install \
+ && rm -rf /app
+WORKDIR /
+ENTRYPOINT ["{{ label }}"]
diff --git a/cement/cli/templates/generate/project-loaded/LICENSE.md b/cement/cli/templates/generate/project-loaded/LICENSE.md
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/cement/cli/templates/generate/project-loaded/LICENSE.md
@@ -0,0 +1 @@
+
diff --git a/cement/cli/templates/generate/project-loaded/MANIFEST.in b/cement/cli/templates/generate/project-loaded/MANIFEST.in
new file mode 100644
index 00000000..8c2bf41d
--- /dev/null
+++ b/cement/cli/templates/generate/project-loaded/MANIFEST.in
@@ -0,0 +1,4 @@
+recursive-include *.py
+include setup.cfg
+include README.md CHANGELOG.md LICENSE.md
+include *.txt
diff --git a/cement/cli/templates/generate/project/Makefile b/cement/cli/templates/generate/project-loaded/Makefile
similarity index 100%
rename from cement/cli/templates/generate/project/Makefile
rename to cement/cli/templates/generate/project-loaded/Makefile
diff --git a/cement/cli/templates/generate/project-loaded/README.md b/cement/cli/templates/generate/project-loaded/README.md
new file mode 100644
index 00000000..ce27e04a
--- /dev/null
+++ b/cement/cli/templates/generate/project-loaded/README.md
@@ -0,0 +1,85 @@
+# {{ description }}
+
+## Installation
+
+```
+$ pip install -r requirements.txt
+
+$ pip install setup.py
+```
+
+## Development
+
+### Environment Setup
+
+This project includes a basic Docker Compose configuration that will setup a local development environment with all dependencies, and services required for development and testing.
+
+```
+$ make dev
+[...]
+|> {{ label }} <| app #
+```
+
+The `{{ label }}` command line application is installed in `develop` mode, therefore all changes will be live and can be tested immediately as code is modified.
+
+```
+|> {{ label }} <| app # {{ label }} --help
+```
+
+### Running Tests
+
+Execute tests from within the development environment:
+
+```
+|> {{ label }} <| app # make test
+```
+
+
+### Releasing to PyPi
+
+Before releasing to PyPi, you must configure your login credentials:
+
+**~/.pypirc**:
+
+```
+[pypi]
+username = YOUR_USERNAME
+password = YOUR_PASSWORD
+```
+
+Then use the included helper function via the `Makefile`:
+
+```
+$ make dist
+
+$ make dist-upload
+```
+
+## Deployments
+
+### Docker
+
+Included is a basic `Dockerfile` for building and distributing `{{ name }}`,
+and can be built with the included `make` helper:
+
+```
+$ make docker
+
+$ docker run -it {{ label }} --help
+usage: {{ label }} [-h] [--debug] [--quiet] [-o {json,yaml}] [-v] {command1} ...
+
+{{ description }}
+
+optional arguments:
+ -h, --help show this help message and exit
+ --debug toggle debug output
+ --quiet suppress all output
+ -o {json,yaml} output handler
+ -v, --version show program's version number and exit
+
+sub-commands:
+ {command1}
+ command1 example sub command1
+
+Usage: {{ title }} command1 --foo bar
+```
diff --git a/cement/cli/templates/generate/project/config/{{ label }}.yml.example b/cement/cli/templates/generate/project-loaded/config/{{ label }}.yml.example
similarity index 100%
rename from cement/cli/templates/generate/project/config/{{ label }}.yml.example
rename to cement/cli/templates/generate/project-loaded/config/{{ label }}.yml.example
diff --git a/cement/cli/templates/generate/project-loaded/docker-compose.yml b/cement/cli/templates/generate/project-loaded/docker-compose.yml
new file mode 100644
index 00000000..777fa033
--- /dev/null
+++ b/cement/cli/templates/generate/project-loaded/docker-compose.yml
@@ -0,0 +1,12 @@
+version: "3"
+services:
+ {{ label }}:
+ image: "{{ label }}:dev"
+ build:
+ context: .
+ dockerfile: docker/Dockerfile.dev
+ hostname: cement
+ stdin_open: true
+ tty: true
+ volumes:
+ - ".:/app"
diff --git a/cement/cli/templates/generate/project/docker/Dockerfile.dev b/cement/cli/templates/generate/project-loaded/docker/Dockerfile.dev
similarity index 100%
rename from cement/cli/templates/generate/project/docker/Dockerfile.dev
rename to cement/cli/templates/generate/project-loaded/docker/Dockerfile.dev
diff --git a/cement/cli/templates/generate/project/requirements-dev.txt b/cement/cli/templates/generate/project-loaded/requirements-dev.txt
similarity index 100%
rename from cement/cli/templates/generate/project/requirements-dev.txt
rename to cement/cli/templates/generate/project-loaded/requirements-dev.txt
diff --git a/cement/cli/templates/generate/project-loaded/requirements.txt b/cement/cli/templates/generate/project-loaded/requirements.txt
new file mode 100644
index 00000000..d0978892
--- /dev/null
+++ b/cement/cli/templates/generate/project-loaded/requirements.txt
@@ -0,0 +1,6 @@
+### FIXME: Replace with 'cement==3.0.0' once it is stable
+-e git+https://github.com/datafolklabs/cement.git@portland#egg=cement
+
+jinja2
+pyyaml
+colorlog
diff --git a/cement/cli/templates/generate/project-loaded/setup.cfg b/cement/cli/templates/generate/project-loaded/setup.cfg
new file mode 100644
index 00000000..e69de29b
diff --git a/cement/cli/templates/generate/project-loaded/setup.py b/cement/cli/templates/generate/project-loaded/setup.py
new file mode 100644
index 00000000..e37d9877
--- /dev/null
+++ b/cement/cli/templates/generate/project-loaded/setup.py
@@ -0,0 +1,28 @@
+
+from setuptools import setup, find_packages
+from {{ label }}.core.version import get_version
+
+VERSION = get_version()
+
+f = open('README.md', 'r')
+LONG_DESCRIPTION = f.read()
+f.close()
+
+setup(
+ name='{{ label }}',
+ version=VERSION,
+ description='{{ description }}',
+ long_description=LONG_DESCRIPTION,
+ long_description_content_type='text/markdown',
+ author='{{ creator }}',
+ author_email='{{ creator_email }}',
+ url='{{ url }}',
+ license='{{ license }}',
+ packages=find_packages(exclude=['ez_setup', 'tests*']),
+ package_data={'{{ label }}': ['templates/*']},
+ include_package_data=True,
+ entry_points="""
+ [console_scripts]
+ {{ label }} = {{ label }}.main:main
+ """,
+)
diff --git a/cement/cli/templates/generate/project-loaded/tests/conftest.py b/cement/cli/templates/generate/project-loaded/tests/conftest.py
new file mode 100644
index 00000000..4e3d4d70
--- /dev/null
+++ b/cement/cli/templates/generate/project-loaded/tests/conftest.py
@@ -0,0 +1,27 @@
+"""
+PyTest Fixtures.
+"""
+
+import os
+import shutil
+import pytest
+from tempfile import mkstemp, mkdtemp
+
+
+@pytest.fixture(scope="function")
+def tmp(request):
+ """
+ Create a `tmp` object that geneates a unique temporary directory, and file
+ for each test function that requires it.
+ """
+
+ class Tmp(object):
+ def __init__(self):
+ self.dir = mkdtemp()
+ _, self.file = mkstemp(dir=self.dir)
+ t = Tmp()
+ yield t
+
+ # cleanup
+ if os.path.exists(t.dir):
+ shutil.rmtree(t.dir)
diff --git a/cement/cli/templates/generate/project-loaded/tests/test_main.py b/cement/cli/templates/generate/project-loaded/tests/test_main.py
new file mode 100644
index 00000000..5f3a3f27
--- /dev/null
+++ b/cement/cli/templates/generate/project-loaded/tests/test_main.py
@@ -0,0 +1,13 @@
+
+from {{ label }}.main import {{ class_name}}Test
+
+def test_{{ label }}(tmp):
+ with {{ class_name }}Test() as app:
+ res = app.run()
+ print(res)
+ raise Exception
+
+def test_command1(tmp):
+ argv = ['command1']
+ with {{ class_name }}Test(argv=argv) as app:
+ app.run()
diff --git a/cement/cli/templates/generate/project-loaded/{{ label }}/__init__.py b/cement/cli/templates/generate/project-loaded/{{ label }}/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/cement/cli/templates/generate/project-loaded/{{ label }}/bootstrap.py b/cement/cli/templates/generate/project-loaded/{{ label }}/bootstrap.py
new file mode 100644
index 00000000..acc79a39
--- /dev/null
+++ b/cement/cli/templates/generate/project-loaded/{{ label }}/bootstrap.py
@@ -0,0 +1,5 @@
+
+from .controllers.base import Base
+
+def load(app):
+ app.handler.register(Base)
diff --git a/cement/cli/templates/generate/project-loaded/{{ label }}/controllers/__init__.py b/cement/cli/templates/generate/project-loaded/{{ label }}/controllers/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/cement/cli/templates/generate/project-loaded/{{ label }}/controllers/base.py b/cement/cli/templates/generate/project-loaded/{{ label }}/controllers/base.py
new file mode 100644
index 00000000..5db1041b
--- /dev/null
+++ b/cement/cli/templates/generate/project-loaded/{{ label }}/controllers/base.py
@@ -0,0 +1,61 @@
+
+from cement import Controller, ex
+from ..core.version import get_version
+
+VERSION_BANNER = """
+{{ description }}
+Version: %s
+Created by: {{ creator }}
+URL: {{ url }}
+""" % get_version()
+
+
+class Base(Controller):
+ class Meta:
+ label = 'base'
+
+ # text displayed at the top of --help output
+ description = '{{ description }}'
+
+ # text displayed at the bottom of --help output
+ epilog = 'Usage: {{ label }} command1 --foo bar'
+
+ # controller level arguments. ex: '{{ label }} --version'
+ arguments = [
+ ### add a version banner
+ ( [ '-v', '--version' ],
+ { 'action' : 'version',
+ 'version' : VERSION_BANNER } ),
+ ]
+
+
+ def _default(self):
+ """Default action if no sub-command is passed."""
+
+ self.app.args.print_help()
+
+
+ @ex(
+ help='example sub command1',
+
+ # sub-command level arguments. ex: '{{ label }} command1 --foo bar'
+ arguments=[
+ ### add a sample foo option under subcommand namespace
+ ( [ '-f', '--foo' ],
+ { 'help' : 'notorious foo option',
+ 'action' : 'store',
+ 'dest' : 'foo' } ),
+ ],
+ )
+ def command1(self):
+ """Example sub-command."""
+
+ data = {
+ 'foo' : 'bar',
+ }
+
+ ### do something with arguments
+ if self.app.pargs.foo is not None:
+ data['foo'] = self.app.pargs.foo
+
+ self.app.render(data, 'command1.jinja2')
diff --git a/cement/cli/templates/generate/project-loaded/{{ label }}/core/__init__.py b/cement/cli/templates/generate/project-loaded/{{ label }}/core/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/cement/cli/templates/generate/project-loaded/{{ label }}/core/exc.py b/cement/cli/templates/generate/project-loaded/{{ label }}/core/exc.py
new file mode 100644
index 00000000..c86ffb73
--- /dev/null
+++ b/cement/cli/templates/generate/project-loaded/{{ label }}/core/exc.py
@@ -0,0 +1,13 @@
+
+class {{ class_name }}Error(Exception):
+ """Generic errors."""
+
+ def __init__(self, msg):
+ Exception.__init__(self)
+ self.msg = msg
+
+ def __str__(self):
+ return self.msg
+
+ def __repr__(self):
+ return "<{{ class_name }}Error - %s>" % self.msg
diff --git a/cement/cli/templates/generate/project-loaded/{{ label }}/core/version.py b/cement/cli/templates/generate/project-loaded/{{ label }}/core/version.py
new file mode 100644
index 00000000..d130f858
--- /dev/null
+++ b/cement/cli/templates/generate/project-loaded/{{ label }}/core/version.py
@@ -0,0 +1,7 @@
+
+from cement.utils.version import get_version as cement_get_version
+
+VERSION = (0, 0, 1, 'alpha', 0)
+
+def get_version(version=VERSION):
+ return cement_get_version(version)
diff --git a/cement/cli/templates/generate/project-loaded/{{ label }}/ext/__init__.py b/cement/cli/templates/generate/project-loaded/{{ label }}/ext/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/cement/cli/templates/generate/project-loaded/{{ label }}/main.py b/cement/cli/templates/generate/project-loaded/{{ label }}/main.py
new file mode 100644
index 00000000..130bed0b
--- /dev/null
+++ b/cement/cli/templates/generate/project-loaded/{{ label }}/main.py
@@ -0,0 +1,90 @@
+
+from cement import App, init_defaults
+from cement.core.exc import CaughtSignal
+from .core.exc import {{ class_name }}Error
+
+
+# configuration defaults
+DEFAULTS = init_defaults('{{ label }}')
+DEFAULTS['{{ label }}']['{{ foo }}'] = 'bar'
+
+
+class {{ class_name }}(App):
+ """{{ name }} primary application."""
+
+ class Meta:
+ label = '{{ label }}'
+
+ # offload handler/hook registration to a separate module
+ bootstrap = '{{ label }}.bootstrap'
+
+ # configuration defaults
+ config_defaults = DEFAULTS
+
+ # load additional framework extensions
+ extensions = [
+ 'json',
+ 'yaml',
+ 'colorlog',
+ 'jinja2',
+ ]
+
+ # configuration handler
+ config_handler = 'yaml'
+
+ # configuration file suffix
+ config_file_suffix = '.yml'
+
+ # set the log handler
+ log_handler = 'colorlog'
+
+ # set the output handler
+ output_handler = 'jinja2'
+
+ # call sys.exit() on close
+ close_on_exit = True
+
+
+class {{ class_name }}Test({{ class_name}}):
+ """A test app that is better suited for testing."""
+
+ class Meta:
+ # default argv to empty (don't use sys.argv)
+ argv = []
+
+ # don't look for config files (could break tests)
+ config_files = []
+
+ # don't call sys.exit() when app.close() is called in tests
+ exit_on_close = False
+
+
+def main():
+ with {{ class_name }}() as app:
+ try:
+ app.run()
+
+ except AssertionError as e:
+ print('AssertionError > %s' % e.args[0])
+ app.exit_code = 1
+
+ if app.debug is True:
+ import traceback
+ traceback.print_exc()
+
+ except {{ class_name}}Error:
+ print('{{ class_name }}Error > %s' % e.args[0])
+ app.exit_code = 1
+
+ if app.debug is True:
+ import traceback
+ traceback.print_exc()
+
+ except CaughtSignal as e:
+ # Default Cement signals are SIGINT and SIGTERM, exit 0 (non-error)
+ print('\n%s' % e)
+ app.exit_code = 0
+
+
+if __name__ == '__main__':
+ main()
diff --git a/cement/cli/templates/generate/project-loaded/{{ label }}/plugins/__init__.py b/cement/cli/templates/generate/project-loaded/{{ label }}/plugins/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/cement/cli/templates/generate/project-loaded/{{ label }}/templates/__init__.py b/cement/cli/templates/generate/project-loaded/{{ label }}/templates/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/cement/cli/templates/generate/project/{{ label }}/templates/command1.jinja2 b/cement/cli/templates/generate/project-loaded/{{ label }}/templates/command1.jinja2
similarity index 100%
rename from cement/cli/templates/generate/project/{{ label }}/templates/command1.jinja2
rename to cement/cli/templates/generate/project-loaded/{{ label }}/templates/command1.jinja2
diff --git a/cement/cli/templates/generate/project/.generate.yml b/cement/cli/templates/generate/project/.generate.yml
index ceac5be0..9583624e 100644
--- a/cement/cli/templates/generate/project/.generate.yml
+++ b/cement/cli/templates/generate/project/.generate.yml
@@ -3,6 +3,11 @@
exclude:
- '^(.*)[\/\\\\]project[\/\\\\]{{ label }}[\/\\\\]templates[\/\\\\](.*)$'
+ignore:
+ - '^(.*)pyc(.*)$'
+ - '^(.*)pyo(.*)$'
+ - '^(.*)__pycache__(.*)$'
+
variables:
- name: label
prompt: "App Label"
diff --git a/cement/cli/templates/generate/project/Dockerfile b/cement/cli/templates/generate/project/Dockerfile
index 91dd3343..564cc38b 100644
--- a/cement/cli/templates/generate/project/Dockerfile
+++ b/cement/cli/templates/generate/project/Dockerfile
@@ -2,14 +2,15 @@ FROM python:3.6-alpine
MAINTAINER {{ creator }} <{{ creator_email }}>
WORKDIR /app
COPY . /app
-RUN apk update \
- && apk add git \
- && pip install --no-cache-dir -r requirements.txt \
+RUN apk update && \
+ apk add git && \
+ pip install --no-cache-dir -r requirements.txt \
&& rm -f /usr/local/lib/python3.6/site-packages/cement.egg-link \
&& cd src/cement \
&& python setup.py install \
&& cd /app \
- && python setup.py install \
- && rm -rf /app
+ && rm -rf src/cement \
+ && python setup.py install
+RUN rm -rf /app
WORKDIR /
ENTRYPOINT ["{{ label }}"]
diff --git a/cement/cli/templates/generate/project/MANIFEST.in b/cement/cli/templates/generate/project/MANIFEST.in
index 8c2bf41d..7bfc348b 100644
--- a/cement/cli/templates/generate/project/MANIFEST.in
+++ b/cement/cli/templates/generate/project/MANIFEST.in
@@ -2,3 +2,4 @@ recursive-include *.py
include setup.cfg
include README.md CHANGELOG.md LICENSE.md
include *.txt
+recursive-include {{ label }}/templates *
diff --git a/cement/cli/templates/generate/project/config/{{ label }}.conf.example b/cement/cli/templates/generate/project/config/{{ label }}.conf.example
new file mode 100644
index 00000000..069caee2
--- /dev/null
+++ b/cement/cli/templates/generate/project/config/{{ label }}.conf.example
@@ -0,0 +1,36 @@
+### {{ name }} Configuration Settings
+
+[{{ label }}]
+
+### Toggle application level debug (does not toggle framework debugging)
+# debug = false
+
+### Where external (third-party) plugins are loaded from
+# plugin_dir = /var/lib/{{ label }}/plugins/
+
+### Where all plugin configurations are loaded from
+# plugin_config_dir = /etc/{{ label }}/plugins.d/
+
+### sample foo option
+# foo = bar
+
+
+[log.logging]
+
+### Where the log file lives (no log file by default)
+# file =
+
+### The level for which to log. One of: info, warning, error, fatal, debug
+# level = info
+
+### Whether or not to log to console
+# to_console = true
+
+### Whether or not to rotate the log file when it reaches `max_bytes`
+# rotate = false
+
+### Max size in bytes that a log file can grow until it is rotated.
+# max_bytes = 512000
+
+### The maximun number of log files to maintain when rotating
+# max_files = 4
diff --git a/cement/cli/templates/generate/project/docker-compose.yml b/cement/cli/templates/generate/project/docker-compose.yml
index 777fa033..972e16ef 100644
--- a/cement/cli/templates/generate/project/docker-compose.yml
+++ b/cement/cli/templates/generate/project/docker-compose.yml
@@ -4,9 +4,11 @@ services:
image: "{{ label }}:dev"
build:
context: .
- dockerfile: docker/Dockerfile.dev
- hostname: cement
+ dockerfile: Dockerfile
+ hostname: {{ label }}
stdin_open: true
tty: true
+ working_dir: '/{{ label }}'
+ entrypoint: '/bin/ash'
volumes:
- - ".:/app"
+ - ".:/{{ label }}"
diff --git a/cement/cli/templates/generate/project/requirements.txt b/cement/cli/templates/generate/project/requirements.txt
index d0978892..e0ce94bd 100644
--- a/cement/cli/templates/generate/project/requirements.txt
+++ b/cement/cli/templates/generate/project/requirements.txt
@@ -1,6 +1,2 @@
### FIXME: Replace with 'cement==3.0.0' once it is stable
-e git+https://github.com/datafolklabs/cement.git@portland#egg=cement
-
-jinja2
-pyyaml
-colorlog
diff --git a/cement/cli/templates/generate/project/tests/conftest.py b/cement/cli/templates/generate/project/tests/conftest.py
index 4e3d4d70..6e1fe722 100644
--- a/cement/cli/templates/generate/project/tests/conftest.py
+++ b/cement/cli/templates/generate/project/tests/conftest.py
@@ -16,6 +16,8 @@ def tmp(request):
"""
class Tmp(object):
+ cleanup = True
+
def __init__(self):
self.dir = mkdtemp()
_, self.file = mkstemp(dir=self.dir)
@@ -23,5 +25,5 @@ def tmp(request):
yield t
# cleanup
- if os.path.exists(t.dir):
+ if os.path.exists(t.dir) and cleanup is True:
shutil.rmtree(t.dir)
diff --git a/cement/cli/templates/generate/project/{{ label }}/controllers/base.py b/cement/cli/templates/generate/project/{{ label }}/controllers/base.py
index 5db1041b..d41ebc45 100644
--- a/cement/cli/templates/generate/project/{{ label }}/controllers/base.py
+++ b/cement/cli/templates/generate/project/{{ label }}/controllers/base.py
@@ -1,13 +1,12 @@
from cement import Controller, ex
+from cement.utils.version import get_version_banner
from ..core.version import get_version
VERSION_BANNER = """
-{{ description }}
-Version: %s
-Created by: {{ creator }}
-URL: {{ url }}
-""" % get_version()
+{{ description }} %s
+%s
+""" % (get_version(), get_version_banner())
class Base(Controller):
@@ -54,8 +53,8 @@ class Base(Controller):
'foo' : 'bar',
}
+ self.app.log.info('Inside Base.command1()')
+
### do something with arguments
if self.app.pargs.foo is not None:
- data['foo'] = self.app.pargs.foo
-
- self.app.render(data, 'command1.jinja2')
+ print('Foo => %s' % self.app.pargs.foo)
diff --git a/cement/cli/templates/generate/project/{{ label }}/main.py b/cement/cli/templates/generate/project/{{ label }}/main.py
index 130bed0b..5aa14b79 100644
--- a/cement/cli/templates/generate/project/{{ label }}/main.py
+++ b/cement/cli/templates/generate/project/{{ label }}/main.py
@@ -2,7 +2,7 @@
from cement import App, init_defaults
from cement.core.exc import CaughtSignal
from .core.exc import {{ class_name }}Error
-
+from .controllers.base import Base
# configuration defaults
DEFAULTS = init_defaults('{{ label }}')
@@ -15,38 +15,20 @@ class {{ class_name }}(App):
class Meta:
label = '{{ label }}'
- # offload handler/hook registration to a separate module
- bootstrap = '{{ label }}.bootstrap'
-
# configuration defaults
config_defaults = DEFAULTS
- # load additional framework extensions
- extensions = [
- 'json',
- 'yaml',
- 'colorlog',
- 'jinja2',
- ]
-
- # configuration handler
- config_handler = 'yaml'
-
- # configuration file suffix
- config_file_suffix = '.yml'
-
- # set the log handler
- log_handler = 'colorlog'
-
- # set the output handler
- output_handler = 'jinja2'
-
# call sys.exit() on close
close_on_exit = True
+ # register handlers
+ handlers = [
+ Base
+ ]
+
class {{ class_name }}Test({{ class_name}}):
- """A test app that is better suited for testing."""
+ """A sub-class of {{ class_name}} that is better suited for testing."""
class Meta:
# default argv to empty (don't use sys.argv)
diff --git a/cement/cli/templates/generate/script/.generate.yml b/cement/cli/templates/generate/script/.generate.yml
index 7936e28a..9bf22381 100644
--- a/cement/cli/templates/generate/script/.generate.yml
+++ b/cement/cli/templates/generate/script/.generate.yml
@@ -1,5 +1,14 @@
---
+exclude:
+ - '^(.*)[\/\\\\]script[\/\\\\]{{ label }}[\/\\\\]templates[\/\\\\](.*)$'
+
+ignore:
+ - '^(.*)pyc(.*)$'
+ - '^(.*)pyo(.*)$'
+ - '^(.*)__pycache__(.*)$'
+
+
variables:
- name: label
prompt: "Script Name"
diff --git a/cement/core/template.py b/cement/core/template.py
index b400a4dd..fd37d81f 100644
--- a/cement/core/template.py
+++ b/cement/core/template.py
@@ -126,6 +126,12 @@ class TemplateHandler(TemplateInterface, Handler):
# must be provided by a subclass
raise NotImplemented # pragma: nocover
+ def _match_patterns(self, item, patterns):
+ for pattern in patterns:
+ if re.match(pattern, item):
+ return True
+ return False
+
def copy(self, src, dest, data, force=False, exclude=None, ignore=None):
"""
Render ``src`` directory as template, including directory and file
@@ -152,89 +158,109 @@ class TemplateHandler(TemplateInterface, Handler):
dest = fs.abspath(dest)
src = fs.abspath(src)
+
if exclude is None:
exclude = []
if ignore is None:
ignore = []
+ ignore_patterns = self._meta.ignore + ignore
+ exclude_patterns = self._meta.exclude + exclude
assert os.path.exists(src), "Source path %s does not exist!" % src
if not os.path.exists(dest):
os.makedirs(dest)
- self.app.log.debug('Copying source template %s -> %s' % (src, dest))
+ LOG.debug('copying source template %s -> %s' % (src, dest))
# here's the fun
for cur_dir, sub_dirs, files in os.walk(src):
if cur_dir == '.':
continue # pragma: nocover
-
- # don't render the source base dir (because we are telling it
- # where to go as `dest`)
- if cur_dir == src:
+ elif cur_dir == src:
+ # don't render the source base dir (because we are telling it
+ # where to go as `dest`)
cur_dir_dest = dest
+ elif self._match_patterns(cur_dir, ignore_patterns):
+ LOG.debug(
+ 'not copying ignored directory: %s' % cur_dir)
+ continue
+ elif self._match_patterns(cur_dir, exclude_patterns):
+ LOG.debug(
+ 'not rendering excluded directory as template: ' +
+ '%s' % cur_dir)
+ cur_dir_stub = re.sub(src, '', cur_dir)
+ cur_dir_stub = cur_dir_stub.lstrip('/')
+ cur_dir_stub = cur_dir_stub.lstrip('\\')
+ cur_dir_dest = os.path.join(dest, cur_dir_stub)
else:
# render the cur dir
- self.app.log.debug('rendering template %s' % cur_dir)
+ LOG.debug(
+ 'rendering directory as template: %s' % cur_dir)
cur_dir_stub = re.sub(src,
'',
self.render(cur_dir, data))
+
cur_dir_stub = cur_dir_stub.lstrip('/')
cur_dir_stub = cur_dir_stub.lstrip('\\')
cur_dir_dest = os.path.join(dest, cur_dir_stub)
# render sub-dirs
for sub_dir in sub_dirs:
- self.app.log.debug('rendering template %s' % sub_dir)
- new_sub_dir = re.sub(src,
- '',
- self.render(sub_dir, data))
- sub_dir_dest = os.path.join(cur_dir_dest, new_sub_dir)
+ full_path = os.path.join(cur_dir, sub_dir)
+
+ if self._match_patterns(full_path, ignore_patterns):
+ LOG.debug(
+ 'not copying ignored sub-directory: ' +
+ '%s' % full_path)
+ continue
+ elif self._match_patterns(full_path, exclude_patterns):
+ LOG.debug(
+ 'not rendering excluded sub-directory as template: ' +
+ '%s' % full_path)
+ sub_dir_dest = os.path.join(cur_dir_dest, sub_dir)
+ else:
+ LOG.debug(
+ 'rendering sub-directory as template: %s' % full_path)
+ new_sub_dir = re.sub(src,
+ '',
+ self.render(sub_dir, data))
+ sub_dir_dest = os.path.join(cur_dir_dest, new_sub_dir)
if not os.path.exists(sub_dir_dest):
- self.app.log.debug('Creating sub-directory %s' %
- sub_dir_dest)
+ LOG.debug('creating sub-directory %s' % sub_dir_dest)
os.makedirs(sub_dir_dest)
for _file in files:
- self.app.log.debug('rendering template %s' % _file)
new_file = re.sub(src, '', self.render(_file, data))
_file = fs.abspath(os.path.join(cur_dir, _file))
_file_dest = fs.abspath(os.path.join(cur_dir_dest, new_file))
- if force is True:
- LOG.debug('Overwriting existing file: %s ' % _file_dest)
- else:
- assert not os.path.exists(_file_dest), \
- 'Destination file already exists: %s ' % _file_dest
+ # handle if destination path already exists
- ignore_it = False
- all_patterns = self._meta.ignore + ignore
- for pattern in all_patterns:
- if re.match(pattern, _file):
- ignore_it = True
- break
+ if os.path.exists(_file_dest):
+ if force is True:
+ LOG.debug(
+ 'overwriting existing file: %s ' % _file_dest)
+ else:
+ assert False, \
+ 'Destination file already exists: %s ' % _file_dest
- if ignore_it is True:
- self.app.log.debug(
- 'Not copying ignored file: ' +
+ if self._match_patterns(_file, ignore_patterns):
+ LOG.debug(
+ 'not copying ignored file: ' +
'%s' % _file)
continue
- exclude_it = False
- all_patterns = self._meta.exclude + exclude
- for pattern in all_patterns:
- if re.match(pattern, _file):
- exclude_it = True
- break
-
- if exclude_it is True:
- self.app.log.debug(
- 'Not rendering excluded file as template: ' +
+ elif self._match_patterns(_file, exclude_patterns):
+ LOG.debug(
+ 'not rendering excluded file: ' +
'%s' % _file)
shutil.copy(_file, _file_dest)
+
else:
- f = open(os.path.join(cur_dir, _file), 'r')
+ LOG.debug('rendering file as template: %s' % _file)
+ f = open(_file, 'r')
content = f.read()
f.close()
@@ -251,8 +277,8 @@ class TemplateHandler(TemplateInterface, Handler):
template_path = template_path.lstrip('/')
full_path = fs.abspath(os.path.join(template_prefix,
template_path))
- LOG.debug("attemping to load output template from file %s" %
- full_path)
+ LOG.debug(
+ "attemping to load output template from file %s" % full_path)
if os.path.exists(full_path):
content = open(full_path, 'r').read()
LOG.debug("loaded output template from file %s" %
diff --git a/cement/ext/ext_generate.py b/cement/ext/ext_generate.py
index 0fa55965..f633bb9f 100644
--- a/cement/ext/ext_generate.py
+++ b/cement/ext/ext_generate.py
@@ -123,6 +123,7 @@ import os
import inspect
import yaml
from .. import Controller, minimal_logger, shell, FrameworkError
+from ..utils.version import VERSION, get_version
LOG = minimal_logger(__name__)
@@ -140,6 +141,14 @@ class GenerateTemplateAbstractBase(Controller):
self.app.log.info(msg)
data = {}
+ # builtin vars
+ maj_min = float('%s.%s' % (VERSION[0], VERSION[1]))
+ data['cement'] = {}
+ data['cement']['version'] = get_version()
+ data['cement']['major_version'] = VERSION[0]
+ data['cement']['minor_version'] = VERSION[1]
+ data['cement']['major_minor_version'] = maj_min
+
f = open(os.path.join(source, '.generate.yml'))
g_config = yaml.load(f)
f.close()
diff --git a/cement/utils/version.py b/cement/utils/version.py
index 61628028..de92da97 100644
--- a/cement/utils/version.py
+++ b/cement/utils/version.py
@@ -38,8 +38,9 @@
import datetime # pragma: nocover
import os # pragma: nocover
+import sys # pragma: nocover
import subprocess # pragma: nocover
-
+import platform # pragma: nocover
from ..core.backend import VERSION # pragma: nocover
@@ -71,6 +72,18 @@ def get_version(version=VERSION): # pragma: nocover
return main + sub
+def get_version_banner():
+ cement_ver = get_version()
+ python_ver = '.'.join([str(x) for x in sys.version_info[0:3]])
+ plat = platform.platform()
+
+ banner = 'Cement Framework %s\n' % cement_ver + \
+ 'Python %s\n' % python_ver + \
+ 'Platform %s' % plat
+
+ return banner
+
+
def get_git_changeset(): # pragma: nocover
"""Returns a numeric identifier of the latest git changeset.
diff --git a/docker/Dockerfile.dev b/docker/Dockerfile.dev
index 312c5822..b075b1e1 100644
--- a/docker/Dockerfile.dev
+++ b/docker/Dockerfile.dev
@@ -13,6 +13,7 @@ RUN apk update \
make \
vim \
bash \
+ git \
&& ln -sf /usr/bin/vim /usr/bin/vi \
&& pip install --no-cache-dir -r requirements-dev.txt
COPY . /app
diff --git a/docker/Dockerfile.docs b/docker/Dockerfile.docs
deleted file mode 100644
index 85e2532b..00000000
--- a/docker/Dockerfile.docs
+++ /dev/null
@@ -1,8 +0,0 @@
-FROM alpine:3.5
-MAINTAINER BJ Dierkes
-EXPOSE 8000
-WORKDIR /app
-RUN apk update && apk add hugo py-pygments
-COPY doc-new/ /app/
-COPY docker/bin/run-docs.sh /usr/bin/run-docs.sh
-CMD ["/usr/bin/run-docs.sh"]
diff --git a/setup.py b/setup.py
index 26091d68..e437f55d 100644
--- a/setup.py
+++ b/setup.py
@@ -11,16 +11,18 @@ f.close()
setup(name='cement',
version=VERSION,
- description='CLI Application Framework for Python',
+ description='CLI Framework for Python',
long_description=LONG,
long_description_content_type='text/markdown',
classifiers=[],
+ install_requires=[],
keywords='cli framework',
author='Data Folk Labs, LLC',
author_email='derks@datafolklabs.com',
url='http://builtoncement.org',
license='BSD',
packages=find_packages(exclude=['ez_setup', 'tests*']),
+ package_data={'cement': ['cement/cli/templates/generate/*']},
include_package_data=True,
zip_safe=False,
test_suite='nose.collector',
diff --git a/tests/cli/test_main.py b/tests/cli/test_main.py
index 2ad8f03d..bb3e6048 100644
--- a/tests/cli/test_main.py
+++ b/tests/cli/test_main.py
@@ -19,7 +19,7 @@ def test_app():
def test_generate(tmp):
- argv = ['generate', 'app', tmp.dir, '--defaults']
+ argv = ['generate', 'project', tmp.dir, '--defaults']
with App(argv=argv) as app:
app.run()
diff --git a/tests/conftest.py b/tests/conftest.py
index cb85739b..d166a73d 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -9,6 +9,8 @@ from tempfile import mkstemp, mkdtemp
@pytest.fixture(scope="function")
def tmp(request):
class Tmp(object):
+ cleanup = True
+
def __init__(self):
self.dir = mkdtemp()
_, self.file = mkstemp(dir=self.dir)
@@ -16,7 +18,7 @@ def tmp(request):
yield t
# cleanup
- if os.path.exists(t.dir):
+ if os.path.exists(t.dir) and t.cleanup is True:
shutil.rmtree(t.dir)
diff --git a/tests/data/templates/generate/test4/.generate.yml b/tests/data/templates/generate/test4/.generate.yml
new file mode 100644
index 00000000..ad74b68d
--- /dev/null
+++ b/tests/data/templates/generate/test4/.generate.yml
@@ -0,0 +1,8 @@
+---
+
+ignore:
+- '.*ignore-me.*'
+
+exclude:
+- '.*exclude-me.*'
+
diff --git a/tests/data/templates/generate/test4/exclude-me/take-me b/tests/data/templates/generate/test4/exclude-me/take-me
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/data/templates/generate/test4/ignore-me/take-me b/tests/data/templates/generate/test4/ignore-me/take-me
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/data/templates/generate/test4/take-me/exclude-me b/tests/data/templates/generate/test4/take-me/exclude-me
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/data/templates/generate/test4/take-me/ignore-me b/tests/data/templates/generate/test4/take-me/ignore-me
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/data/templates/generate/test4/take-me/take-me b/tests/data/templates/generate/test4/take-me/take-me
new file mode 100644
index 00000000..e69de29b
diff --git a/tests/ext/test_ext_generate.py b/tests/ext/test_ext_generate.py
index 1dbd9f1d..73c94e48 100644
--- a/tests/ext/test_ext_generate.py
+++ b/tests/ext/test_ext_generate.py
@@ -108,3 +108,20 @@ def test_generate_default_command(tmp):
argv = ['generate']
with GenerateApp(argv=argv) as app:
app.run()
+
+
+def test_filtered_sub_dirs(tmp):
+ tmp.cleanup = False
+ argv = ['generate', 'test4', tmp.dir, '--defaults']
+
+ with GenerateApp(argv=argv) as app:
+ app.run()
+
+ assert exists_join(tmp.dir, 'take-me')
+ assert exists_join(tmp.dir, 'take-me', 'take-me')
+ assert exists_join(tmp.dir, 'take-me', 'exclude-me')
+ assert not exists_join(tmp.dir, 'take-me', 'ignore-me')
+ assert exists_join(tmp.dir, 'exclude-me')
+ assert exists_join(tmp.dir, 'exclude-me', 'take-me')
+ assert not exists_join(tmp.dir, 'ignore-me')
+ assert not exists_join(tmp.dir, 'ignore-me', 'take-me')