Commit 66cced5d authored by Jannis Leidel's avatar Jannis Leidel

Moved tests out of the app to stay sane and changed a few things. E.g. the...

Moved tests out of the app to stay sane and changed a few things. E.g. the inclusion of JavaScript files now happens without charset (as they are deprecated in HTML5).
parent fdc05a64
build
compressor/tests/media/CACHE
compressor/tests/media/custom
compressor/tests/media/js/066cd253eada.js
tests/media/CACHE
tests/media/custom
tests/media/js/066cd253eada.js
dist
MANIFEST
*.pyc
......
......@@ -2,4 +2,4 @@ include AUTHORS
include README.rst
include LICENSE
recursive-include compressor/templates/compressor *.html
recursive-include compressor/tests/media *.js *.css *.png
recursive-include tests *.js *.css *.png *.py
This diff is collapsed.
......@@ -10,7 +10,7 @@ class CssCompressor(Compressor):
def __init__(self, content=None, output_prefix="css"):
super(CssCompressor, self).__init__(content, output_prefix)
self.filters = list(settings.COMPRESS_CSS_FILTERS)
self.type = 'css'
self.type = output_prefix
def split_contents(self):
if self.split_content:
......@@ -21,13 +21,9 @@ class CssCompressor(Compressor):
elem_name = self.parser.elem_name(elem)
elem_attribs = self.parser.elem_attribs(elem)
if elem_name == 'link' and elem_attribs['rel'] == 'stylesheet':
try:
basename = self.get_basename(elem_attribs['href'])
filename = self.get_filename(basename)
data = (SOURCE_FILE, filename, basename, elem)
except UncompressableFileError:
if settings.DEBUG:
raise
basename = self.get_basename(elem_attribs['href'])
filename = self.get_filename(basename)
data = (SOURCE_FILE, filename, basename, elem)
elif elem_name == 'style':
data = (SOURCE_HUNK, self.parser.elem_content(elem), None, elem)
if data:
......@@ -38,15 +34,15 @@ class CssCompressor(Compressor):
if self.media_nodes and self.media_nodes[-1][0] == media:
self.media_nodes[-1][1].split_content.append(data)
else:
node = CssCompressor(str(elem))
node = CssCompressor(self.parser.elem_str(elem))
node.split_content.append(data)
self.media_nodes.append((media, node))
return self.split_content
def output(self, *args, **kwargs):
# Populate self.split_content
if (settings.COMPRESS_ENABLED or settings.COMPRESS_PRECOMPILERS or
kwargs.get('forced', False)):
# Populate self.split_content
self.split_contents()
if hasattr(self, 'media_nodes'):
ret = []
......
......@@ -10,7 +10,7 @@ class JsCompressor(Compressor):
def __init__(self, content=None, output_prefix="js"):
super(JsCompressor, self).__init__(content, output_prefix)
self.filters = list(settings.COMPRESS_JS_FILTERS)
self.type = 'js'
self.type = output_prefix
def split_contents(self):
if self.split_content:
......@@ -18,14 +18,10 @@ class JsCompressor(Compressor):
for elem in self.parser.js_elems():
attribs = self.parser.elem_attribs(elem)
if 'src' in attribs:
try:
basename = self.get_basename(attribs['src'])
filename = self.get_filename(basename)
self.split_content.append(
(SOURCE_FILE, filename, basename, elem))
except UncompressableFileError:
if settings.DEBUG:
raise
basename = self.get_basename(attribs['src'])
filename = self.get_filename(basename)
content = (SOURCE_FILE, filename, basename, elem)
self.split_content.append(content)
else:
content = self.parser.elem_content(elem)
self.split_content.append((SOURCE_HUNK, content, None, elem))
......
<style type="text/css"{% if media %} media="{{ media }}"{% endif %}>{{ content|safe }}</style>
<style type="text/css"{% if media %} media="{{ media }}"{% endif %}>{{ content|safe }}</style>
\ No newline at end of file
<script type="text/javascript" src="{{ url }}" charset="utf-8"></script>
\ No newline at end of file
<script type="text/javascript" src="{{ url }}"></script>
\ No newline at end of file
import functools
class memoize(object):
def __init__ (self, func):
self.func = func
def __call__ (self, *args, **kwargs):
if (args, str(kwargs)) in self.__dict__:
value = self.__dict__[args, str(kwargs)]
else:
value = self.func(*args, **kwargs)
self.__dict__[args, str(kwargs)] = value
return value
def __repr__(self):
"""
Return the function's docstring.
"""
return self.func.__doc__ or ''
def __get__(self, obj, objtype):
"""
Support instance methods.
"""
return functools.partial(self.__call__, obj)
class cached_property(object):
......
......@@ -109,7 +109,7 @@ setup(
long_description = README,
author = 'Jannis Leidel',
author_email = 'jannis@leidel.info',
packages = find_packages(),
packages = find_packages(exclude=['tests', 'tests.*']),
package_data = find_package_data('compressor', only_in_packages=False),
classifiers = [
'Development Status :: 4 - Beta',
......
......@@ -14,7 +14,7 @@ if not settings.configured:
DATABASE_ENGINE='sqlite3',
INSTALLED_APPS=[
'compressor',
'compressor.tests',
'tests',
],
MEDIA_URL = '/media/',
MEDIA_ROOT = os.path.join(TEST_DIR, 'media'),
......@@ -30,14 +30,13 @@ from django.test.simple import run_tests
def runtests(*test_args):
if not test_args:
test_args = ['tests']
parent_dir = os.path.join(TEST_DIR, "..", "..")
parent_dir = os.path.join(TEST_DIR, "..")
sys.path.insert(0, parent_dir)
cov = coverage.coverage(branch=True,
include=[
os.path.join(parent_dir, 'compressor', '*.py')
],
omit=[
join(parent_dir, 'compressor', 'tests', '*.py'),
join(parent_dir, 'compressor', 'utils', 'stringformat.py'),
join(parent_dir, 'compressor', 'filters', 'jsmin', 'rjsmin.py'),
join(parent_dir, 'compressor', 'filters', 'cssmin', 'cssmin.py'),
......
from .base import CompressorTestCase, CssMediaTestCase, VerboseTestCase, CacheBackendTestCase
from .filters import CssTidyTestCase, CompassTestCase, PrecompilerTestCase, CssMinTestCase, CssAbsolutizingTestCase, CssDataUriTestCase
from .offline import OfflineGenerationTestCase
from .parsers import LxmlParserTests, Html5LibParserTests, BeautifulSoupParserTests, HtmlParserTests
from .storages import StorageTestCase
from .templatetags import TemplatetagTestCase
from __future__ import with_statement
import os
import re
from BeautifulSoup import BeautifulSoup
from django.core.cache.backends import locmem
from django.test import TestCase
from compressor.base import SOURCE_HUNK, SOURCE_FILE
from compressor.cache import get_hexdigest
from compressor.conf import settings
from compressor.css import CssCompressor
from compressor.js import JsCompressor
def css_tag(href, **kwargs):
rendered_attrs = ''.join(['%s="%s" ' % (k, v) for k, v in kwargs.items()])
template = u'<link rel="stylesheet" href="%s" type="text/css" %s/>'
return template % (href, rendered_attrs)
test_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
class CompressorTestCase(TestCase):
def setUp(self):
settings.COMPRESS_ENABLED = True
settings.COMPRESS_PRECOMPILERS = {}
settings.COMPRESS_DEBUG_TOGGLE = 'nocompress'
self.css = """\
<link rel="stylesheet" href="/media/css/one.css" type="text/css" />
<style type="text/css">p { border:5px solid green;}</style>
<link rel="stylesheet" href="/media/css/two.css" type="text/css" />"""
self.css_node = CssCompressor(self.css)
self.js = """\
<script src="/media/js/one.js" type="text/javascript"></script>
<script type="text/javascript">obj.value = "value";</script>"""
self.js_node = JsCompressor(self.js)
def test_css_split(self):
out = [
(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'css/one.css'), u'css/one.css', u'<link rel="stylesheet" href="/media/css/one.css" type="text/css" />'),
(SOURCE_HUNK, u'p { border:5px solid green;}', None, u'<style type="text/css">p { border:5px solid green;}</style>'),
(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'css/two.css'), u'css/two.css', u'<link rel="stylesheet" href="/media/css/two.css" type="text/css" />'),
]
split = self.css_node.split_contents()
split = [(x[0], x[1], x[2], self.css_node.parser.elem_str(x[3])) for x in split]
self.assertEqual(out, split)
def test_css_hunks(self):
out = ['body { background:#990; }', u'p { border:5px solid green;}', 'body { color:#fff; }']
hunks = [h for m, h in self.css_node.hunks()]
self.assertEqual(out, hunks)
def test_css_output(self):
out = u'body { background:#990; }\np { border:5px solid green;}\nbody { color:#fff; }'
hunks = '\n'.join([h for m, h in self.css_node.hunks()])
self.assertEqual(out, hunks)
def test_css_mtimes(self):
is_date = re.compile(r'^\d{10}[\.\d]+$')
for date in self.css_node.mtimes:
self.assertTrue(is_date.match(str(float(date))),
"mtimes is returning something that doesn't look like a date: %s" % date)
def test_css_return_if_off(self):
settings.COMPRESS_ENABLED = False
self.assertEqual(self.css, self.css_node.output())
def test_cachekey(self):
is_cachekey = re.compile(r'\w{12}')
self.assertTrue(is_cachekey.match(self.css_node.cachekey),
"cachekey is returning something that doesn't look like r'\w{12}'")
def test_css_return_if_on(self):
output = css_tag('/media/CACHE/css/e41ba2cc6982.css')
self.assertEqual(output, self.css_node.output().strip())
def test_js_split(self):
out = [
(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'js/one.js'), u'js/one.js', '<script src="/media/js/one.js" type="text/javascript"></script>'),
(SOURCE_HUNK, u'obj.value = "value";', None, '<script type="text/javascript">obj.value = "value";</script>')
]
split = self.js_node.split_contents()
split = [(x[0], x[1], x[2], self.js_node.parser.elem_str(x[3])) for x in split]
self.assertEqual(out, split)
def test_js_hunks(self):
out = ['obj = {};', u'obj.value = "value";']
hunks = [h for m, h in self.js_node.hunks()]
self.assertEqual(out, hunks)
def test_js_output(self):
out = u'<script type="text/javascript" src="/media/CACHE/js/066cd253eada.js"></script>'
self.assertEqual(out, self.js_node.output())
def test_js_return_if_off(self):
try:
enabled = settings.COMPRESS_ENABLED
precompilers = settings.COMPRESS_PRECOMPILERS
settings.COMPRESS_ENABLED = False
settings.COMPRESS_PRECOMPILERS = {}
self.assertEqual(self.js, self.js_node.output())
finally:
settings.COMPRESS_ENABLED = enabled
settings.COMPRESS_PRECOMPILERS = precompilers
def test_js_return_if_on(self):
output = u'<script type="text/javascript" src="/media/CACHE/js/066cd253eada.js"></script>'
self.assertEqual(output, self.js_node.output())
def test_custom_output_dir(self):
try:
old_output_dir = settings.COMPRESS_OUTPUT_DIR
settings.COMPRESS_OUTPUT_DIR = 'custom'
output = u'<script type="text/javascript" src="/media/custom/js/066cd253eada.js"></script>'
self.assertEqual(output, JsCompressor(self.js).output())
settings.COMPRESS_OUTPUT_DIR = ''
output = u'<script type="text/javascript" src="/media/js/066cd253eada.js"></script>'
self.assertEqual(output, JsCompressor(self.js).output())
settings.COMPRESS_OUTPUT_DIR = '/custom/nested/'
output = u'<script type="text/javascript" src="/media/custom/nested/js/066cd253eada.js"></script>'
self.assertEqual(output, JsCompressor(self.js).output())
finally:
settings.COMPRESS_OUTPUT_DIR = old_output_dir
class CssMediaTestCase(TestCase):
def setUp(self):
self.css = """\
<link rel="stylesheet" href="/media/css/one.css" type="text/css" media="screen">
<style type="text/css" media="print">p { border:5px solid green;}</style>
<link rel="stylesheet" href="/media/css/two.css" type="text/css" media="all">
<style type="text/css">h1 { border:5px solid green;}</style>"""
self.css_node = CssCompressor(self.css)
def test_css_output(self):
links = BeautifulSoup(self.css_node.output()).findAll('link')
media = [u'screen', u'print', u'all', None]
self.assertEqual(len(links), 4)
self.assertEqual(media, [l.get('media', None) for l in links])
def test_avoid_reordering_css(self):
css = self.css + '<style type="text/css" media="print">p { border:10px solid red;}</style>'
node = CssCompressor(css)
media = [u'screen', u'print', u'all', None, u'print']
links = BeautifulSoup(node.output()).findAll('link')
self.assertEqual(media, [l.get('media', None) for l in links])
class VerboseTestCase(CompressorTestCase):
def setUp(self):
super(VerboseTestCase, self).setUp()
settings.COMPRESS_VERBOSE = True
class CacheBackendTestCase(CompressorTestCase):
def test_correct_backend(self):
from compressor.cache import cache
self.assertEqual(cache.__class__, locmem.CacheClass)
from __future__ import with_statement
import os
from django.template import Template, Context
from django.test import TestCase
from compressor.conf import settings
from compressor.management.commands.compress import Command as CompressCommand
from .base import test_dir, css_tag
class OfflineGenerationTestCase(TestCase):
"""Uses templates/test_compressor_offline.html"""
maxDiff = None
def setUp(self):
self._old_compress = settings.COMPRESS_ENABLED
self._old_compress_offline = settings.COMPRESS_OFFLINE
settings.COMPRESS_ENABLED = True
settings.COMPRESS_OFFLINE = True
self.template_file = open(os.path.join(test_dir, "templates/test_compressor_offline.html"))
self.template = Template(self.template_file.read().decode(settings.FILE_CHARSET))
def tearDown(self):
settings.COMPRESS_ENABLED = self._old_compress
settings.COMPRESS_OFFLINE = self._old_compress_offline
self.template_file.close()
def test_offline(self):
count, result = CompressCommand().compress()
self.assertEqual(5, count)
self.assertEqual([
css_tag('/media/CACHE/css/cd579b7deb7d.css')+'\n',
u'<script type="text/javascript" src="/media/CACHE/js/0a2bb9a287c0.js"></script>',
u'<script type="text/javascript" src="/media/CACHE/js/fb1736ad48b7.js"></script>',
u'<script type="text/javascript" src="/media/CACHE/js/770a7311729e.js"></script>',
u'<link rel="stylesheet" href="/media/CACHE/css/67ed6aff7f7b.css" type="text/css" />\n',
], result)
# Template rendering should use the cache. FIXME: how to make sure of it ? Should we test the cache
# key<->values ourselves?
rendered_template = self.template.render(Context({})).replace("\n", "")
self.assertEqual(rendered_template, "".join(result).replace("\n", ""))
def test_offline_with_context(self):
self._old_offline_context = settings.COMPRESS_OFFLINE_CONTEXT
settings.COMPRESS_OFFLINE_CONTEXT = {
'color': 'blue',
}
count, result = CompressCommand().compress()
self.assertEqual(5, count)
self.assertEqual([
css_tag('/media/CACHE/css/ee62fbfd116a.css')+'\n',
u'<script type="text/javascript" src="/media/CACHE/js/0a2bb9a287c0.js"></script>',
u'<script type="text/javascript" src="/media/CACHE/js/fb1736ad48b7.js"></script>',
u'<script type="text/javascript" src="/media/CACHE/js/770a7311729e.js"></script>',
u'<link rel="stylesheet" href="/media/CACHE/css/73e015f740c6.css" type="text/css" />\n',
], result)
# Template rendering should use the cache. FIXME: how to make sure of it ? Should we test the cache
# key<->values ourselves?
rendered_template = self.template.render(Context(settings.COMPRESS_OFFLINE_CONTEXT)).replace("\n", "")
self.assertEqual(rendered_template, "".join(result).replace("\n", ""))
settings.COMPRESS_OFFLINE_CONTEXT = self._old_offline_context
def test_get_loaders(self):
old_loaders = settings.TEMPLATE_LOADERS
settings.TEMPLATE_LOADERS = (
('django.template.loaders.cached.Loader', (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)),
)
try:
from django.template.loaders.filesystem import Loader as FileSystemLoader
from django.template.loaders.app_directories import Loader as AppDirectoriesLoader
except ImportError:
pass
else:
loaders = CompressCommand().get_loaders()
self.assertTrue(isinstance(loaders[0], FileSystemLoader))
self.assertTrue(isinstance(loaders[1], AppDirectoriesLoader))
finally:
settings.TEMPLATE_LOADERS = old_loaders
from __future__ import with_statement
import os
from unittest2 import skipIf
from BeautifulSoup import BeautifulSoup
try:
import lxml
except ImportError:
lxml = None
try:
import html5lib
except ImportError:
html5lib = None
try:
from BeautifulSoup import BeautifulSoup
except ImportError:
BeautifulSoup = None
from compressor.conf import settings
from compressor.base import SOURCE_HUNK, SOURCE_FILE
from .base import CompressorTestCase
class ParserTestCase(object):
def setUp(self):
self.old_parser = settings.COMPRESS_PARSER
settings.COMPRESS_PARSER = self.parser_cls
super(ParserTestCase, self).setUp()
def tearDown(self):
settings.COMPRESS_PARSER = self.old_parser
class LxmlParserTests(ParserTestCase, CompressorTestCase):
parser_cls = 'compressor.parser.LxmlParser'
LxmlParserTests = skipIf(lxml is None, 'lxml not found')(LxmlParserTests)
class Html5LibParserTests(ParserTestCase, CompressorTestCase):
parser_cls = 'compressor.parser.Html5LibParser'
def test_css_split(self):
out = [
(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'css/one.css'), u'css/one.css', u'<link href="/media/css/one.css" rel="stylesheet" type="text/css">'),
(SOURCE_HUNK, u'p { border:5px solid green;}', None, u'<style type="text/css">p { border:5px solid green;}</style>'),
(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'css/two.css'), u'css/two.css', u'<link href="/media/css/two.css" rel="stylesheet" type="text/css">'),
]
split = self.css_node.split_contents()
split = [(x[0], x[1], x[2], self.css_node.parser.elem_str(x[3])) for x in split]
self.assertEqual(out, split)
def test_js_split(self):
out = [
(SOURCE_FILE, os.path.join(settings.COMPRESS_ROOT, u'js/one.js'), u'js/one.js', u'<script src="/media/js/one.js" type="text/javascript"></script>'),
(SOURCE_HUNK, u'obj.value = "value";', None, u'<script type="text/javascript">obj.value = "value";</script>'),
]
split = self.js_node.split_contents()
split = [(x[0], x[1], x[2], self.js_node.parser.elem_str(x[3])) for x in split]
self.assertEqual(out, split)
Html5LibParserTests = skipIf(
html5lib is None, 'html5lib not found')(Html5LibParserTests)
class BeautifulSoupParserTests(ParserTestCase, CompressorTestCase):
parser_cls = 'compressor.parser.BeautifulSoupParser'
BeautifulSoupParserTests = skipIf(
BeautifulSoup is None, 'BeautifulSoup not found')(BeautifulSoupParserTests)
class HtmlParserTests(ParserTestCase, CompressorTestCase):
parser_cls = 'compressor.parser.HtmlParser'
from __future__ import with_statement
from django.core.files.storage import get_storage_class
from django.test import TestCase
from compressor import base
from compressor.conf import settings
from .base import css_tag
from .templatetags import render
class StorageTestCase(TestCase):
def setUp(self):
self._storage = base.default_storage
base.default_storage = get_storage_class(
'compressor.storage.GzipCompressorFileStorage')()
settings.COMPRESS_ENABLED = True
def tearDown(self):
base.default_storage = self._storage
def test_css_tag_with_storage(self):
template = u"""{% load compress %}{% compress css %}
<link rel="stylesheet" href="{{ MEDIA_URL }}css/one.css" type="text/css">
<style type="text/css">p { border:5px solid white;}</style>
<link rel="stylesheet" href="{{ MEDIA_URL }}css/two.css" type="text/css">
{% endcompress %}
"""
context = {'MEDIA_URL': settings.COMPRESS_URL}
out = css_tag("/media/CACHE/css/1d4424458f88.css")
self.assertEqual(out, render(template, context))
from __future__ import with_statement
from django.template import Template, Context, TemplateSyntaxError
from django.test import TestCase
from compressor.conf import settings
from .base import css_tag
def render(template_string, context_dict=None):
"""
A shortcut for testing template output.
"""
if context_dict is None:
context_dict = {}
c = Context(context_dict)
t = Template(template_string)
return t.render(c).strip()
class TemplatetagTestCase(TestCase):
def setUp(self):
self.old_enabled = settings.COMPRESS_ENABLED
settings.COMPRESS_ENABLED = True
self.context = {'MEDIA_URL': settings.COMPRESS_URL}
def tearDown(self):
settings.COMPRESS_ENABLED = self.old_enabled
def test_empty_tag(self):
template = u"""{% load compress %}{% compress js %}{% block js %}
{% endblock %}{% endcompress %}"""
self.assertEqual(u'', render(template, self.context))
def test_css_tag(self):
template = u"""{% load compress %}{% compress css %}
<link rel="stylesheet" href="{{ MEDIA_URL }}css/one.css" type="text/css">
<style type="text/css">p { border:5px solid green;}</style>
<link rel="stylesheet" href="{{ MEDIA_URL }}css/two.css" type="text/css">
{% endcompress %}"""
out = css_tag("/media/CACHE/css/e41ba2cc6982.css")
self.assertEqual(out, render(template, self.context))
def test_nonascii_css_tag(self):
template = u"""{% load compress %}{% compress css %}
<link rel="stylesheet" href="{{ MEDIA_URL }}css/nonasc.css" type="text/css">
<style type="text/css">p { border:5px solid green;}</style>
{% endcompress %}
"""
out = css_tag("/media/CACHE/css/799f6defe43c.css")
self.assertEqual(out, render(template, self.context))
def test_js_tag(self):
template = u"""{% load compress %}{% compress js %}
<script src="{{ MEDIA_URL }}js/one.js" type="text/javascript"></script>
<script type="text/javascript">obj.value = "value";</script>
{% endcompress %}
"""
out = u'<script type="text/javascript" src="/media/CACHE/js/066cd253eada.js"></script>'
self.assertEqual(out, render(template, self.context))
def test_nonascii_js_tag(self):
template = u"""{% load compress %}{% compress js %}
<script src="{{ MEDIA_URL }}js/nonasc.js" type="text/javascript"></script>
<script type="text/javascript">var test_value = "\u2014";</script>
{% endcompress %}
"""
out = u'<script type="text/javascript" src="/media/CACHE/js/e214fe629b28.js"></script>'
self.assertEqual(out, render(template, self.context))
def test_nonascii_latin1_js_tag(self):
template = u"""{% load compress %}{% compress js %}
<script src="{{ MEDIA_URL }}js/nonasc-latin1.js" type="text/javascript" charset="latin-1"></script>
<script type="text/javascript">var test_value = "\u2014";</script>
{% endcompress %}
"""
out = u'<script type="text/javascript" src="/media/CACHE/js/be9e078b5ca7.js"></script>'
self.assertEqual(out, render(template, self.context))
def test_compress_tag_with_illegal_arguments(self):
template = u"""{% load compress %}{% compress pony %}
<script type="pony/application">unicorn</script>
{% endcompress %}"""
self.assertRaises(TemplateSyntaxError, render, template, {})
def test_debug_toggle(self):
template = u"""{% load compress %}{% compress js %}
<script src="{{ MEDIA_URL }}js/one.js" type="text/javascript"></script>
<script type="text/javascript">obj.value = "value";</script>
{% endcompress %}
"""
class MockDebugRequest(object):
GET = {settings.COMPRESS_DEBUG_TOGGLE: 'true'}
context = dict(self.context, request=MockDebugRequest())
out = u"""<script src="/media/js/one.js" type="text/javascript"></script>
<script type="text/javascript">obj.value = "value";</script>"""
self.assertEqual(out, render(template, context))
[testenv]
[tox]
setupdir = ..
distribute = false
sitepackages = true
downloadcache = {toxinidir}/_download/
[testenv]
commands =
{envpython} compressor/tests/runtests.py []
{envpython} {toxinidir}/runtests.py []
coverage html -d {envtmpdir}/coverage
[testenv:docs]
changedir = docs
changedir = ../docs
deps =
Sphinx
commands =
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment